mirror of https://github.com/crytic/slither
parent
369dbddeaf
commit
ce1f456daa
@ -0,0 +1,94 @@ |
|||||||
|
""" |
||||||
|
Check if an incorrect version of solc is used |
||||||
|
""" |
||||||
|
|
||||||
|
from slither.detectors.abstract_detector import AbstractDetector, DetectorClassification |
||||||
|
import re |
||||||
|
|
||||||
|
# group: |
||||||
|
# 0: ^ > >= < <= (optional) |
||||||
|
# 1: ' ' (optional) |
||||||
|
# 2: version number |
||||||
|
# 3: version number |
||||||
|
# 4: version number |
||||||
|
PATTERN = re.compile('(\^|>|>=|<|<=)?([ ]+)?(\d+)\.(\d+)\.(\d+)') |
||||||
|
|
||||||
|
class IncorrectSolc(AbstractDetector): |
||||||
|
""" |
||||||
|
Check if an old version of solc is used |
||||||
|
""" |
||||||
|
|
||||||
|
ARGUMENT = 'solc-version' |
||||||
|
HELP = 'Incorrect Solidity version (< 0.4.25 or complex pragma)' |
||||||
|
IMPACT = DetectorClassification.INFORMATIONAL |
||||||
|
CONFIDENCE = DetectorClassification.HIGH |
||||||
|
|
||||||
|
WIKI = 'https://github.com/trailofbits/slither/wiki/Vulnerabilities-Description#incorrect-version-of-solidity' |
||||||
|
|
||||||
|
COMPLEX_PRAGMA = "is has a complex pragma" |
||||||
|
OLD_VERSION = "it allows old versions" |
||||||
|
LESS_THAN = "it uses lesser than" |
||||||
|
|
||||||
|
# Indicates the allowed versions. |
||||||
|
ALLOWED_VERSIONS = ["0.4.25", "0.5.2", "0.5.3"] |
||||||
|
|
||||||
|
def _check_version(self, version): |
||||||
|
op = version[0] |
||||||
|
if op and not op in ['>', '>=', '^']: |
||||||
|
return self.LESS_THAN |
||||||
|
version_number = '.'.join(version[2:]) |
||||||
|
if version_number not in self.ALLOWED_VERSIONS: |
||||||
|
return self.OLD_VERSION |
||||||
|
return None |
||||||
|
|
||||||
|
def _check_pragma(self, version): |
||||||
|
versions = PATTERN.findall(version) |
||||||
|
if len(versions) == 1: |
||||||
|
version = versions[0] |
||||||
|
return self._check_version(version) |
||||||
|
elif len(versions) == 2: |
||||||
|
version_left = versions[0] |
||||||
|
version_right = versions[1] |
||||||
|
# Only allow two elements if the second one is |
||||||
|
# <0.5.0 or <0.6.0 |
||||||
|
if version_right not in [('<', '', '0', '5', '0'), ('<', '', '0', '6', '0')]: |
||||||
|
return self.COMPLEX_PRAGMA |
||||||
|
return self._check_version(version_left) |
||||||
|
else: |
||||||
|
return self.COMPLEX_PRAGMA |
||||||
|
def detect(self): |
||||||
|
""" |
||||||
|
Detects pragma statements that allow for outdated solc versions. |
||||||
|
:return: Returns the relevant JSON data for the findings. |
||||||
|
""" |
||||||
|
# Detect all version related pragmas and check if they are disallowed. |
||||||
|
results = [] |
||||||
|
pragma = self.slither.pragma_directives |
||||||
|
disallowed_pragmas = [] |
||||||
|
detected_version = False |
||||||
|
for p in pragma: |
||||||
|
# Skip any pragma directives which do not refer to version |
||||||
|
if len(p.directive) < 1 or p.directive[0] != "solidity": |
||||||
|
continue |
||||||
|
|
||||||
|
# This is version, so we test if this is disallowed. |
||||||
|
detected_version = True |
||||||
|
reason = self._check_pragma(p.version) |
||||||
|
if reason: |
||||||
|
disallowed_pragmas.append((reason, p)) |
||||||
|
|
||||||
|
# If we found any disallowed pragmas, we output our findings. |
||||||
|
if disallowed_pragmas: |
||||||
|
info = "Detected issues with version pragma in {}:\n".format(self.filename) |
||||||
|
for (reason, p) in disallowed_pragmas: |
||||||
|
info += "\t- {} ({}): {}\n".format(p, p.source_mapping_str, reason) |
||||||
|
self.log(info) |
||||||
|
|
||||||
|
json = self.generate_json_result(info) |
||||||
|
|
||||||
|
# follow the same format than add_nodes_to_json |
||||||
|
json['expressions'] = [{'expression': p.version, |
||||||
|
'source_mapping': p.source_mapping} for (reason, p) in disallowed_pragmas] |
||||||
|
results.append(json) |
||||||
|
|
||||||
|
return results |
@ -1,362 +0,0 @@ |
|||||||
""" |
|
||||||
Check if an old version of solc is used |
|
||||||
Solidity >= 0.4.23 should be used |
|
||||||
""" |
|
||||||
|
|
||||||
from slither.detectors.abstract_detector import AbstractDetector, DetectorClassification |
|
||||||
import re |
|
||||||
|
|
||||||
class OldSolc(AbstractDetector): |
|
||||||
""" |
|
||||||
Check if an old version of solc is used |
|
||||||
""" |
|
||||||
|
|
||||||
ARGUMENT = 'solc-version' |
|
||||||
HELP = 'Old versions of Solidity (< 0.4.23)' |
|
||||||
IMPACT = DetectorClassification.INFORMATIONAL |
|
||||||
CONFIDENCE = DetectorClassification.HIGH |
|
||||||
|
|
||||||
WIKI = 'https://github.com/trailofbits/slither/wiki/Vulnerabilities-Description#old-versions-of-solidity' |
|
||||||
|
|
||||||
CAPTURING_VERSION_PATTERN = re.compile("(?:(\d+|\*|x|X)\.(\d+|\*|x|X)\.(\d+|\*|x|X)|(\d+|\*|x|X)\.(\d+|\*|x|X)|(\d+|\*|x|X))") |
|
||||||
VERSION_PATTERN = "(?:(?:\d+|\*|x|X)\.(?:\d+|\*|x|X)\.(?:\d+|\*|x|X)|(?:\d+|\*|x|X)\.(?:\d+|\*|x|X)|(?:\d+|\*|x|X))" |
|
||||||
SPEC_PATTERN = re.compile(f"(?:(?:\s*({VERSION_PATTERN})\s*(\-)\s*({VERSION_PATTERN})\s*)|(?:(\^|\~|\>\s*=|\<\s*\=|\<|\>|\=|v|)\s*({VERSION_PATTERN})))(\s*\|\|\s*|\s*)") |
|
||||||
|
|
||||||
# Indicates the highest disallowed version. |
|
||||||
DISALLOWED_THRESHOLD = "0.4.22" |
|
||||||
|
|
||||||
class SemVerVersion(object): |
|
||||||
""" |
|
||||||
Represents a version in semver. Wildcards are supported. |
|
||||||
""" |
|
||||||
MAX_DIGIT_VALUE = 2**256 |
|
||||||
MIN_DIGIT_VALUE = -(2**256) |
|
||||||
|
|
||||||
def __init__(self, version, original_length=3): |
|
||||||
if not isinstance(version, list) or len(version) != 3: |
|
||||||
raise NotImplementedError("SemVer versions can only be initialized with a 3-element list.") |
|
||||||
self.version = version |
|
||||||
self.original_length = original_length |
|
||||||
|
|
||||||
def __str__(self): |
|
||||||
return f"{self.version[0] if self.version[0] is not None else '*'}.{self.version[1] if self.version[1] is not None else '*'}.{self.version[2] if self.version[2] is not None else '*'}" |
|
||||||
|
|
||||||
def __eq__(self, other): |
|
||||||
if not isinstance(other, OldSolc.SemVerVersion): |
|
||||||
return False |
|
||||||
|
|
||||||
# Loop through all digits looking for differences. |
|
||||||
for i in range(0, len(self.version)): |
|
||||||
if self.version[i] is None or other.version[i] is None or self.version[i] == other.version[i]: |
|
||||||
continue |
|
||||||
else: |
|
||||||
return False |
|
||||||
|
|
||||||
# We could not find a difference, they are equal. |
|
||||||
return True |
|
||||||
|
|
||||||
def __ne__(self, other): |
|
||||||
if not isinstance(other, OldSolc.SemVerVersion): |
|
||||||
return True |
|
||||||
return self.version != other.version |
|
||||||
|
|
||||||
def __lt__(self, other): |
|
||||||
if not isinstance(other, OldSolc.SemVerVersion): |
|
||||||
return False |
|
||||||
|
|
||||||
# Loop through all digits looking for one which is less. |
|
||||||
for i in range(0, len(self.version)): |
|
||||||
self_digit = self.MIN_DIGIT_VALUE if self.version[i] is None else self.version[i] |
|
||||||
other_digit = self.MIN_DIGIT_VALUE if other.version[i] is None else other.version[i] |
|
||||||
if self_digit == other_digit: |
|
||||||
continue |
|
||||||
elif self_digit < other_digit: |
|
||||||
return True |
|
||||||
else: |
|
||||||
return False |
|
||||||
|
|
||||||
# If we reach here, they are equal. |
|
||||||
return False |
|
||||||
|
|
||||||
def __le__(self, other): |
|
||||||
if not isinstance(other, OldSolc.SemVerVersion): |
|
||||||
return False |
|
||||||
return self == other or self < other |
|
||||||
|
|
||||||
def __gt__(self, other): |
|
||||||
if not isinstance(other, OldSolc.SemVerVersion): |
|
||||||
return False |
|
||||||
|
|
||||||
# Loop through all digits looking for one which is greater. |
|
||||||
for i in range(0, len(self.version)): |
|
||||||
self_digit = self.MAX_DIGIT_VALUE if self.version[i] is None else self.version[i] |
|
||||||
other_digit = self.MAX_DIGIT_VALUE if other.version[i] is None else other.version[i] |
|
||||||
if self_digit == other_digit: |
|
||||||
continue |
|
||||||
elif self_digit > other_digit: |
|
||||||
return True |
|
||||||
else: |
|
||||||
return False |
|
||||||
|
|
||||||
# If we reach here, they are equal. |
|
||||||
return False |
|
||||||
|
|
||||||
def __ge__(self, other): |
|
||||||
if not isinstance(other, OldSolc.SemVerVersion): |
|
||||||
return False |
|
||||||
return self == other or self > other |
|
||||||
|
|
||||||
def lower(self): |
|
||||||
return OldSolc.SemVerVersion([v if v is not None else self.MIN_DIGIT_VALUE for v in self.version]) |
|
||||||
|
|
||||||
def upper(self): |
|
||||||
return OldSolc.SemVerVersion([v if v is not None else self.MAX_DIGIT_VALUE for v in self.version]) |
|
||||||
|
|
||||||
class SemVerRange: |
|
||||||
""" |
|
||||||
Represents a range of versions supported in semver. |
|
||||||
""" |
|
||||||
def __init__(self, lower, upper, lower_inclusive=True, upper_inclusive=True): |
|
||||||
self.lower = lower |
|
||||||
self.upper = upper |
|
||||||
self.lower_inclusive = lower_inclusive |
|
||||||
self.upper_inclusive = upper_inclusive |
|
||||||
|
|
||||||
def __str__(self): |
|
||||||
return f"{{SemVerRange: {self.lower} <{'=' if self.upper_inclusive else ''} Version <{'=' if self.upper_inclusive else ''} {self.upper}}}" |
|
||||||
|
|
||||||
def intersection(self, other): |
|
||||||
""" |
|
||||||
Performs an intersection operation on both ranges. |
|
||||||
:param other: The other SemVerRange to perform the intersection with. |
|
||||||
:return: Returns a SemVerRange which is the intersection of this and the other range provided. |
|
||||||
""" |
|
||||||
low, high, low_inc, high_inc = self.lower, self.upper, self.lower_inclusive, self.upper_inclusive |
|
||||||
if other.lower > low or (other.lower == low and not other.lower_inclusive): |
|
||||||
low = other.lower |
|
||||||
low_inc = other.lower_inclusive |
|
||||||
if other.upper < high or (other.upper == high and not other.upper_inclusive): |
|
||||||
high = other.upper |
|
||||||
high_inc = other.upper_inclusive |
|
||||||
return OldSolc.SemVerRange(low, high, low_inc, high_inc) |
|
||||||
|
|
||||||
@property |
|
||||||
def is_valid(self): |
|
||||||
""" |
|
||||||
Indicates whether the range can encapsulate any values. |
|
||||||
:return: Returns True if the range can encapsulate any possible value, False if it is an invalid range. |
|
||||||
""" |
|
||||||
return self.lower < self.upper or \ |
|
||||||
(self.lower == self.upper and (self.lower_inclusive and self.upper_inclusive)) |
|
||||||
|
|
||||||
|
|
||||||
@property |
|
||||||
def max_version(self): |
|
||||||
return OldSolc.SemVerVersion([OldSolc.SemVerVersion.MAX_DIGIT_VALUE] * 3) |
|
||||||
|
|
||||||
@property |
|
||||||
def min_version(self): |
|
||||||
return OldSolc.SemVerVersion([OldSolc.SemVerVersion.MIN_DIGIT_VALUE] * 3) |
|
||||||
|
|
||||||
@staticmethod |
|
||||||
def _parse_version(version, unspecified_digit=None): |
|
||||||
""" |
|
||||||
Returns a 3-item array where each item is [major, minor, patch] version. |
|
||||||
-Either each number is an integer, or if it is a wildcard, it is None. |
|
||||||
:param version: The semver version string to parse. |
|
||||||
:return: The resulting SemVerVersion which represents major, minor and patch revisions. |
|
||||||
""" |
|
||||||
|
|
||||||
# Match the version pattern. |
|
||||||
match = OldSolc.CAPTURING_VERSION_PATTERN.findall(version) |
|
||||||
|
|
||||||
# If there was no matches (or more than one) the format is irregular, so we return None. |
|
||||||
if not match or len(match) > 1: |
|
||||||
return None |
|
||||||
|
|
||||||
# Filter all blank groups out. |
|
||||||
match = [int(y) if y.isdigit() else None for x in match for y in x if y] |
|
||||||
|
|
||||||
# Extend the array to a length of 3 and return it. |
|
||||||
original_length = len(match) |
|
||||||
match += [unspecified_digit] * max(0, 3 - original_length) |
|
||||||
return OldSolc.SemVerVersion(match, original_length) |
|
||||||
|
|
||||||
def _get_range(self, operation, version): |
|
||||||
""" |
|
||||||
Obtains a SemVerRange given an operation and version (a single spec item). |
|
||||||
:param operation: A string (typically single char) denoting the operation to perform on the provided version. |
|
||||||
:param version: The version string to perform an operation on to obtain a range. |
|
||||||
:return: Returns a SemVerRange that represents the range of values yielded from the given operation. |
|
||||||
""" |
|
||||||
# Assert our version state |
|
||||||
assert version.original_length > 0, "Original version should specify at least one digit" |
|
||||||
|
|
||||||
# Handle our range based off of operation type. |
|
||||||
if operation in [None, "", "=", "v"]: |
|
||||||
return OldSolc.SemVerRange(version.lower(), version.upper()) |
|
||||||
elif operation == ">": |
|
||||||
return OldSolc.SemVerRange(version.upper(), self.max_version, False, True) |
|
||||||
elif operation == ">=": |
|
||||||
return OldSolc.SemVerRange(version.upper(), self.max_version, True, True) |
|
||||||
elif operation == "<": |
|
||||||
return OldSolc.SemVerRange(self.min_version, version.lower(), True, False) |
|
||||||
elif operation == "<=": |
|
||||||
return OldSolc.SemVerRange(self.min_version, version.lower(), True, True) |
|
||||||
elif operation == "~": |
|
||||||
# Patch-level changes if minor version was defined, minor-level changes otherwise. |
|
||||||
low = version.lower() |
|
||||||
high = version.upper() |
|
||||||
|
|
||||||
# Determine which index we should increment based off how many were specified. |
|
||||||
increment_index = 0 if version.original_length == 1 else 1 |
|
||||||
|
|
||||||
# Increment the significant version digit and zero out the following ones. |
|
||||||
high.version[increment_index] += 1 |
|
||||||
for i in range(increment_index + 1, len(high.version)): |
|
||||||
high.version[i] = 0 |
|
||||||
|
|
||||||
# Our result is an exclusive upper bound, and inclusive lower. |
|
||||||
return OldSolc.SemVerRange(low, high, True, False) |
|
||||||
|
|
||||||
elif operation == "^": |
|
||||||
# The upper bound is determined by incrementing the first non-zero digit from left, and zeroing out all |
|
||||||
# following digits. |
|
||||||
low = version.lower() |
|
||||||
high = version.upper() |
|
||||||
|
|
||||||
# Determine the first significant digit (non-zero) from left. |
|
||||||
digit_index = len(high.version) - 1 |
|
||||||
for i in range(0, len(high.version)): |
|
||||||
if version.original_length < i + 1 or version.version[i] is None: |
|
||||||
digit_index = max(0, i - 1) |
|
||||||
break |
|
||||||
if high.version[i] != 0: |
|
||||||
digit_index = i |
|
||||||
break |
|
||||||
|
|
||||||
# Increment the digit and zero out all following digits. |
|
||||||
high.version[digit_index] += 1 |
|
||||||
for i in range(digit_index + 1, len(high.version)): |
|
||||||
high.version[i] = 0 |
|
||||||
|
|
||||||
# Our result is an exclusive upper bound, and inclusive lower. |
|
||||||
return OldSolc.SemVerRange(low, high, True, False) |
|
||||||
|
|
||||||
def _is_disallowed_pragma(self, version): |
|
||||||
""" |
|
||||||
Determines if a given version pragma is allowed (not outdated). |
|
||||||
:param version: The version string to check Solidity's semver is satisfied. |
|
||||||
:return: Returns a string with a reason why the pragma is disallowed, or returns None if it is valid. |
|
||||||
""" |
|
||||||
|
|
||||||
# First we parse the overall pragma statement, which could have multiple spec items in it (> x, <= y, etc). |
|
||||||
spec_items = self.SPEC_PATTERN.findall(version) |
|
||||||
|
|
||||||
# If we don't have any items, we return the appropriate error |
|
||||||
if not spec_items: |
|
||||||
return f"Untraditional or complex version spec" |
|
||||||
|
|
||||||
# Loop for each spec item, of which there are two kinds: |
|
||||||
# (1) <operator><version_operand> (standard) |
|
||||||
# (2) <version1> - <version2> (range) |
|
||||||
result_ranges = [] |
|
||||||
intersecting = False # True if this is an AND operation, False if it is an OR operation. |
|
||||||
for spec_item in spec_items: |
|
||||||
|
|
||||||
# Skip any results that don't have the correct count of items (to be safe) |
|
||||||
if len(spec_item) != 6: |
|
||||||
continue |
|
||||||
|
|
||||||
# If the first item does not exist, it is a case (1). |
|
||||||
if not spec_item[0]: |
|
||||||
# This is a range specified by a standard operation applied on a version. |
|
||||||
operation, version_operand = spec_item[3], self._parse_version(spec_item[4]) |
|
||||||
spec_range = self._get_range(operation, version_operand) |
|
||||||
else: |
|
||||||
# This is a range from a lower bound to upper bound. |
|
||||||
version_lower, operation, version_higher = self._parse_version(spec_item[0]).lower(), spec_item[1], \ |
|
||||||
self._parse_version(spec_item[2]).upper() |
|
||||||
spec_range = OldSolc.SemVerRange(version_lower.lower(), version_higher.upper(), True, True) |
|
||||||
|
|
||||||
# If we have no items, or we are performing a union, we simply add the range to our list |
|
||||||
if len(result_ranges) == 0 or not intersecting: |
|
||||||
result_ranges.append(spec_range) |
|
||||||
else: |
|
||||||
# If we are intersecting, we only intersect with the most recent versions. |
|
||||||
result_ranges[-1] = result_ranges[-1].intersection(spec_range) |
|
||||||
|
|
||||||
# Set our operation (AND/OR) from the captured end of this. |
|
||||||
intersecting = "||" not in spec_item[5] |
|
||||||
|
|
||||||
# Parse the newest disallowed version, and determine if we fall into the lower bound. |
|
||||||
newest_disallowed = self._parse_version(self.DISALLOWED_THRESHOLD, 0) |
|
||||||
|
|
||||||
# Verify any range doesn't allow as old or older than our newest disallowed. |
|
||||||
valid_ranges = 0 |
|
||||||
for result_range in result_ranges: |
|
||||||
|
|
||||||
# Skip any invalid ranges that would allow no versions through. |
|
||||||
if not result_range.is_valid: |
|
||||||
continue |
|
||||||
|
|
||||||
# Increment our valid ranges. |
|
||||||
valid_ranges += 1 |
|
||||||
|
|
||||||
# We now know this range allows some values through. If it's lower bound is less than the newest disallowed, |
|
||||||
# then it lets through the newest disallowed, or some lower values. |
|
||||||
if (result_range.lower_inclusive and newest_disallowed >= result_range.lower) \ |
|
||||||
or newest_disallowed > result_range.lower: |
|
||||||
return f"Version spec allows old versions of solidity (<={self.DISALLOWED_THRESHOLD})" |
|
||||||
|
|
||||||
# Verify we did allow some valid range of versions through. |
|
||||||
if valid_ranges == 0: |
|
||||||
return "Version spec does not allow any valid range of versions" |
|
||||||
else: |
|
||||||
return None |
|
||||||
|
|
||||||
def detect(self): |
|
||||||
""" |
|
||||||
Detects pragma statements that allow for outdated solc versions. |
|
||||||
:return: Returns the relevant JSON data for the findings. |
|
||||||
""" |
|
||||||
# Detect all version related pragmas and check if they are disallowed. |
|
||||||
results = [] |
|
||||||
pragma = self.slither.pragma_directives |
|
||||||
disallowed_pragmas = [] |
|
||||||
detected_version = False |
|
||||||
for p in pragma: |
|
||||||
# Skip any pragma directives which do not refer to version |
|
||||||
if len(p.directive) < 1 or p.directive[0] != "solidity": |
|
||||||
continue |
|
||||||
|
|
||||||
# This is version, so we test if this is disallowed. |
|
||||||
detected_version = True |
|
||||||
reason = self._is_disallowed_pragma(p.version) |
|
||||||
if reason: |
|
||||||
disallowed_pragmas.append((reason, p)) |
|
||||||
|
|
||||||
# If we found any disallowed pragmas, we output our findings. |
|
||||||
if disallowed_pragmas: |
|
||||||
info = "Detected issues with version pragma in {}:\n".format(self.filename) |
|
||||||
for (reason, p) in disallowed_pragmas: |
|
||||||
info += "\t- {} ({})\n".format(reason, p.source_mapping_str) |
|
||||||
self.log(info) |
|
||||||
|
|
||||||
json = self.generate_json_result(info) |
|
||||||
|
|
||||||
# follow the same format than add_nodes_to_json |
|
||||||
json['expressions'] = [{'expression': p.version, |
|
||||||
'source_mapping': p.source_mapping} for (reason, p) in disallowed_pragmas] |
|
||||||
results.append(json) |
|
||||||
|
|
||||||
# If we never detected a version-related pragma statement, output an error. |
|
||||||
elif not detected_version: |
|
||||||
# If we had no pragma statements, we warn the user that no version spec was included in this file. |
|
||||||
info = "No version pragma detected in {}\n".format(self.filename) |
|
||||||
self.log(info) |
|
||||||
|
|
||||||
json = self.generate_json_result(info) |
|
||||||
results.append(json) |
|
||||||
|
|
||||||
return results |
|
@ -1,7 +1,7 @@ |
|||||||
// The version pragma below should get flagged by the detector |
// The version pragma below should get flagged by the detector |
||||||
// Reason: The expression "^0.4.0 >0.4.2" allows old solc (<0.4.23) |
// Reason: The expression "^0.4.0 >0.4.2" allows old solc (<0.4.23) |
||||||
pragma solidity ^0.5.22 || 0.4.23 - 0.4.24 || ^0.4.0 >0.4.2 || ~0.0.0 ^0.5.0 <= 0.5.50 || > 0.7.0; |
pragma solidity ^0.4.24; |
||||||
|
pragma solidity >=0.4.0 <0.6.0; |
||||||
contract Contract{ |
contract Contract{ |
||||||
|
|
||||||
} |
} |
||||||
|
Loading…
Reference in new issue