Add re-entrancy detector

Add Prioritization information to README
pull/21/head
Josselin 6 years ago
parent bf8303b081
commit e18be0b483
  1. 21
      README.md
  2. 51
      examples/bugs/reentrancy.sol
  3. 5
      scripts/travis_test.sh
  4. 3
      slither/__main__.py
  5. 0
      slither/detectors/reentrancy/__init__.py
  6. 142
      slither/detectors/reentrancy/reentrancy.py

@ -55,17 +55,20 @@ If Slither is applied on a directory, it will run on every `.sol` file of the di
By default, all the checks are run.
Check | Purpose | Impact | Confidence
--- | --- | --- | ---
`--detect-uninitialized`| Detect uninitialized variables | High | High
`--detect-pragma`| Detect if different pragma directives are used | Informational | High
`--detect-solc-version`| Detect if an old version of Solidity is used (<0.4.23) | Informational | High
Check | Purpose | Impact | Confidence | Prioritization
--- | --- | --- | --- | ---
`--detect-uninitialized`| Detect uninitialized variables | High | High | High
`--detect-pragma`| Detect if different pragma directives are used | Informational | High | Informational
`--detect-reentrancy`| Detect if different pragma directives are used | High | Medium | Medium
`--detect-solc-version`| Detect if an old version of Solidity is used (<0.4.23) | Informational | High | Informational
A high prioritization check is likely to be a true positive with a severe impact.
### Exclude analyses
* `--exclude-informational`: Exclude informational impact analyses
* `--exclude-low`: Exclude low impact analyses
* `--exclude-medium`: Exclude medium impact analyses
* `--exclude-high`: Exclude high impact analyses
* `--exclude-informational`: Exclude informational prioritization analyses
* `--exclude-low`: Exclude low prioritization analyses
* `--exclude-medium`: Exclude medium prioritization impact analyses
* `--exclude-high`: Exclude high impact prioritization analyses
* `--exclude-name` will exclude the detector `name`
## Configuration

@ -0,0 +1,51 @@
pragma solidity ^0.4.24;
contract Reentrancy {
mapping (address => uint) userBalance;
function getBalance(address u) view public returns(uint){
return userBalance[u];
}
function addToBalance() payable public{
userBalance[msg.sender] += msg.value;
}
function withdrawBalance() public{
// send userBalance[msg.sender] ethers to msg.sender
// if mgs.sender is a contract, it will call its fallback function
if( ! (msg.sender.call.value(userBalance[msg.sender])() ) ){
revert();
}
userBalance[msg.sender] = 0;
}
function withdrawBalance_fixed() public{
// To protect against re-entrancy, the state variable
// has to be change before the call
uint amount = userBalance[msg.sender];
userBalance[msg.sender] = 0;
if( ! (msg.sender.call.value(amount)() ) ){
revert();
}
}
function withdrawBalance_fixed_2() public{
// send() and transfer() are safe against reentrancy
// they do not transfer the remaining gas
// and they give just enough gas to execute few instructions
// in the fallback function (no further call possible)
msg.sender.transfer(userBalance[msg.sender]);
userBalance[msg.sender] = 0;
}
function withdrawBalance_fixed_3() public{
// The state can be changed
// But it is fine, as it can only occur if the transaction fails
uint amount = userBalance[msg.sender];
userBalance[msg.sender] = 0;
if( ! (msg.sender.call.value(amount)() ) ){
userBalance[msg.sender] = amount;
}
}
}

@ -20,4 +20,9 @@ if [ $? -ne 1 ]; then
exit 1
fi
slither examples/bugs/reentrancy.sol --disable-solc-warnings
if [ $? -ne 1 ]; then
exit 1
fi
exit 0

@ -69,8 +69,9 @@ def main():
from slither.detectors.variables.uninitializedStateVarsDetection import UninitializedStateVarsDetection
from slither.detectors.attributes.constant_pragma import ConstantPragma
from slither.detectors.attributes.old_solc import OldSolc
from slither.detectors.reentrancy.reentrancy import Reentrancy
detectors = [Backdoor, UninitializedStateVarsDetection, ConstantPragma, OldSolc]
detectors = [Backdoor, UninitializedStateVarsDetection, ConstantPragma, OldSolc, Reentrancy]
from slither.printers.summary.printerSummary import PrinterSummary
from slither.printers.summary.printerQuickSummary import PrinterQuickSummary

@ -0,0 +1,142 @@
""""
Re-entrancy detection
Based on heuristics, it may lead to FP and FN
"""
from slither.core.declarations.function import Function
from slither.core.declarations.solidityVariables import SolidityFunction
from slither.core.expressions.unaryOperation import UnaryOperation, UnaryOperationType
from slither.core.cfg.node import NodeType
from slither.detectors.abstract_detector import AbstractDetector, DetectorClassification
from slither.visitors.expression.exportValues import ExportValues
class Reentrancy(AbstractDetector):
ARGUMENT = 'reentrancy'
HELP = 'Re-entrancy'
# Classified as medium, as the confience on the heuristic is not high
CLASSIFICATION = DetectorClassification.MEDIUM
@staticmethod
def _is_legit_call(call_name):
"""
Detect if the call seems legit
Slither has no taint analysis, and do not make yet the link
to the libraries. As a result, we look for any low-level calls
"""
call_str = str(call_name)
return not ('.call(' in call_str or
'.call.' in call_str or
'delegatecall' in call_str or
'callcode' in call_str or
'.value(' in call_str)
key = 'REENTRANCY'
def _check_on_call_returned(self, node):
"""
Check if the node is a condtional node where
there is an external call checked
Heuristic:
- The call is a IF node
- It contains a, external call
- The condition is the negation (!)
This will work only on naive implementation
"""
if node.type == NodeType.IF:
external_calls = node.external_calls
if any(not self._is_legit_call(call) for call in external_calls):
return isinstance(node.expression, UnaryOperation)\
and node.expression.type == UnaryOperationType.BANG
return False
def _explore(self, node, visited):
"""
Explore the CFG and look for re-entrancy
Heuristic: There is a re-entrancy if a state variable is written
after an external call
node.context will contains the external calls executed
It contains the calls executed in father nodes
if node.context is not empty, and variables are written, a re-entrancy is possible
"""
if node in visited:
return
visited = visited + [node]
# First we add the external calls executed in previous nodes
node.context[self.key] = []
for father in node.fathers:
if self.key in father.context:
node.context[self.key] += father.context[self.key]
# Get all the new external calls
for call in node.external_calls:
if self._is_legit_call(call):
continue
node.context[self.key] += [str(call)]
# All the state variables written
state_vars_written = node.state_variables_written
# Add the state variables written in internal calls
for internal_call in node.internal_calls:
# Filter to Function, as internal_call can be a solidity call
if isinstance(internal_call, Function):
state_vars_written += internal_call.all_state_variables_written()
# If a state variables is written, and there was an external call
# We found a potential re-entrancy bug
if state_vars_written and node.context[self.key]:
# we save the result wth (contract, func, calls) as key
# calls are ordered
finding_key = (node.function.contract.name,
node.function.name,
tuple(set(node.context[self.key])))
finding_vars = state_vars_written
if finding_key not in self.result:
self.result[finding_key] = []
self.result[finding_key] = list(set(self.result[finding_key] + finding_vars))
sons = node.sons
if self._check_on_call_returned(node):
sons = sons[1:]
for son in sons:
self._explore(son, visited)
def detect_reentrancy(self, contract):
"""
"""
for function in contract.functions:
if function.is_implemented:
self._explore(function.entry_point, [])
def detect(self):
"""
"""
self.result = {}
for c in self.contracts:
self.detect_reentrancy(c)
results = []
for (contract, func, calls), varsWritten in self.result.items():
varsWritten = list(set([str(x) for x in list(varsWritten)]))
calls = list(set([str(x) for x in list(calls)]))
info = 'Reentrancy in %s, Contract: %s, ' % (self.filename, contract) + \
'Func: %s, Call: %s, ' % (func, calls) + \
'Vars Written:%s' % (str(varsWritten))
self.log(info)
results.append({'vuln': 'Reentrancy',
# 'sourceMapping': sourceMapping,
'filename': self.filename,
'contract': contract,
'function_name': func,
'call': calls,
'varsWritten': varsWritten})
return results
Loading…
Cancel
Save