Repository: crytic/slither

Issue:
Vulnerability caused by condition of an If Statement having node source mapping the whole If Statement
Slither supports printing the located problematic statement along with its line number. 

Actual Behaviour
---
When the problematic statement is a condition statement for an If Statement, the line number reported by Slither is the line numbers for the whole If statement.

Expected Behaviour
---
Line number is only covering the condition statement.

Thanks
Vulnerability caused by condition of an If Statement having node source mapping the whole If Statement
Slither supports printing the located problematic statement along with its line number. 

Actual Behaviour
---
When the problematic statement is a condition statement for an If Statement, the line number reported by Slither is the line numbers for the whole If statement.

Expected Behaviour
---
Line number is only covering the condition statement.

Thanks


Discussion:
I think this might be because that the condition statement is not an individual object in Slither internal representation, so, when locating the problematic line, the whole If statement was added as the problematic node.
I think this might be because that the condition statement is not an individual object in Slither internal representation, so, when locating the problematic line, the whole If statement was added as the problematic node.

Resolving patch:
diff --git a/slither/core/cfg/node.py b/slither/core/cfg/node.py
--- a/slither/core/cfg/node.py
+++ b/slither/core/cfg/node.py
@@ -44,9 +44,9 @@ class NodeType:
 
     # Merging nodes
     # Can have phi IR operation
-    ENDIF = 0x50
-    STARTLOOP = 0x51
-    ENDLOOP = 0x52
+    ENDIF = 0x50     # ENDIF node source mapping points to the if/else body
+    STARTLOOP = 0x51 # STARTLOOP node source mapping points to the entire loop body
+    ENDLOOP = 0x52   # ENDLOOP node source mapping points to the entire loop body
 
     # Below the nodes have no expression
     # But are used to expression CFG structure
diff --git a/slither/solc_parsing/declarations/function.py b/slither/solc_parsing/declarations/function.py
--- a/slither/solc_parsing/declarations/function.py
+++ b/slither/solc_parsing/declarations/function.py
@@ -256,7 +256,7 @@ def _parse_if(self, ifStatement, node):
             condition = ifStatement['condition']
             # Note: check if the expression could be directly
             # parsed here
-            condition_node = self._new_node(NodeType.IF, ifStatement['src'])
+            condition_node = self._new_node(NodeType.IF, condition['src'])
             condition_node.add_unparsed_expression(condition)
             link_nodes(node, condition_node)
             trueStatement = self._parse_statement(ifStatement['trueBody'], condition_node)
@@ -267,7 +267,7 @@ def _parse_if(self, ifStatement, node):
             condition = children[0]
             # Note: check if the expression could be directly
             # parsed here
-            condition_node = self._new_node(NodeType.IF, ifStatement['src'])
+            condition_node = self._new_node(NodeType.IF, condition['src'])
             condition_node.add_unparsed_expression(condition)
             link_nodes(node, condition_node)
             trueStatement = self._parse_statement(children[1], condition_node)
@@ -287,14 +287,15 @@ def _parse_while(self, whileStatement, node):
         # WhileStatement = 'while' '(' Expression ')' Statement
 
         node_startWhile = self._new_node(NodeType.STARTLOOP, whileStatement['src'])
-        node_condition = self._new_node(NodeType.IFLOOP, whileStatement['src'])
 
         if self.is_compact_ast:
+            node_condition = self._new_node(NodeType.IFLOOP, whileStatement['condition']['src'])
             node_condition.add_unparsed_expression(whileStatement['condition'])
             statement = self._parse_statement(whileStatement['body'], node_condition)
         else:
             children = whileStatement[self.get_children('children')]
             expression = children[0]
+            node_condition = self._new_node(NodeType.IFLOOP, expression['src'])
             node_condition.add_unparsed_expression(expression)
             statement = self._parse_statement(children[1], node_condition)
 
@@ -323,7 +324,7 @@ def _parse_for_compact_ast(self, statement, node):
             link_nodes(node, node_startLoop)
 
         if condition:
-            node_condition = self._new_node(NodeType.IFLOOP, statement['src'])
+            node_condition = self._new_node(NodeType.IFLOOP, condition['src'])
             node_condition.add_unparsed_expression(condition)
             link_nodes(node_startLoop, node_condition)
             link_nodes(node_condition, node_endLoop)
@@ -417,9 +418,9 @@ def _parse_for(self, statement, node):
             if candidate[self.get_key()] not in ['VariableDefinitionStatement',
                                          'VariableDeclarationStatement',
                                          'ExpressionStatement']:
-                node_condition = self._new_node(NodeType.IFLOOP, statement['src'])
-                #expression = parse_expression(candidate, self)
                 expression = candidate
+                node_condition = self._new_node(NodeType.IFLOOP, expression['src'])
+                #expression = parse_expression(candidate, self)
                 node_condition.add_unparsed_expression(expression)
                 link_nodes(node_startLoop, node_condition)
                 link_nodes(node_condition, node_endLoop)
@@ -448,15 +449,16 @@ def _parse_for(self, statement, node):
     def _parse_dowhile(self, doWhilestatement, node):
 
         node_startDoWhile = self._new_node(NodeType.STARTLOOP, doWhilestatement['src'])
-        node_condition = self._new_node(NodeType.IFLOOP, doWhilestatement['src'])
 
         if self.is_compact_ast:
+            node_condition = self._new_node(NodeType.IFLOOP, doWhilestatement['condition']['src'])
             node_condition.add_unparsed_expression(doWhilestatement['condition'])
             statement = self._parse_statement(doWhilestatement['body'], node_condition)
         else:
             children = doWhilestatement[self.get_children('children')]
             # same order in the AST as while
             expression = children[0]
+            node_condition = self._new_node(NodeType.IFLOOP, expression['src'])
             node_condition.add_unparsed_expression(expression)
             statement = self._parse_statement(children[1], node_condition)
 


Test patch:
diff --git a/scripts/tests_generate_expected_json_5.sh b/scripts/tests_generate_expected_json_5.sh
--- a/scripts/tests_generate_expected_json_5.sh
+++ b/scripts/tests_generate_expected_json_5.sh
@@ -20,20 +20,20 @@ generate_expected_json(){
     sed "s|$CURRENT_PATH|$TRAVIS_PATH|g" "$output_filename_txt" -i
 }
 
-generate_expected_json tests/solc_version_incorrect_05.ast.json "solc-version"
-generate_expected_json tests/uninitialized-0.5.1.sol "uninitialized-state"
-generate_expected_json tests/backdoor.sol "backdoor"
-generate_expected_json tests/backdoor.sol "suicidal"
-generate_expected_json tests/old_solc.sol.json "solc-version"
-generate_expected_json tests/reentrancy-0.5.1.sol "reentrancy-eth"
-generate_expected_json tests/tx_origin-0.5.1.sol "tx-origin"
-generate_expected_json tests/locked_ether-0.5.1.sol "locked-ether"
-generate_expected_json tests/arbitrary_send-0.5.1.sol "arbitrary-send"
-generate_expected_json tests/inline_assembly_contract-0.5.1.sol "assembly"
-generate_expected_json tests/inline_assembly_library-0.5.1.sol "assembly"
-generate_expected_json tests/constant-0.5.1.sol "constant-function"
-generate_expected_json tests/incorrect_equality.sol "incorrect-equality"
-generate_expected_json tests/too_many_digits.sol "too-many-digits"
-generate_expected_json tests/unchecked_lowlevel-0.5.1.sol "unchecked-lowlevel"
-generate_expected_json tests/unchecked_send-0.5.1.sol "unchecked-send"
+#generate_expected_json tests/solc_version_incorrect_05.ast.json "solc-version"
+#generate_expected_json tests/uninitialized-0.5.1.sol "uninitialized-state"
+#generate_expected_json tests/backdoor.sol "backdoor"
+#generate_expected_json tests/backdoor.sol "suicidal"
+#generate_expected_json tests/old_solc.sol.json "solc-version"
+#generate_expected_json tests/reentrancy-0.5.1.sol "reentrancy-eth"
+#generate_expected_json tests/tx_origin-0.5.1.sol "tx-origin"
+#generate_expected_json tests/locked_ether-0.5.1.sol "locked-ether"
+#generate_expected_json tests/arbitrary_send-0.5.1.sol "arbitrary-send"
+#generate_expected_json tests/inline_assembly_contract-0.5.1.sol "assembly"
+#generate_expected_json tests/inline_assembly_library-0.5.1.sol "assembly"
+#generate_expected_json tests/constant-0.5.1.sol "constant-function"
+#generate_expected_json tests/incorrect_equality.sol "incorrect-equality"
+#generate_expected_json tests/too_many_digits.sol "too-many-digits"
+#generate_expected_json tests/unchecked_lowlevel-0.5.1.sol "unchecked-lowlevel"
+#generate_expected_json tests/unchecked_send-0.5.1.sol "unchecked-send"
 
diff --git a/tests/expected_json/deprecated_calls.deprecated-standards.json b/tests/expected_json/deprecated_calls.deprecated-standards.json
--- a/tests/expected_json/deprecated_calls.deprecated-standards.json
+++ b/tests/expected_json/deprecated_calls.deprecated-standards.json
@@ -77,26 +77,23 @@
         "check": "deprecated-standards",
         "impact": "Informational",
         "confidence": "High",
-        "description": "Deprecated standard detected @ tests/deprecated_calls.sol#7-10:\n\t- Usage of \"msg.gas\" should be replaced with \"gasleft()\"\n",
+        "description": "Deprecated standard detected @ tests/deprecated_calls.sol#7:\n\t- Usage of \"msg.gas\" should be replaced with \"gasleft()\"\n",
         "elements": [
           {
             "type": "node",
             "name": "msg.gas == msg.value",
             "source_mapping": {
-              "start": 258,
-              "length": 107,
+              "start": 261,
+              "length": 20,
               "filename_used": "/home/travis/build/crytic/slither/tests/deprecated_calls.sol",
               "filename_relative": "tests/deprecated_calls.sol",
               "filename_absolute": "/home/travis/build/crytic/slither/tests/deprecated_calls.sol",
               "filename_short": "tests/deprecated_calls.sol",
               "lines": [
-                7,
-                8,
-                9,
-                10
+                7
               ],
-              "starting_column": 9,
-              "ending_column": 10
+              "starting_column": 12,
+              "ending_column": 32
             },
             "type_specific_fields": {
               "parent": {
diff --git a/tests/expected_json/deprecated_calls.deprecated-standards.txt b/tests/expected_json/deprecated_calls.deprecated-standards.txt
--- a/tests/expected_json/deprecated_calls.deprecated-standards.txt
+++ b/tests/expected_json/deprecated_calls.deprecated-standards.txt
@@ -1,7 +1,7 @@
 INFO:Detectors:[92m
 Deprecated standard detected @ tests/deprecated_calls.sol#2:
 	- Usage of "block.blockhash()" should be replaced with "blockhash()"
-Deprecated standard detected @ tests/deprecated_calls.sol#7-10:
+Deprecated standard detected @ tests/deprecated_calls.sol#7:
 	- Usage of "msg.gas" should be replaced with "gasleft()"
 Deprecated standard detected @ tests/deprecated_calls.sol#9:
 	- Usage of "throw" should be replaced with "revert()"
diff --git a/tests/expected_json/incorrect_equality.incorrect-equality.json b/tests/expected_json/incorrect_equality.incorrect-equality.json
--- a/tests/expected_json/incorrect_equality.incorrect-equality.json
+++ b/tests/expected_json/incorrect_equality.incorrect-equality.json
@@ -1259,19 +1259,17 @@
             "type": "node",
             "name": "balance == 10000000000000000000",
             "source_mapping": {
-              "start": 1270,
-              "length": 80,
+              "start": 1274,
+              "length": 19,
               "filename_used": "/home/travis/build/crytic/slither/tests/incorrect_equality.sol",
               "filename_relative": "tests/incorrect_equality.sol",
               "filename_absolute": "/home/travis/build/crytic/slither/tests/incorrect_equality.sol",
               "filename_short": "tests/incorrect_equality.sol",
               "lines": [
-                54,
-                55,
-                56
+                54
               ],
-              "starting_column": 9,
-              "ending_column": 10
+              "starting_column": 13,
+              "ending_column": 32
             },
             "type_specific_fields": {
               "parent": {
@@ -1506,19 +1504,17 @@
             "type": "node",
             "name": "10000000000000000000 == balance",
             "source_mapping": {
-              "start": 1446,
-              "length": 80,
+              "start": 1450,
+              "length": 19,
               "filename_used": "/home/travis/build/crytic/slither/tests/incorrect_equality.sol",
               "filename_relative": "tests/incorrect_equality.sol",
               "filename_absolute": "/home/travis/build/crytic/slither/tests/incorrect_equality.sol",
               "filename_short": "tests/incorrect_equality.sol",
               "lines": [
-                61,
-                62,
-                63
+                61
               ],
-              "starting_column": 9,
-              "ending_column": 10
+              "starting_column": 13,
+              "ending_column": 32
             },
             "type_specific_fields": {
               "parent": {
@@ -1753,19 +1749,17 @@
             "type": "node",
             "name": "balance == 10000000000000000000",
             "source_mapping": {
-              "start": 1631,
-              "length": 80,
+              "start": 1635,
+              "length": 19,
               "filename_used": "/home/travis/build/crytic/slither/tests/incorrect_equality.sol",
               "filename_relative": "tests/incorrect_equality.sol",
               "filename_absolute": "/home/travis/build/crytic/slither/tests/incorrect_equality.sol",
               "filename_short": "tests/incorrect_equality.sol",
               "lines": [
-                68,
-                69,
-                70
+                68
               ],
-              "starting_column": 9,
-              "ending_column": 10
+              "starting_column": 13,
+              "ending_column": 32
             },
             "type_specific_fields": {
               "parent": {
diff --git a/tests/expected_json/reentrancy.reentrancy-eth.json b/tests/expected_json/reentrancy.reentrancy-eth.json
--- a/tests/expected_json/reentrancy.reentrancy-eth.json
+++ b/tests/expected_json/reentrancy.reentrancy-eth.json
@@ -7,7 +7,7 @@
         "check": "reentrancy-eth",
         "impact": "High",
         "confidence": "Medium",
-        "description": "Reentrancy in Reentrancy.withdrawBalance() (tests/reentrancy.sol#14-21):\n\tExternal calls:\n\t- ! (msg.sender.call.value(userBalance[msg.sender])()) (tests/reentrancy.sol#17-19)\n\tState variables written after the call(s):\n\t- userBalance (tests/reentrancy.sol#20)\n",
+        "description": "Reentrancy in Reentrancy.withdrawBalance() (tests/reentrancy.sol#14-21):\n\tExternal calls:\n\t- ! (msg.sender.call.value(userBalance[msg.sender])()) (tests/reentrancy.sol#17)\n\tState variables written after the call(s):\n\t- userBalance (tests/reentrancy.sol#20)\n",
         "elements": [
           {
             "type": "function",
@@ -126,19 +126,17 @@
             "type": "node",
             "name": "! (msg.sender.call.value(userBalance[msg.sender])())",
             "source_mapping": {
-              "start": 478,
-              "length": 92,
+              "start": 482,
+              "length": 53,
               "filename_used": "/home/travis/build/crytic/slither/tests/reentrancy.sol",
               "filename_relative": "tests/reentrancy.sol",
               "filename_absolute": "/home/travis/build/crytic/slither/tests/reentrancy.sol",
               "filename_short": "tests/reentrancy.sol",
               "lines": [
-                17,
-                18,
-                19
+                17
               ],
-              "starting_column": 9,
-              "ending_column": 10
+              "starting_column": 13,
+              "ending_column": 66
             },
             "type_specific_fields": {
               "parent": {
diff --git a/tests/expected_json/reentrancy.reentrancy-eth.txt b/tests/expected_json/reentrancy.reentrancy-eth.txt
--- a/tests/expected_json/reentrancy.reentrancy-eth.txt
+++ b/tests/expected_json/reentrancy.reentrancy-eth.txt
@@ -1,7 +1,7 @@
 INFO:Detectors:[91m
 Reentrancy in Reentrancy.withdrawBalance() (tests/reentrancy.sol#14-21):
 	External calls:
-	- ! (msg.sender.call.value(userBalance[msg.sender])()) (tests/reentrancy.sol#17-19)
+	- ! (msg.sender.call.value(userBalance[msg.sender])()) (tests/reentrancy.sol#17)
 	State variables written after the call(s):
 	- userBalance (tests/reentrancy.sol#20)
 Reentrancy in Reentrancy.withdrawBalance_nested() (tests/reentrancy.sol#64-70):
diff --git a/tests/expected_json/tx_origin-0.5.1.tx-origin.json b/tests/expected_json/tx_origin-0.5.1.tx-origin.json
--- a/tests/expected_json/tx_origin-0.5.1.tx-origin.json
+++ b/tests/expected_json/tx_origin-0.5.1.tx-origin.json
@@ -96,25 +96,23 @@
         "check": "tx-origin",
         "impact": "Medium",
         "confidence": "Medium",
-        "description": "TxOrigin.bug2() uses tx.origin for authorization: \"tx.origin != owner\" (tests/tx_origin-0.5.1.sol#14-16)\n",
+        "description": "TxOrigin.bug2() uses tx.origin for authorization: \"tx.origin != owner\" (tests/tx_origin-0.5.1.sol#14)\n",
         "elements": [
           {
             "type": "node",
             "name": "tx.origin != owner",
             "source_mapping": {
-              "start": 231,
-              "length": 57,
+              "start": 235,
+              "length": 18,
               "filename_used": "/home/travis/build/crytic/slither/tests/tx_origin-0.5.1.sol",
               "filename_relative": "tests/tx_origin-0.5.1.sol",
               "filename_absolute": "/home/travis/build/crytic/slither/tests/tx_origin-0.5.1.sol",
               "filename_short": "tests/tx_origin-0.5.1.sol",
               "lines": [
-                14,
-                15,
-                16
+                14
               ],
-              "starting_column": 9,
-              "ending_column": 10
+              "starting_column": 13,
+              "ending_column": 31
             },
             "type_specific_fields": {
               "parent": {
diff --git a/tests/expected_json/tx_origin-0.5.1.tx-origin.txt b/tests/expected_json/tx_origin-0.5.1.tx-origin.txt
--- a/tests/expected_json/tx_origin-0.5.1.tx-origin.txt
+++ b/tests/expected_json/tx_origin-0.5.1.tx-origin.txt
@@ -1,5 +1,5 @@
 INFO:Detectors:[93m
 TxOrigin.bug0() uses tx.origin for authorization: "require(bool)(tx.origin == owner)" (tests/tx_origin-0.5.1.sol#10)
-TxOrigin.bug2() uses tx.origin for authorization: "tx.origin != owner" (tests/tx_origin-0.5.1.sol#14-16)
+TxOrigin.bug2() uses tx.origin for authorization: "tx.origin != owner" (tests/tx_origin-0.5.1.sol#14)
 Reference: https://github.com/crytic/slither/wiki/Detector-Documentation#dangerous-usage-of-txorigin[0m
 INFO:Slither:tests/tx_origin-0.5.1.sol analyzed (1 contracts), 2 result(s) found
diff --git a/tests/expected_json/tx_origin.tx-origin.json b/tests/expected_json/tx_origin.tx-origin.json
--- a/tests/expected_json/tx_origin.tx-origin.json
+++ b/tests/expected_json/tx_origin.tx-origin.json
@@ -96,25 +96,23 @@
         "check": "tx-origin",
         "impact": "Medium",
         "confidence": "Medium",
-        "description": "TxOrigin.bug2() uses tx.origin for authorization: \"tx.origin != owner\" (tests/tx_origin.sol#14-16)\n",
+        "description": "TxOrigin.bug2() uses tx.origin for authorization: \"tx.origin != owner\" (tests/tx_origin.sol#14)\n",
         "elements": [
           {
             "type": "node",
             "name": "tx.origin != owner",
             "source_mapping": {
-              "start": 208,
-              "length": 57,
+              "start": 212,
+              "length": 18,
               "filename_used": "/home/travis/build/crytic/slither/tests/tx_origin.sol",
               "filename_relative": "tests/tx_origin.sol",
               "filename_absolute": "/home/travis/build/crytic/slither/tests/tx_origin.sol",
               "filename_short": "tests/tx_origin.sol",
               "lines": [
-                14,
-                15,
-                16
+                14
               ],
-              "starting_column": 9,
-              "ending_column": 10
+              "starting_column": 13,
+              "ending_column": 31
             },
             "type_specific_fields": {
               "parent": {
diff --git a/tests/expected_json/tx_origin.tx-origin.txt b/tests/expected_json/tx_origin.tx-origin.txt
--- a/tests/expected_json/tx_origin.tx-origin.txt
+++ b/tests/expected_json/tx_origin.tx-origin.txt
@@ -1,5 +1,5 @@
 INFO:Detectors:[93m
 TxOrigin.bug0() uses tx.origin for authorization: "require(bool)(tx.origin == owner)" (tests/tx_origin.sol#10)
-TxOrigin.bug2() uses tx.origin for authorization: "tx.origin != owner" (tests/tx_origin.sol#14-16)
+TxOrigin.bug2() uses tx.origin for authorization: "tx.origin != owner" (tests/tx_origin.sol#14)
 Reference: https://github.com/crytic/slither/wiki/Detector-Documentation#dangerous-usage-of-txorigin[0m
 INFO:Slither:tests/tx_origin.sol analyzed (1 contracts), 2 result(s) found