Skip to content
This repository has been archived by the owner on Jul 11, 2019. It is now read-only.

Warn if contract has a selfdestruct call #347

Merged
merged 9 commits into from
Aug 9, 2018
Merged

Warn if contract has a selfdestruct call #347

merged 9 commits into from
Aug 9, 2018

Conversation

HardlyDifficult
Copy link

Fix for #320

We are scanning the AST as suggested by @nventuro

Fix for #320

We are scanning the AST as suggested by @nventuro
@HardlyDifficult
Copy link
Author

I'm working on the test fails now.

HardlyDifficult added 2 commits August 1, 2018 19:20
I confirmed tests are passing locally this time ;)
for consistency.
Copy link
Contributor

@spalladino spalladino left a comment

Choose a reason for hiding this comment

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

Thanks for the contribution @HardlyDifficult! Could you add some tests to check that the feature works properly?

Also, I don't think the current method for scanning the AST manages to catch all cases, since it only scans the topmost set of statements in a function body. So, if there is a selfdestruct statement within a nested block, it will miss it. For instance:

contract Destructible {
  function hasDestruct() {
    if (true) {
      if (true) {
        if (true) {
          selfdestruct(address(0));
        }
      }
    }
  }
}

I think it'd be more robust to use a visitor pattern to walk the AST, and check for selfdestruct nodes anywhere in the tree.

Also, keep in mind that the selfdestruct call may be present in an ancestor, so you'd need to repeat this process for all contracts in the hierarchy. But I'd suggest to start from just the specific contract, and then work our way up!

…erstood the structure of the AST. I made the changes you suggested and added a Test. Please let me know if this looks okay.

I'm unsure how to address ancestor concern.  I wrote a test for this, and commented it out because it fails ATM.  If you have a suggestion on how to proceed, I'm happy to take a crack at it.
@HardlyDifficult
Copy link
Author

@spalladino Correct, the original approach did not work. I had misunderstood the structure of the AST. I made the changes you suggested and added a Test. Please let me know if this looks okay.

I'm unsure how to address ancestor concern. I wrote a test for this, and commented it out because it fails ATM. If you have a suggestion on how to proceed, I'm happy to take a crack at it.

Copy link
Contributor

@spalladino spalladino left a comment

Choose a reason for hiding this comment

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

Looks much better @HardlyDifficult, thanks!! I think the hasKeyValue approach can be improved using an actual AST visitor, but I couldn't find any existing implementation for the format returned by solc.

As for the parent implementations, note that the AST returns a contractDependencies node, with node IDs that refer to parent contracts you'd need to scan as well.

@@ -92,10 +92,26 @@ contract('add script', function() {
this.logs.errors[0].should.match(/constructor/i);
});

it('should not warn when adding a contract without a constructor', async function() {
it('should not warn when adding a contract without a constructor or a selfdestruct cull', async function() {
Copy link
Contributor

Choose a reason for hiding this comment

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

typo: cull => call


hasKeyValue(data, key, value) {
if (!data) return false
if (data[key] === value) return true

This comment was marked as resolved.

if (typeof(data) === 'object') {
for (const child_key in data) {
const child_value = data[child_key];
if (this.hasKeyValue(child_value, key, value)) {
Copy link
Contributor

Choose a reason for hiding this comment

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

I think the type check should be moved onto here: only call this.hasKeyValue if child_value is an object.

if (!data) return false
if (data[key] === value) return true
if (typeof(data) === 'object') {
for (const child_key in data) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Please make sure to use camelCase for all variable names, and not snake_case, for consistency

});

// TODO: This warning is not yet implemented
// it('should warn when adding a contract who\'s parent has a selfdestruct call', async function() {
Copy link
Contributor

Choose a reason for hiding this comment

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

Use it.skip instead of commenting out the code if it's still unimplemented

I made changes addressing the code review feedback from @spalladino.

For the parent implementations, I'm not sure how to go from node ID to the AST.  I coded an alternative which is similar.  Per the unit test it seems to work.  Thoughts on this?
Copy link
Contributor

@spalladino spalladino left a comment

Choose a reason for hiding this comment

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

@HardlyDifficult the new approach for querying ancestors looks good! I added a minor comment regarding style, to keep consistency with the rest of the codebase.

Could you look into whether it's possible to detect DELEGATECALLs with the same hasKeyValue method, as @nventuro suggested, so we output another warning for the same reason? (ie the contract may delegatecall into an arbitrary one that does selfdestruct)

const ast = fs.parseJson(contractDataPath).ast
if (this.hasKeyValue(ast, "typeIdentifier", "t_function_selfdestruct_nonpayable$_t_address_$returns$__$")) return true
for (let i = 0; i < ast.nodes.length; i++) {
for (let j = 0; j < (ast.nodes.baseContracts || []).length; j++) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Could you change these for-loops to use either the for-of construct for forEach method, for better readability?

@HardlyDifficult
Copy link
Author

RE delegate call: happy too. I looked at this a bit but couldn't figure out how to use delegate call (never tried this in a contract before). Could you provide am example I can test with? I tried the following but it does not work (no error, but it does not actually selfdestruct the contract):

this.delegatecall(bytes4(keccak256("selfdestruct(uint256)")), msg.sender);

thanks,
nick.

@spalladino
Copy link
Contributor

A delegatecall is written as a regular low-level call, the example you provided should work, but you'll probably need to be using a target address different than this (see here for an example). The goal would be to see how a delegatecall looks like in the AST, and capture it, as you did with the selfdestruct. Thanks!!

@HardlyDifficult
Copy link
Author

Delegate call does not seem to create a clear entry in the AST we could scan for. A couple options I can think of:

  • Look for: 'typeIdentifier' == "t_stringliteral_ecd652fcce345f51df38ced4d0016cc21c8a20495863986a365f73a38368985d"
  • Or look for "t_contract$Delegate$42" and provide a more generic warning against making low level calls.

Either of these seem okay?

@spalladino
Copy link
Contributor

spalladino commented Aug 6, 2018

@HardlyDifficult I made a small example, and the AST seems to record the delegate call with the type t_function_baredelegatecall_nonpayable$__$returns$_t_bool_$. If this is reliable, we could use this to check for delegatecalls, as we did with selfdestructs.

@nventuro or @ajsantander, given that you are working with the Solidity compiler internals, could you confirm if this is always the case for a delegatecall (at least when not using assembly)?

I think we are getting real close to wrapping this up!!

HardlyDifficult added 2 commits August 6, 2018 12:16
Including a unit test for that warning.

RE feedback: Changed the for loop style to for-of as suggested.

I considered adding a warning for callcode as well, but there is already a message "Warning: "callcode" has been deprecated in favour of "delegatecall"."
Copy link
Contributor

@spalladino spalladino left a comment

Choose a reason for hiding this comment

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

Looks good @HardlyDifficult, thanks for the update! There is a test missing (the contract was added, but not the corresponding test case), and the CI is reporting a max call stack error (maybe hasTypeIdentifier is looping over the same baseContract or node over and over again?).

this.logs.warns[0].should.match(/selfdestruct/i);
});

it('should warn when adding a contract with a delegatecall call', async function() {
Copy link
Contributor

Choose a reason for hiding this comment

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

Please add a test for WithParentWithDelegateCall

// Log a warning anytime `delegatecall` is found. This is a potential security risk,
// but not an error/throw as it may be a desired feature
if (this.hasDelegateCall(Contracts.getLocalPath(contractName))) {
log.warn(`Contract ${contractName} (or its parent class) has a delegatecall call. Avoid using low-level function 'delegatecall'. This is potentially a security risk. Please review and consider removing this call.`)
Copy link
Contributor

Choose a reason for hiding this comment

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

Let's change:
Avoid using low-level function 'delegatecall'. This is potentially a security risk.
To the following:
This is potentially a security risk, as the logic contract could be destructed by issuing a delegatecall to another contract with a selfdestruct instruction.

WDYT?

this.logs.warns[0].should.match(/selfdestruct/i);
});

it('should warn when adding a contract who\'s parent has a selfdestruct call', async function() {
Copy link
Contributor

Choose a reason for hiding this comment

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

who' s => whose (sorry, grammar nazi here)

Updating per the latest feedback.  Note that I expect the test will still fail.  I'm having trouble locally...

When running the test suite, it simply skips the failing tests without message.

"....
    parseInit
      √ should not init
      √ should init with default when init is set
      √ should init when args is set
      √ should init with specific function
      √ should init with specific function and args

  Contract: add-all script

E:\zos-cli2>"

And when I try to run the test explicitly it also just terminates without messaging why..

"E:\zos-cli2>C:\Users\lopop\AppData\Roaming\npm\truffle test .\test\scripts\add-all.test.js
Using network 'test'.

  Contract: add-all script

E:\zos-cli2>"

I tried one-off local tests (for all 4 test cases) and it seems to work fine.

"E:\zostest>node ..\zos-cli2\lib\bin\zos-cli add SelfDestruct
Compiling contracts
Compiling .\contracts\Child.sol...
Compiling .\contracts\Nested2.sol...
Compiling .\contracts\SelfDestruct.sol...
Writing artifacts to .\build\contracts

Adding SelfDestruct
Contract SelfDestruct (or its parent class) has a selfdestruct call. This is potentially a security risk. Please review and consider removing this call.
Successfully written zos.json"

I can't repro the fail.  I'm unsure how to proceed at the moment.  I'm going to set this down and try again later, let me know if you have any tips.

thanks.
@HardlyDifficult
Copy link
Author

Quick update: I have the stack overflow fixed locally, but currently investigating another issue.

(the AST is referencing every class in the .sol file, vs just ones related to the contract being tested... leading to false positives)

Will continue tomorrow.

And addressed the feedback from @spalladino
@HardlyDifficult
Copy link
Author

I believe all the outstanding feedback has been addressed. Let me know if there's anything I missed.

thanks,
nick.

@spalladino spalladino merged commit cbed3cc into zeppelinos:master Aug 9, 2018
@spalladino
Copy link
Contributor

Awesome @HardlyDifficult, thanks for the hard work!! I'll port the commit to the new monorepo at zeppelinos/zos.

spalladino pushed a commit to OpenZeppelin/openzeppelin-sdk that referenced this pull request Aug 14, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants