Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Perform cross-contract taint analysis from diff of two upgrade versions #1816

Merged
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
072cb0b
Find tainted functions/variables from external calls
webthethird Mar 29, 2023
4b6dd02
Find contracts tainted by inheriting a tainted contract
webthethird Mar 30, 2023
0deff18
Add docstring to `tainted_inheriting_contracts`
webthethird Mar 30, 2023
5967958
Black
webthethird Mar 30, 2023
675dbea
Get tainted variables in `tainted_inheriting_contracts`
webthethird Mar 31, 2023
d88bba4
Fix too many values to unpack in test_upgradeability_util.py
webthethird Mar 31, 2023
cba0dc9
Add python types
webthethird Mar 31, 2023
386c3e1
Pylint and black
webthethird Mar 31, 2023
20f5825
Pylint and black
webthethird Mar 31, 2023
c786658
Merge remote-tracking branch 'crytic/slither/dev' into dev-upgradeabi…
webthethird Mar 31, 2023
f585d2b
Add TODO
webthethird Mar 31, 2023
2b330a1
Tweak how tainted variables are handled
webthethird Apr 3, 2023
da045d6
Make TaintedExternalContract a regular class
webthethird Apr 4, 2023
d1b34b6
Avoid string comparison
webthethird Apr 4, 2023
85c22f4
Avoid re-defining `contracts` in `tainted_inheriting_contracts`
webthethird Apr 4, 2023
38acd93
Add inline comments
webthethird Apr 4, 2023
98a5cf0
Use canonical_name in comparisons
webthethird Apr 4, 2023
1e73979
Fix expected tainted funcs, since we changed what's considered tainted
webthethird Apr 4, 2023
178960f
Simplify by removing `TaintedFunction` and `TaintedVariable` classes
webthethird Apr 4, 2023
3bcefac
Black
webthethird Apr 4, 2023
ae7f0b2
Only check internal calls to Functions
webthethird Apr 10, 2023
ca82da0
Update compare docstring
webthethird Apr 17, 2023
853051e
Reduce tainted variables to only written
webthethird Apr 17, 2023
823337e
Update test
webthethird Apr 17, 2023
6ccc8cf
Black
webthethird Apr 17, 2023
ef2eadc
Update test_upgrades_compare to test cross-contract taint
webthethird Apr 19, 2023
2cdc544
Make cross-contract taint optional in `compare`
webthethird May 10, 2023
1064185
Update test
webthethird May 10, 2023
af7279a
Update docstring
webthethird May 10, 2023
6a10e0a
Merge branch 'dev' into dev-upgradeability-util-cross-contract-taint
webthethird May 19, 2023
4ff33c0
Merge branch 'dev' into dev-upgradeability-util-cross-contract-taint
webthethird May 25, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
151 changes: 144 additions & 7 deletions slither/utils/upgradeability.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Optional, Tuple, List, Union
from typing import Optional, Tuple, List, Union, TypedDict
from slither.core.declarations import (
Contract,
Structure,
Expand All @@ -17,12 +17,15 @@
from slither.core.variables.local_variable import LocalVariable
from slither.core.variables.local_variable_init_from_tuple import LocalVariableInitFromTuple
from slither.core.variables.state_variable import StateVariable
from slither.slithir.variables import TemporaryVariable
from slither.analyses.data_dependency.data_dependency import get_dependencies
from slither.core.variables.variable import Variable
from slither.core.expressions.literal import Literal
from slither.core.expressions.identifier import Identifier
from slither.core.expressions.call_expression import CallExpression
from slither.core.expressions.assignment_operation import AssignmentOperation
from slither.core.expressions import (
Literal,
Identifier,
CallExpression,
AssignmentOperation,
)
from slither.core.cfg.node import Node, NodeType
from slither.slithir.operations import (
Operation,
Expand Down Expand Up @@ -61,11 +64,23 @@
from slither.tools.read_storage.read_storage import SlotInfo, SlitherReadStorage


class TaintedExternalContract(TypedDict):
webthethird marked this conversation as resolved.
Show resolved Hide resolved
contract: Contract
functions: List[Function]
variables: List[Variable]


# pylint: disable=too-many-locals
def compare(
v1: Contract, v2: Contract
) -> Tuple[
List[Variable], List[Variable], List[Variable], List[Function], List[Function], List[Function]
List[Variable],
List[Variable],
List[Variable],
List[Function],
List[Function],
List[Function],
List[TaintedExternalContract],
]:
"""
Compares two versions of a contract. Most useful for upgradeable (logic) contracts,
Expand Down Expand Up @@ -159,16 +174,137 @@ def compare(
):
tainted_variables.append(var)

# Find all external contracts and functions called by new/modified/tainted functions
tainted_contracts = tainted_external_contracts(
new_functions + modified_functions + tainted_functions
)

return (
missing_vars_in_v2,
new_variables,
tainted_variables,
new_functions,
modified_functions,
tainted_functions,
tainted_contracts,
)


def tainted_external_contracts(funcs: List[Function]) -> List[TaintedExternalContract]:
"""
Takes a list of functions from one contract, finds any calls in these to functions in external contracts,
and determines which variables and functions in the external contracts are tainted by these external calls.
Args:
funcs: a list of Function objects to search for external calls.

Returns:
TaintedExternalContract(TypedDict) (
contract: Contract,
functions: List[Function],
variables: List[Variable]
)
"""
tainted_contracts: dict[str, TaintedExternalContract] = {}
tainted_list: list[TaintedExternalContract] = []

for func in funcs:
for contract, target in func.all_high_level_calls():
if contract.is_library:
continue
if contract.name not in tainted_contracts:
tainted_contracts[contract.name] = TaintedExternalContract(
contract=contract, functions=[], variables=[]
)
if (
isinstance(target, Function)
and target not in funcs
and target not in tainted_contracts[contract.name]["functions"]
and not (target.is_constructor or target.is_fallback or target.is_receive)
):
tainted_contracts[contract.name]["functions"].append(target)
for var in target.all_state_variables_written():
if var not in tainted_contracts[contract.name]["variables"]:
tainted_contracts[contract.name]["variables"].append(var)
elif (
isinstance(target, Variable)
and target not in tainted_contracts[contract.name]["variables"]
and not (target.is_constant or target.is_immutable)
):
tainted_contracts[contract.name]["variables"].append(target)
for c in tainted_contracts.items():
webthethird marked this conversation as resolved.
Show resolved Hide resolved
if len(c[1]["functions"]) == 0 and len(c[1]["variables"]) == 0:
continue
tainted_list.append(c[1])
contract = c[1]["contract"]
variables = c[1]["variables"]
for var in variables:
read_write = set(
contract.get_functions_reading_from_variable(var)
+ contract.get_functions_writing_to_variable(var)
)
for f in read_write:
if f not in tainted_contracts[contract.name]["functions"] and not (
f.is_constructor or f.is_fallback or f.is_receive
):
tainted_contracts[contract.name]["functions"].append(f)
return tainted_list


def tainted_inheriting_contracts(
tainted_contracts: List[TaintedExternalContract], contracts: List[Contract] = None
) -> List[TaintedExternalContract]:
"""
Takes a list of TaintedExternalContract obtained from tainted_external_contracts, and finds any contracts which
inherit a tainted contract, as well as any functions that call tainted functions or read tainted variables in
the inherited contract.
Args:
tainted_contracts: the list obtained from `tainted_external_contracts` or `compare`.
contracts: (optional) the list of contracts to check for inheritance. If not provided, defaults to
`contract.compilation_unit.contracts` for each contract in tainted_contracts.

Returns:
An updated list of TaintedExternalContract, including all from the input list.
"""
for tainted in tainted_contracts:
contract = tainted["contract"]
if contracts is None:
contracts = contract.compilation_unit.contracts
contracts = [
c
for c in contracts
if c.name not in tainted_contracts and c.name in [i.name for i in c.inheritance]
]
for c in contracts:
new_taint = TaintedExternalContract(contract=c, functions=[], variables=[])
for f in c.functions_declared:
internal_calls = f.all_internal_calls()
if any(
str(call) == str(t) for t in tainted["functions"] for call in internal_calls
webthethird marked this conversation as resolved.
Show resolved Hide resolved
) or any(
str(var) == str(t)
for t in tainted["variables"]
for var in f.all_state_variables_read() + f.all_state_variables_written()
):
new_taint["functions"].append(f)
for f in new_taint["functions"]:
for var in f.all_state_variables_read() + f.all_state_variables_written():
if not (var in tainted["variables"] or var in new_taint["variables"]):
new_taint["variables"].append(var)
for var in new_taint["variables"]:
read_write = set(
contract.get_functions_reading_from_variable(var)
+ contract.get_functions_writing_to_variable(var)
)
for f in read_write:
if f not in tainted["functions"] + new_taint["functions"] and not (
f.is_constructor or f.is_fallback or f.is_receive
):
new_taint["functions"].append(f)
if len(new_taint["functions"]) > 0:
tainted_contracts.append(new_taint)
return tainted_contracts


def get_missing_vars(v1: Contract, v2: Contract) -> List[StateVariable]:
"""
Gets all non-constant/immutable StateVariables that appear in v1 but not v2
Expand Down Expand Up @@ -273,7 +409,7 @@ def encode_ir_for_compare(ir: Operation) -> str:
if isinstance(ir, Assignment):
return f"({encode_var_for_compare(ir.lvalue)}):=({encode_var_for_compare(ir.rvalue)})"
if isinstance(ir, Index):
return f"index({ntype(ir.index_type)})"
return f"index({ntype(ir.variable_right.type)})"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add a test that highlight why this was wrong? It will help in the long term

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It only became wrong when the index_type property was removed from the Index class. Slither was just crashing when I tried to access the property, since it didn't exist anymore. Is that still something we can test?

if isinstance(ir, Member):
return "member" # .format(ntype(ir._type))
if isinstance(ir, Length):
Expand Down Expand Up @@ -398,6 +534,7 @@ def get_proxy_implementation_var(proxy: Contract) -> Optional[Variable]:
try:
delegate = next(var for var in dependencies if isinstance(var, StateVariable))
except StopIteration:
# TODO: Handle cases where get_dependencies doesn't return any state variables.
return delegate
return delegate

Expand Down
12 changes: 10 additions & 2 deletions tests/unit/utils/test_upgradeability_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,16 @@ def test_upgrades_compare() -> None:
sl = Slither(os.path.join(TEST_DATA_DIR, "TestUpgrades-0.8.2.sol"))
v1 = sl.get_contract_from_name("ContractV1")[0]
v2 = sl.get_contract_from_name("ContractV2")[0]
missing_vars, new_vars, tainted_vars, new_funcs, modified_funcs, tainted_funcs = compare(v1, v2)
assert len(missing_vars) == 0
(
missing_vars,
new_vars,
tainted_vars,
new_funcs,
modified_funcs,
tainted_funcs,
tainted_contracts,
) = compare(v1, v2)
assert len(missing_vars) == len(tainted_contracts) == 0
assert new_vars == [v2.get_state_variable_from_name("stateC")]
assert tainted_vars == [
v2.get_state_variable_from_name("stateB"),
Expand Down