mirror of https://github.com/crytic/slither
commit
4214b932ad
@ -0,0 +1,48 @@ |
|||||||
|
--- |
||||||
|
body: |
||||||
|
- |
||||||
|
attributes: |
||||||
|
value: | |
||||||
|
Please check the issues tab to avoid duplicates. |
||||||
|
Thanks for taking the time to fill out this bug report! |
||||||
|
type: markdown |
||||||
|
- |
||||||
|
attributes: |
||||||
|
label: "Describe the issue:" |
||||||
|
id: what-happened |
||||||
|
type: textarea |
||||||
|
validations: |
||||||
|
required: true |
||||||
|
- |
||||||
|
attributes: |
||||||
|
description: "It can be a github repo, etherscan link, or code snippet." |
||||||
|
label: "Code example to reproduce the issue:" |
||||||
|
placeholder: "`contract A {}`\n" |
||||||
|
id: reproduce |
||||||
|
type: textarea |
||||||
|
validations: |
||||||
|
required: true |
||||||
|
- |
||||||
|
attributes: |
||||||
|
description: | |
||||||
|
What version of slither are you running? |
||||||
|
Run `slither --version` |
||||||
|
label: "Version:" |
||||||
|
id: version |
||||||
|
type: textarea |
||||||
|
validations: |
||||||
|
required: true |
||||||
|
- |
||||||
|
attributes: |
||||||
|
description: | |
||||||
|
Please copy and paste any relevant log output. This |
||||||
|
will be automatically formatted into code, so no need for backticks. |
||||||
|
render: shell |
||||||
|
label: "Relevant log output:" |
||||||
|
id: logs |
||||||
|
type: textarea |
||||||
|
description: "File a bug report" |
||||||
|
labels: |
||||||
|
- bug-candidate |
||||||
|
name: "Bug Report" |
||||||
|
title: "[Bug-Candidate]: " |
@ -0,0 +1,17 @@ |
|||||||
|
--- |
||||||
|
name: Feature request |
||||||
|
description: Suggest a feature |
||||||
|
labels: ["enhancement"] |
||||||
|
body: |
||||||
|
- type: markdown |
||||||
|
attributes: |
||||||
|
value: | |
||||||
|
Please check the issues tab to avoid duplicates. |
||||||
|
|
||||||
|
Thanks for providing feedback on Slither! |
||||||
|
- type: textarea |
||||||
|
attributes: |
||||||
|
label: Describe the desired feature |
||||||
|
description: Explain what the feature solves/ improves. |
||||||
|
validations: |
||||||
|
required: true |
@ -0,0 +1,42 @@ |
|||||||
|
--- |
||||||
|
name: IR tests |
||||||
|
|
||||||
|
defaults: |
||||||
|
run: |
||||||
|
# To load bashrc |
||||||
|
shell: bash -ieo pipefail {0} |
||||||
|
|
||||||
|
on: |
||||||
|
pull_request: |
||||||
|
branches: [master, dev] |
||||||
|
schedule: |
||||||
|
# run CI every day even if no PRs/merges occur |
||||||
|
- cron: '0 12 * * *' |
||||||
|
|
||||||
|
jobs: |
||||||
|
build: |
||||||
|
name: IR tests |
||||||
|
runs-on: ${{ matrix.os }} |
||||||
|
strategy: |
||||||
|
fail-fast: false |
||||||
|
matrix: |
||||||
|
os: [ubuntu-latest, windows-2022] |
||||||
|
|
||||||
|
steps: |
||||||
|
- name: Checkout Code |
||||||
|
uses: actions/checkout@v2 |
||||||
|
|
||||||
|
- name: Set up Python 3.8 |
||||||
|
uses: actions/setup-python@v3 |
||||||
|
with: |
||||||
|
python-version: 3.8 |
||||||
|
|
||||||
|
- name: Install dependencies |
||||||
|
run: | |
||||||
|
pip install ".[dev]" |
||||||
|
solc-select install all |
||||||
|
solc-select use 0.8.11 |
||||||
|
|
||||||
|
- name: Test with pytest |
||||||
|
run: | |
||||||
|
pytest tests/test_ssa_generation.py |
@ -0,0 +1,50 @@ |
|||||||
|
--- |
||||||
|
name: Test slither-read-storage |
||||||
|
|
||||||
|
defaults: |
||||||
|
run: |
||||||
|
# To load bashrc |
||||||
|
shell: bash -ieo pipefail {0} |
||||||
|
|
||||||
|
on: |
||||||
|
pull_request: |
||||||
|
branches: [master, dev] |
||||||
|
schedule: |
||||||
|
# run CI every day even if no PRs/merges occur |
||||||
|
- cron: '0 12 * * *' |
||||||
|
|
||||||
|
jobs: |
||||||
|
build: |
||||||
|
name: Test slither-read-storage |
||||||
|
runs-on: ubuntu-latest |
||||||
|
|
||||||
|
steps: |
||||||
|
- uses: actions/checkout@v2 |
||||||
|
- name: Setup node |
||||||
|
uses: actions/setup-node@v2 |
||||||
|
with: |
||||||
|
node-version: '14' |
||||||
|
|
||||||
|
- name: Install ganache |
||||||
|
run: npm install --global ganache |
||||||
|
|
||||||
|
- name: Set up Python 3.8 |
||||||
|
uses: actions/setup-python@v2 |
||||||
|
with: |
||||||
|
python-version: 3.8 |
||||||
|
|
||||||
|
- name: Install python dependencies |
||||||
|
run: | |
||||||
|
pip install ".[dev]" |
||||||
|
pip install web3 |
||||||
|
solc-select install 0.8.1 |
||||||
|
solc-select install 0.8.10 |
||||||
|
solc-select use 0.8.1 |
||||||
|
|
||||||
|
- name: Run slither-read-storage |
||||||
|
run: | |
||||||
|
pytest tests/test_read_storage.py |
||||||
|
|
||||||
|
- name: Run storage layout tests |
||||||
|
run: | |
||||||
|
pytest tests/test_storage_layout.py |
@ -0,0 +1,41 @@ |
|||||||
|
from typing import TYPE_CHECKING, Tuple |
||||||
|
|
||||||
|
from slither.core.children.child_contract import ChildContract |
||||||
|
from slither.core.declarations.top_level import TopLevel |
||||||
|
from slither.core.solidity_types import Type |
||||||
|
|
||||||
|
if TYPE_CHECKING: |
||||||
|
from slither.core.declarations import Contract |
||||||
|
from slither.core.scope.scope import FileScope |
||||||
|
|
||||||
|
|
||||||
|
class TypeAlias(Type): |
||||||
|
def __init__(self, underlying_type: Type, name: str): |
||||||
|
super().__init__() |
||||||
|
self.name = name |
||||||
|
self.underlying_type = underlying_type |
||||||
|
|
||||||
|
@property |
||||||
|
def storage_size(self) -> Tuple[int, bool]: |
||||||
|
return self.underlying_type.storage_size |
||||||
|
|
||||||
|
def __hash__(self): |
||||||
|
return hash(str(self)) |
||||||
|
|
||||||
|
|
||||||
|
class TypeAliasTopLevel(TypeAlias, TopLevel): |
||||||
|
def __init__(self, underlying_type: Type, name: str, scope: "FileScope"): |
||||||
|
super().__init__(underlying_type, name) |
||||||
|
self.file_scope: "FileScope" = scope |
||||||
|
|
||||||
|
def __str__(self): |
||||||
|
return self.name |
||||||
|
|
||||||
|
|
||||||
|
class TypeAliasContract(TypeAlias, ChildContract): |
||||||
|
def __init__(self, underlying_type: Type, name: str, contract: "Contract"): |
||||||
|
super().__init__(underlying_type, name) |
||||||
|
self._contract: "Contract" = contract |
||||||
|
|
||||||
|
def __str__(self): |
||||||
|
return self.contract.name + "." + self.name |
@ -0,0 +1,95 @@ |
|||||||
|
from typing import List |
||||||
|
from slither.core.cfg.node import Node |
||||||
|
from slither.core.declarations.solidity_variables import SolidityVariable |
||||||
|
from slither.slithir.operations import HighLevelCall, LibraryCall |
||||||
|
from slither.core.declarations import Contract, Function, SolidityVariableComposed |
||||||
|
from slither.analyses.data_dependency.data_dependency import is_dependent |
||||||
|
from slither.core.compilation_unit import SlitherCompilationUnit |
||||||
|
|
||||||
|
|
||||||
|
class ArbitrarySendErc20: |
||||||
|
"""Detects instances where ERC20 can be sent from an arbitrary from address.""" |
||||||
|
|
||||||
|
def __init__(self, compilation_unit: SlitherCompilationUnit): |
||||||
|
self._compilation_unit = compilation_unit |
||||||
|
self._no_permit_results: List[Node] = [] |
||||||
|
self._permit_results: List[Node] = [] |
||||||
|
|
||||||
|
@property |
||||||
|
def compilation_unit(self) -> SlitherCompilationUnit: |
||||||
|
return self._compilation_unit |
||||||
|
|
||||||
|
@property |
||||||
|
def no_permit_results(self) -> List[Node]: |
||||||
|
return self._no_permit_results |
||||||
|
|
||||||
|
@property |
||||||
|
def permit_results(self) -> List[Node]: |
||||||
|
return self._permit_results |
||||||
|
|
||||||
|
def _detect_arbitrary_from(self, contract: Contract): |
||||||
|
for f in contract.functions: |
||||||
|
all_high_level_calls = [ |
||||||
|
f_called[1].solidity_signature |
||||||
|
for f_called in f.high_level_calls |
||||||
|
if isinstance(f_called[1], Function) |
||||||
|
] |
||||||
|
all_library_calls = [f_called[1].solidity_signature for f_called in f.library_calls] |
||||||
|
if ( |
||||||
|
"transferFrom(address,address,uint256)" in all_high_level_calls |
||||||
|
or "safeTransferFrom(address,address,address,uint256)" in all_library_calls |
||||||
|
): |
||||||
|
if ( |
||||||
|
"permit(address,address,uint256,uint256,uint8,bytes32,bytes32)" |
||||||
|
in all_high_level_calls |
||||||
|
): |
||||||
|
ArbitrarySendErc20._arbitrary_from(f.nodes, self._permit_results) |
||||||
|
else: |
||||||
|
ArbitrarySendErc20._arbitrary_from(f.nodes, self._no_permit_results) |
||||||
|
|
||||||
|
@staticmethod |
||||||
|
def _arbitrary_from(nodes: List[Node], results: List[Node]): |
||||||
|
"""Finds instances of (safe)transferFrom that do not use msg.sender or address(this) as from parameter.""" |
||||||
|
for node in nodes: |
||||||
|
for ir in node.irs: |
||||||
|
if ( |
||||||
|
isinstance(ir, HighLevelCall) |
||||||
|
and isinstance(ir.function, Function) |
||||||
|
and ir.function.solidity_signature == "transferFrom(address,address,uint256)" |
||||||
|
and not ( |
||||||
|
is_dependent( |
||||||
|
ir.arguments[0], |
||||||
|
SolidityVariableComposed("msg.sender"), |
||||||
|
node.function.contract, |
||||||
|
) |
||||||
|
or is_dependent( |
||||||
|
ir.arguments[0], |
||||||
|
SolidityVariable("this"), |
||||||
|
node.function.contract, |
||||||
|
) |
||||||
|
) |
||||||
|
): |
||||||
|
results.append(ir.node) |
||||||
|
elif ( |
||||||
|
isinstance(ir, LibraryCall) |
||||||
|
and ir.function.solidity_signature |
||||||
|
== "safeTransferFrom(address,address,address,uint256)" |
||||||
|
and not ( |
||||||
|
is_dependent( |
||||||
|
ir.arguments[1], |
||||||
|
SolidityVariableComposed("msg.sender"), |
||||||
|
node.function.contract, |
||||||
|
) |
||||||
|
or is_dependent( |
||||||
|
ir.arguments[1], |
||||||
|
SolidityVariable("this"), |
||||||
|
node.function.contract, |
||||||
|
) |
||||||
|
) |
||||||
|
): |
||||||
|
results.append(ir.node) |
||||||
|
|
||||||
|
def detect(self): |
||||||
|
"""Detect transfers that use arbitrary `from` parameter.""" |
||||||
|
for c in self.compilation_unit.contracts_derived: |
||||||
|
self._detect_arbitrary_from(c) |
@ -0,0 +1,45 @@ |
|||||||
|
from typing import List |
||||||
|
from slither.detectors.abstract_detector import AbstractDetector, DetectorClassification |
||||||
|
from slither.utils.output import Output |
||||||
|
from .arbitrary_send_erc20 import ArbitrarySendErc20 |
||||||
|
|
||||||
|
|
||||||
|
class ArbitrarySendErc20NoPermit(AbstractDetector): |
||||||
|
""" |
||||||
|
Detect when `msg.sender` is not used as `from` in transferFrom |
||||||
|
""" |
||||||
|
|
||||||
|
ARGUMENT = "arbitrary-send-erc20" |
||||||
|
HELP = "transferFrom uses arbitrary `from`" |
||||||
|
IMPACT = DetectorClassification.HIGH |
||||||
|
CONFIDENCE = DetectorClassification.HIGH |
||||||
|
|
||||||
|
WIKI = "https://github.com/trailofbits/slither/wiki/Detector-Documentation#arbitrary-send-erc20" |
||||||
|
|
||||||
|
WIKI_TITLE = "Arbitrary `from` in transferFrom" |
||||||
|
WIKI_DESCRIPTION = "Detect when `msg.sender` is not used as `from` in transferFrom." |
||||||
|
WIKI_EXPLOIT_SCENARIO = """ |
||||||
|
```solidity |
||||||
|
function a(address from, address to, uint256 amount) public { |
||||||
|
erc20.transferFrom(from, to, am); |
||||||
|
} |
||||||
|
``` |
||||||
|
Alice approves this contract to spend her ERC20 tokens. Bob can call `a` and specify Alice's address as the `from` parameter in `transferFrom`, allowing him to transfer Alice's tokens to himself.""" |
||||||
|
|
||||||
|
WIKI_RECOMMENDATION = """ |
||||||
|
Use `msg.sender` as `from` in transferFrom. |
||||||
|
""" |
||||||
|
|
||||||
|
def _detect(self) -> List[Output]: |
||||||
|
"""""" |
||||||
|
results: List[Output] = [] |
||||||
|
|
||||||
|
arbitrary_sends = ArbitrarySendErc20(self.compilation_unit) |
||||||
|
arbitrary_sends.detect() |
||||||
|
for node in arbitrary_sends.no_permit_results: |
||||||
|
func = node.function |
||||||
|
info = [func, " uses arbitrary from in transferFrom: ", node, "\n"] |
||||||
|
res = self.generate_result(info) |
||||||
|
results.append(res) |
||||||
|
|
||||||
|
return results |
@ -0,0 +1,53 @@ |
|||||||
|
from typing import List |
||||||
|
from slither.detectors.abstract_detector import AbstractDetector, DetectorClassification |
||||||
|
from slither.utils.output import Output |
||||||
|
from .arbitrary_send_erc20 import ArbitrarySendErc20 |
||||||
|
|
||||||
|
|
||||||
|
class ArbitrarySendErc20Permit(AbstractDetector): |
||||||
|
""" |
||||||
|
Detect when `msg.sender` is not used as `from` in transferFrom along with the use of permit. |
||||||
|
""" |
||||||
|
|
||||||
|
ARGUMENT = "arbitrary-send-erc20-permit" |
||||||
|
HELP = "transferFrom uses arbitrary from with permit" |
||||||
|
IMPACT = DetectorClassification.HIGH |
||||||
|
CONFIDENCE = DetectorClassification.MEDIUM |
||||||
|
|
||||||
|
WIKI = "https://github.com/trailofbits/slither/wiki/Detector-Documentation#arbitrary-send-erc20-permit" |
||||||
|
|
||||||
|
WIKI_TITLE = "Arbitrary `from` in transferFrom used with permit" |
||||||
|
WIKI_DESCRIPTION = ( |
||||||
|
"Detect when `msg.sender` is not used as `from` in transferFrom and permit is used." |
||||||
|
) |
||||||
|
WIKI_EXPLOIT_SCENARIO = """ |
||||||
|
```solidity |
||||||
|
function bad(address from, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s, address to) public { |
||||||
|
erc20.permit(from, address(this), value, deadline, v, r, s); |
||||||
|
erc20.transferFrom(from, to, value); |
||||||
|
} |
||||||
|
``` |
||||||
|
If an ERC20 token does not implement permit and has a fallback function e.g. WETH, transferFrom allows an attacker to transfer all tokens approved for this contract.""" |
||||||
|
|
||||||
|
WIKI_RECOMMENDATION = """ |
||||||
|
Ensure that the underlying ERC20 token correctly implements a permit function. |
||||||
|
""" |
||||||
|
|
||||||
|
def _detect(self) -> List[Output]: |
||||||
|
"""""" |
||||||
|
results: List[Output] = [] |
||||||
|
|
||||||
|
arbitrary_sends = ArbitrarySendErc20(self.compilation_unit) |
||||||
|
arbitrary_sends.detect() |
||||||
|
for node in arbitrary_sends.permit_results: |
||||||
|
func = node.function |
||||||
|
info = [ |
||||||
|
func, |
||||||
|
" uses arbitrary from in transferFrom in combination with permit: ", |
||||||
|
node, |
||||||
|
"\n", |
||||||
|
] |
||||||
|
res = self.generate_result(info) |
||||||
|
results.append(res) |
||||||
|
|
||||||
|
return results |
@ -1,144 +0,0 @@ |
|||||||
""" |
|
||||||
Module detecting send to arbitrary address |
|
||||||
|
|
||||||
To avoid FP, it does not report: |
|
||||||
- If msg.sender is used as index (withdraw situation) |
|
||||||
- If the function is protected |
|
||||||
- If the value sent is msg.value (repay situation) |
|
||||||
- If there is a call to transferFrom |
|
||||||
|
|
||||||
TODO: dont report if the value is tainted by msg.value |
|
||||||
""" |
|
||||||
from typing import List |
|
||||||
|
|
||||||
from slither.core.cfg.node import Node |
|
||||||
from slither.core.declarations import Function, Contract |
|
||||||
from slither.analyses.data_dependency.data_dependency import is_tainted, is_dependent |
|
||||||
from slither.core.declarations.solidity_variables import ( |
|
||||||
SolidityFunction, |
|
||||||
SolidityVariableComposed, |
|
||||||
) |
|
||||||
from slither.detectors.abstract_detector import AbstractDetector, DetectorClassification |
|
||||||
from slither.slithir.operations import ( |
|
||||||
HighLevelCall, |
|
||||||
Index, |
|
||||||
LowLevelCall, |
|
||||||
Send, |
|
||||||
SolidityCall, |
|
||||||
Transfer, |
|
||||||
) |
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=too-many-nested-blocks,too-many-branches |
|
||||||
from slither.utils.output import Output |
|
||||||
|
|
||||||
|
|
||||||
def arbitrary_send(func: Function): |
|
||||||
if func.is_protected(): |
|
||||||
return [] |
|
||||||
|
|
||||||
ret: List[Node] = [] |
|
||||||
for node in func.nodes: |
|
||||||
for ir in node.irs: |
|
||||||
if isinstance(ir, SolidityCall): |
|
||||||
if ir.function == SolidityFunction("ecrecover(bytes32,uint8,bytes32,bytes32)"): |
|
||||||
return False |
|
||||||
if isinstance(ir, Index): |
|
||||||
if ir.variable_right == SolidityVariableComposed("msg.sender"): |
|
||||||
return False |
|
||||||
if is_dependent( |
|
||||||
ir.variable_right, |
|
||||||
SolidityVariableComposed("msg.sender"), |
|
||||||
func.contract, |
|
||||||
): |
|
||||||
return False |
|
||||||
if isinstance(ir, (HighLevelCall, LowLevelCall, Transfer, Send)): |
|
||||||
if isinstance(ir, (HighLevelCall)): |
|
||||||
if isinstance(ir.function, Function): |
|
||||||
if ir.function.full_name == "transferFrom(address,address,uint256)": |
|
||||||
return False |
|
||||||
if ir.call_value is None: |
|
||||||
continue |
|
||||||
if ir.call_value == SolidityVariableComposed("msg.value"): |
|
||||||
continue |
|
||||||
if is_dependent( |
|
||||||
ir.call_value, |
|
||||||
SolidityVariableComposed("msg.value"), |
|
||||||
func.contract, |
|
||||||
): |
|
||||||
continue |
|
||||||
|
|
||||||
if is_tainted(ir.destination, func.contract): |
|
||||||
ret.append(node) |
|
||||||
|
|
||||||
return ret |
|
||||||
|
|
||||||
|
|
||||||
def detect_arbitrary_send(contract: Contract): |
|
||||||
""" |
|
||||||
Detect arbitrary send |
|
||||||
Args: |
|
||||||
contract (Contract) |
|
||||||
Returns: |
|
||||||
list((Function), (list (Node))) |
|
||||||
""" |
|
||||||
ret = [] |
|
||||||
for f in [f for f in contract.functions if f.contract_declarer == contract]: |
|
||||||
nodes = arbitrary_send(f) |
|
||||||
if nodes: |
|
||||||
ret.append((f, nodes)) |
|
||||||
return ret |
|
||||||
|
|
||||||
|
|
||||||
class ArbitrarySend(AbstractDetector): |
|
||||||
ARGUMENT = "arbitrary-send" |
|
||||||
HELP = "Functions that send Ether to arbitrary destinations" |
|
||||||
IMPACT = DetectorClassification.HIGH |
|
||||||
CONFIDENCE = DetectorClassification.MEDIUM |
|
||||||
|
|
||||||
WIKI = "https://github.com/crytic/slither/wiki/Detector-Documentation#functions-that-send-ether-to-arbitrary-destinations" |
|
||||||
|
|
||||||
WIKI_TITLE = "Functions that send Ether to arbitrary destinations" |
|
||||||
WIKI_DESCRIPTION = "Unprotected call to a function sending Ether to an arbitrary address." |
|
||||||
|
|
||||||
# region wiki_exploit_scenario |
|
||||||
WIKI_EXPLOIT_SCENARIO = """ |
|
||||||
```solidity |
|
||||||
contract ArbitrarySend{ |
|
||||||
address destination; |
|
||||||
function setDestination(){ |
|
||||||
destination = msg.sender; |
|
||||||
} |
|
||||||
|
|
||||||
function withdraw() public{ |
|
||||||
destination.transfer(this.balance); |
|
||||||
} |
|
||||||
} |
|
||||||
``` |
|
||||||
Bob calls `setDestination` and `withdraw`. As a result he withdraws the contract's balance.""" |
|
||||||
# endregion wiki_exploit_scenario |
|
||||||
|
|
||||||
WIKI_RECOMMENDATION = "Ensure that an arbitrary user cannot withdraw unauthorized funds." |
|
||||||
|
|
||||||
def _detect(self) -> List[Output]: |
|
||||||
"""""" |
|
||||||
results = [] |
|
||||||
|
|
||||||
for c in self.contracts: |
|
||||||
arbitrary_send_result = detect_arbitrary_send(c) |
|
||||||
for (func, nodes) in arbitrary_send_result: |
|
||||||
|
|
||||||
info = [func, " sends eth to arbitrary user\n"] |
|
||||||
info += ["\tDangerous calls:\n"] |
|
||||||
|
|
||||||
# sort the nodes to get deterministic results |
|
||||||
nodes.sort(key=lambda x: x.node_id) |
|
||||||
|
|
||||||
for node in nodes: |
|
||||||
info += ["\t- ", node, "\n"] |
|
||||||
|
|
||||||
res = self.generate_result(info) |
|
||||||
|
|
||||||
results.append(res) |
|
||||||
|
|
||||||
return results |
|
@ -0,0 +1,144 @@ |
|||||||
|
""" |
||||||
|
Module detecting send to arbitrary address |
||||||
|
|
||||||
|
To avoid FP, it does not report: |
||||||
|
- If msg.sender is used as index (withdraw situation) |
||||||
|
- If the function is protected |
||||||
|
- If the value sent is msg.value (repay situation) |
||||||
|
- If there is a call to transferFrom |
||||||
|
|
||||||
|
TODO: dont report if the value is tainted by msg.value |
||||||
|
""" |
||||||
|
from typing import List |
||||||
|
|
||||||
|
from slither.core.cfg.node import Node |
||||||
|
from slither.core.declarations import Function, Contract |
||||||
|
from slither.analyses.data_dependency.data_dependency import is_tainted, is_dependent |
||||||
|
from slither.core.declarations.solidity_variables import ( |
||||||
|
SolidityFunction, |
||||||
|
SolidityVariableComposed, |
||||||
|
) |
||||||
|
from slither.detectors.abstract_detector import AbstractDetector, DetectorClassification |
||||||
|
from slither.slithir.operations import ( |
||||||
|
HighLevelCall, |
||||||
|
Index, |
||||||
|
LowLevelCall, |
||||||
|
Send, |
||||||
|
SolidityCall, |
||||||
|
Transfer, |
||||||
|
) |
||||||
|
|
||||||
|
|
||||||
|
# pylint: disable=too-many-nested-blocks,too-many-branches |
||||||
|
from slither.utils.output import Output |
||||||
|
|
||||||
|
|
||||||
|
def arbitrary_send(func: Function): |
||||||
|
if func.is_protected(): |
||||||
|
return [] |
||||||
|
|
||||||
|
ret: List[Node] = [] |
||||||
|
for node in func.nodes: |
||||||
|
for ir in node.irs: |
||||||
|
if isinstance(ir, SolidityCall): |
||||||
|
if ir.function == SolidityFunction("ecrecover(bytes32,uint8,bytes32,bytes32)"): |
||||||
|
return False |
||||||
|
if isinstance(ir, Index): |
||||||
|
if ir.variable_right == SolidityVariableComposed("msg.sender"): |
||||||
|
return False |
||||||
|
if is_dependent( |
||||||
|
ir.variable_right, |
||||||
|
SolidityVariableComposed("msg.sender"), |
||||||
|
func.contract, |
||||||
|
): |
||||||
|
return False |
||||||
|
if isinstance(ir, (HighLevelCall, LowLevelCall, Transfer, Send)): |
||||||
|
if isinstance(ir, (HighLevelCall)): |
||||||
|
if isinstance(ir.function, Function): |
||||||
|
if ir.function.full_name == "transferFrom(address,address,uint256)": |
||||||
|
return False |
||||||
|
if ir.call_value is None: |
||||||
|
continue |
||||||
|
if ir.call_value == SolidityVariableComposed("msg.value"): |
||||||
|
continue |
||||||
|
if is_dependent( |
||||||
|
ir.call_value, |
||||||
|
SolidityVariableComposed("msg.value"), |
||||||
|
func.contract, |
||||||
|
): |
||||||
|
continue |
||||||
|
|
||||||
|
if is_tainted(ir.destination, func.contract): |
||||||
|
ret.append(node) |
||||||
|
|
||||||
|
return ret |
||||||
|
|
||||||
|
|
||||||
|
def detect_arbitrary_send(contract: Contract): |
||||||
|
""" |
||||||
|
Detect arbitrary send |
||||||
|
Args: |
||||||
|
contract (Contract) |
||||||
|
Returns: |
||||||
|
list((Function), (list (Node))) |
||||||
|
""" |
||||||
|
ret = [] |
||||||
|
for f in [f for f in contract.functions if f.contract_declarer == contract]: |
||||||
|
nodes = arbitrary_send(f) |
||||||
|
if nodes: |
||||||
|
ret.append((f, nodes)) |
||||||
|
return ret |
||||||
|
|
||||||
|
|
||||||
|
class ArbitrarySendEth(AbstractDetector): |
||||||
|
ARGUMENT = "arbitrary-send-eth" |
||||||
|
HELP = "Functions that send Ether to arbitrary destinations" |
||||||
|
IMPACT = DetectorClassification.HIGH |
||||||
|
CONFIDENCE = DetectorClassification.MEDIUM |
||||||
|
|
||||||
|
WIKI = "https://github.com/crytic/slither/wiki/Detector-Documentation#functions-that-send-ether-to-arbitrary-destinations" |
||||||
|
|
||||||
|
WIKI_TITLE = "Functions that send Ether to arbitrary destinations" |
||||||
|
WIKI_DESCRIPTION = "Unprotected call to a function sending Ether to an arbitrary address." |
||||||
|
|
||||||
|
# region wiki_exploit_scenario |
||||||
|
WIKI_EXPLOIT_SCENARIO = """ |
||||||
|
```solidity |
||||||
|
contract ArbitrarySendEth{ |
||||||
|
address destination; |
||||||
|
function setDestination(){ |
||||||
|
destination = msg.sender; |
||||||
|
} |
||||||
|
|
||||||
|
function withdraw() public{ |
||||||
|
destination.transfer(this.balance); |
||||||
|
} |
||||||
|
} |
||||||
|
``` |
||||||
|
Bob calls `setDestination` and `withdraw`. As a result he withdraws the contract's balance.""" |
||||||
|
# endregion wiki_exploit_scenario |
||||||
|
|
||||||
|
WIKI_RECOMMENDATION = "Ensure that an arbitrary user cannot withdraw unauthorized funds." |
||||||
|
|
||||||
|
def _detect(self) -> List[Output]: |
||||||
|
"""""" |
||||||
|
results = [] |
||||||
|
|
||||||
|
for c in self.contracts: |
||||||
|
arbitrary_send_result = detect_arbitrary_send(c) |
||||||
|
for (func, nodes) in arbitrary_send_result: |
||||||
|
|
||||||
|
info = [func, " sends eth to arbitrary user\n"] |
||||||
|
info += ["\tDangerous calls:\n"] |
||||||
|
|
||||||
|
# sort the nodes to get deterministic results |
||||||
|
nodes.sort(key=lambda x: x.node_id) |
||||||
|
|
||||||
|
for node in nodes: |
||||||
|
info += ["\t- ", node, "\n"] |
||||||
|
|
||||||
|
res = self.generate_result(info) |
||||||
|
|
||||||
|
results.append(res) |
||||||
|
|
||||||
|
return results |
@ -0,0 +1,81 @@ |
|||||||
|
""" |
||||||
|
Module detecting suicidal contract |
||||||
|
|
||||||
|
A suicidal contract is an unprotected function that calls selfdestruct |
||||||
|
""" |
||||||
|
from typing import List |
||||||
|
|
||||||
|
from slither.detectors.abstract_detector import AbstractDetector, DetectorClassification |
||||||
|
from slither.core.declarations import Function, Contract |
||||||
|
from slither.utils.output import Output |
||||||
|
|
||||||
|
|
||||||
|
class ProtectedVariables(AbstractDetector): |
||||||
|
|
||||||
|
ARGUMENT = "protected-vars" |
||||||
|
HELP = "Detected unprotected variables" |
||||||
|
IMPACT = DetectorClassification.HIGH |
||||||
|
CONFIDENCE = DetectorClassification.HIGH |
||||||
|
|
||||||
|
WIKI = "https://github.com/crytic/slither/wiki/Detector-Documentation#protected-variables" |
||||||
|
|
||||||
|
WIKI_TITLE = "Protected Variables" |
||||||
|
WIKI_DESCRIPTION = "Detect unprotected variable that are marked protected" |
||||||
|
|
||||||
|
# region wiki_exploit_scenario |
||||||
|
WIKI_EXPLOIT_SCENARIO = """ |
||||||
|
```solidity |
||||||
|
contract Buggy{ |
||||||
|
|
||||||
|
/// @custom:security write-protection="onlyOwner()" |
||||||
|
address owner; |
||||||
|
|
||||||
|
function set_protected() public onlyOwner(){ |
||||||
|
owner = msg.sender; |
||||||
|
} |
||||||
|
|
||||||
|
function set_not_protected() public{ |
||||||
|
owner = msg.sender; |
||||||
|
} |
||||||
|
} |
||||||
|
``` |
||||||
|
`owner` must be always written by function using `onlyOwner` (`write-protection="onlyOwner()"`), however anyone can call `set_not_protected`. |
||||||
|
""" |
||||||
|
# endregion wiki_exploit_scenario |
||||||
|
|
||||||
|
WIKI_RECOMMENDATION = "Add access controls to the vulnerable function" |
||||||
|
|
||||||
|
def _analyze_function(self, function: Function, contract: Contract) -> List[Output]: |
||||||
|
results = [] |
||||||
|
|
||||||
|
for state_variable_written in function.state_variables_written: |
||||||
|
if state_variable_written.write_protection: |
||||||
|
for function_sig in state_variable_written.write_protection: |
||||||
|
function_protection = contract.get_function_from_signature(function_sig) |
||||||
|
if not function_protection: |
||||||
|
function_protection = contract.get_modifier_from_signature(function_sig) |
||||||
|
if not function_protection: |
||||||
|
self.logger.error(f"{function_sig} not found") |
||||||
|
continue |
||||||
|
if function_protection not in function.all_internal_calls(): |
||||||
|
info = [ |
||||||
|
function, |
||||||
|
" should have ", |
||||||
|
function_protection, |
||||||
|
" to protect ", |
||||||
|
state_variable_written, |
||||||
|
"\n", |
||||||
|
] |
||||||
|
|
||||||
|
res = self.generate_result(info) |
||||||
|
results.append(res) |
||||||
|
return results |
||||||
|
|
||||||
|
def _detect(self): |
||||||
|
"""Detect the suicidal functions""" |
||||||
|
results = [] |
||||||
|
for contract in self.compilation_unit.contracts_derived: |
||||||
|
for function in contract.functions_entry_points: |
||||||
|
results += self._analyze_function(function, contract) |
||||||
|
|
||||||
|
return results |
@ -0,0 +1,30 @@ |
|||||||
|
from slither.core.declarations import Contract |
||||||
|
from slither.core.variables.state_variable import StateVariable |
||||||
|
from slither.core.solidity_types import ArrayType, ElementaryType |
||||||
|
|
||||||
|
|
||||||
|
def is_upgradable_gap_variable(contract: Contract, variable: StateVariable) -> bool: |
||||||
|
"""Helper function that returns true if 'variable' is a gap variable used |
||||||
|
for upgradable contracts. More specifically, the function returns true if: |
||||||
|
- variable is named "__gap" |
||||||
|
- it is a uint256 array declared at the end of the contract |
||||||
|
- it has private visibility""" |
||||||
|
|
||||||
|
# Return early on if the variable name is != gap to avoid iterating over all the state variables |
||||||
|
if variable.name != "__gap": |
||||||
|
return False |
||||||
|
|
||||||
|
declared_variable_ordered = [ |
||||||
|
v for v in contract.state_variables_ordered if v in contract.state_variables_declared |
||||||
|
] |
||||||
|
|
||||||
|
if not declared_variable_ordered: |
||||||
|
return False |
||||||
|
|
||||||
|
variable_type = variable.type |
||||||
|
return ( |
||||||
|
declared_variable_ordered[-1] is variable |
||||||
|
and isinstance(variable_type, ArrayType) |
||||||
|
and variable_type.type == ElementaryType("uint256") |
||||||
|
and variable.visibility == "private" |
||||||
|
) |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue