Skip to content

Commit e9adc63

Browse files
authored
Merge pull request #260 from xiemaisi/js/confusing-precedence
Approved by esben-semmle, mc-semmle
2 parents 4ad4b19 + 7683684 commit e9adc63

File tree

13 files changed

+126
-29
lines changed

13 files changed

+126
-29
lines changed

change-notes/1.19/analysis-javascript.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,10 @@
1515
| **Query** | **Tags** | **Purpose** |
1616
|-----------------------------------------------|------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
1717
| Enabling Node.js integration for Electron web content renderers (`js/enabling-electron-renderer-node-integration`) | security, frameworks/electron, external/cwe/cwe-094 | Highlights Electron web content renderer preferences with Node.js integration enabled, indicating a violation of [CWE-94](https://cwe.mitre.org/data/definitions/94.html). Results are not shown on LGTM by default. |
18-
| Stored cross-site scripting (`js/stored-xss`) | security, external/cwe/cwe-079, external/cwe/cwe-116 | Highlights uncontrolled stored values flowing into HTML content, indicating a violation of [CWE-079](https://cwe.mitre.org/data/definitions/79.html). Results shown on LGTM by default. |
19-
| Replacement of a substring with itself (`js/identity-replacement`) | correctness, security, external/cwe/cwe-116 | Highlights string replacements that replace a string with itself, which usually indicates a mistake. Results shown on LGTM by default. |
2018
| Host header poisoning in email generation | security, external/cwe/cwe-640 | Highlights code that generates emails with links that can be hijacked by HTTP host header poisoning, indicating a violation of [CWE-640](https://cwe.mitre.org/data/definitions/640.html). Results shown on LGTM by default. |
19+
| Replacement of a substring with itself (`js/identity-replacement`) | correctness, security, external/cwe/cwe-116 | Highlights string replacements that replace a string with itself, which usually indicates a mistake. Results shown on LGTM by default. |
20+
| Stored cross-site scripting (`js/stored-xss`) | security, external/cwe/cwe-079, external/cwe/cwe-116 | Highlights uncontrolled stored values flowing into HTML content, indicating a violation of [CWE-079](https://cwe.mitre.org/data/definitions/79.html). Results shown on LGTM by default. |
21+
| Unclear precedence of nested operators (`js/unclear-operator-precedence`) | maintainability, correctness, external/cwe/cwe-783 | Highlights nested binary operators whose relative precedence is easy to misunderstand. Results shown on LGTM by default. |
2122

2223
## Changes to existing queries
2324

@@ -29,5 +30,6 @@
2930
| Remote property injection | Fewer results | The precision of this rule has been revised to "medium". Results are no longer shown on LGTM by default. |
3031
| Missing CSRF middleware | Fewer false-positive results | This rule now recognizes additional CSRF protection middlewares. |
3132
| Server-side URL redirect | More results | This rule now recognizes redirection calls in more cases. |
33+
| Whitespace contradicts operator precedence | Fewer false-positive results | This rule no longer flags operators with asymmetric whitespace. |
3234

3335
## Changes to QL libraries

javascript/config/suites/javascript/correctness-more

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
+ semmlecode-javascript-queries/Expressions/SuspiciousInvocation.ql: /Correctness/Expressions
66
+ semmlecode-javascript-queries/Expressions/SuspiciousPropAccess.ql: /Correctness/Expressions
77
+ semmlecode-javascript-queries/Expressions/UnboundEventHandlerReceiver.ql: /Correctness/Expressions
8+
+ semmlecode-javascript-queries/Expressions/UnclearOperatorPrecedence.ql: /Correctness/Expressions
89
+ semmlecode-javascript-queries/Expressions/UnknownDirective.ql: /Correctness/Expressions
910
+ semmlecode-javascript-queries/Expressions/WhitespaceContradictsPrecedence.ql: /Correctness/Expressions
1011
+ semmlecode-javascript-queries/LanguageFeatures/IllegalInvocation.ql: /Correctness/Language Features
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
<!DOCTYPE qhelp PUBLIC "-//Semmle//qhelp//EN" "qhelp.dtd">
2+
<qhelp>
3+
4+
<overview>
5+
<p>
6+
Nested expressions that rely on less well-known operator precedence rules can be
7+
hard to read and understand. They could even indicate a bug where the author of the
8+
code misunderstood the precedence rules.
9+
</p>
10+
</overview>
11+
12+
<recommendation>
13+
<p>
14+
Use parentheses or additional whitespace to clarify grouping.
15+
</p>
16+
</recommendation>
17+
18+
<example>
19+
<p>
20+
Consider the following snippet of code:
21+
</p>
22+
23+
<sample src="examples/UnclearOperatorPrecedence.js" />
24+
25+
<p>
26+
It might look like this tests whether <code>x</code> and <code>y</code> have any bits in
27+
common, but in fact <code>==</code> binds more tightly than <code>&amp;</code>, so the test
28+
is equivalent to <code>x &amp; (y == 0)</code>.
29+
</p>
30+
31+
<p>
32+
If this is the intended interpretation, parentheses should be used to clarify this. You could
33+
also consider adding extra whitespace around <code>&amp;</code> or removing whitespace
34+
around <code>==</code> to make it visually apparent that it binds less tightly:
35+
<code>x &amp; y==0</code>.
36+
</p>
37+
<p>
38+
Probably the best approach in this case, though, would be to use the <code>&amp;&amp;</code>
39+
operator instead to clarify the intended interpretation: <code>x &amp;&amp; y == 0</code>.
40+
</p>
41+
</example>
42+
43+
<references>
44+
<li>Mozilla Developer Network, <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Operator_Precedence">Operator precedence</a>.</li>
45+
</references>
46+
</qhelp>
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/**
2+
* @name Unclear precedence of nested operators
3+
* @description Nested expressions involving binary bitwise operators and comparisons are easy
4+
* to misunderstand without additional disambiguating parentheses or whitespace.
5+
* @kind problem
6+
* @problem.severity recommendation
7+
* @id js/unclear-operator-precedence
8+
* @tags maintainability
9+
* correctness
10+
* statistical
11+
* non-attributable
12+
* external/cwe/cwe-783
13+
* @precision very-high
14+
*/
15+
16+
import javascript
17+
18+
from BitwiseBinaryExpr bit, Comparison rel, Expr other
19+
where bit.hasOperands(rel, other) and
20+
// only flag if whitespace doesn't clarify the nesting (note that if `bit` has less
21+
// whitespace than `rel`, it will be reported by `js/whitespace-contradicts-precedence`)
22+
bit.getWhitespaceAroundOperator() = rel.getWhitespaceAroundOperator() and
23+
// don't flag if the other operand is itself a comparison,
24+
// since the nesting tends to be visually more obvious in such cases
25+
not other instanceof Comparison and
26+
// don't flag occurrences in minified code
27+
not rel.getTopLevel().isMinified()
28+
select rel, "The '" + rel.getOperator() + "' operator binds more tightly than " +
29+
"'" + bit.getOperator() + "', which may not be obvious in this case."

javascript/ql/src/Expressions/WhitespaceContradictsPrecedence.ql

Lines changed: 2 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -54,29 +54,6 @@ class HarmlessNestedExpr extends BinaryExpr {
5454
}
5555
}
5656

57-
/** Holds if the right operand of `expr` starts on line `line`, at column `col`. */
58-
predicate startOfBinaryRhs(BinaryExpr expr, int line, int col) {
59-
exists(Location rloc | rloc = expr.getRightOperand().getLocation() |
60-
rloc.getStartLine() = line and rloc.getStartColumn() = col
61-
)
62-
}
63-
64-
/** Holds if the left operand of `expr` ends on line `line`, at column `col`. */
65-
predicate endOfBinaryLhs(BinaryExpr expr, int line, int col) {
66-
exists(Location lloc | lloc = expr.getLeftOperand().getLocation() |
67-
lloc.getEndLine() = line and lloc.getEndColumn() = col
68-
)
69-
}
70-
71-
/** Gets the number of whitespace characters around the operator of `expr`. */
72-
int operatorWS(BinaryExpr expr) {
73-
exists(int line, int lcol, int rcol |
74-
endOfBinaryLhs(expr, line, lcol) and
75-
startOfBinaryRhs(expr, line, rcol) and
76-
result = rcol - lcol + 1 - expr.getOperator().length()
77-
)
78-
}
79-
8057
/**
8158
* Holds if `inner` is an operand of `outer`, and the relative precedence
8259
* may not be immediately clear, but is important for the semantics of
@@ -88,10 +65,8 @@ predicate interestingNesting(BinaryExpr inner, BinaryExpr outer) {
8865
not inner instanceof HarmlessNestedExpr
8966
}
9067

91-
from BinaryExpr inner, BinaryExpr outer, int wsouter, int wsinner
68+
from BinaryExpr inner, BinaryExpr outer
9269
where interestingNesting(inner, outer) and
93-
wsinner = operatorWS(inner) and wsouter = operatorWS(outer) and
94-
wsinner % 2 = 0 and wsouter % 2 = 0 and
95-
wsinner > wsouter and
70+
inner.getWhitespaceAroundOperator() > outer.getWhitespaceAroundOperator() and
9671
not outer.getTopLevel().isMinified()
9772
select outer, "Whitespace around nested operators contradicts precedence."
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
if (x & y == 0) {
2+
// ...
3+
}

javascript/ql/src/semmle/javascript/Expr.qll

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1008,6 +1008,25 @@ class BinaryExpr extends @binaryexpr, Expr {
10081008
override ControlFlowNode getFirstControlFlowNode() {
10091009
result = getLeftOperand().getFirstControlFlowNode()
10101010
}
1011+
1012+
/**
1013+
* Gets the number of whitespace characters around the operator of this expression.
1014+
*
1015+
* This predicate is only defined if both operands are on the same line, and if the
1016+
* amount of whitespace before and after the operator are the same.
1017+
*/
1018+
int getWhitespaceAroundOperator() {
1019+
exists (Token lastLeft, Token operator, Token firstRight, int l, int c1, int c2, int c3, int c4 |
1020+
lastLeft = getLeftOperand().getLastToken() and
1021+
operator = lastLeft.getNextToken() and
1022+
firstRight = operator.getNextToken() and
1023+
lastLeft.getLocation().hasLocationInfo(_, _, _, l, c1) and
1024+
operator.getLocation().hasLocationInfo(_, l, c2, l, c3) and
1025+
firstRight.getLocation().hasLocationInfo(_, l, c4, _, _) and
1026+
result = c2-c1-1 and
1027+
result = c4-c3-1
1028+
)
1029+
}
10111030
}
10121031

10131032
/**
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
| tst.js:1:9:1:17 | 0x0A != 0 | The '!=' operator binds more tightly than '&', which may not be obvious in this case. |
2+
| tst.js:6:1:6:7 | x !== y | The '!==' operator binds more tightly than '&', which may not be obvious in this case. |
3+
| tst.js:10:3:10:6 | b==c | The '==' operator binds more tightly than '&', which may not be obvious in this case. |
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Expressions/UnclearOperatorPrecedence.ql
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
x.f() & 0x0A != 0; // NOT OK
2+
x.f() & (0x0A != 0); // OK
3+
x.f() & 0x0A != 0; // OK
4+
x.f() & 0x0A!=0; // OK
5+
6+
x !== y & 1; // NOT OK
7+
8+
x > 0 & x < 10; // OK
9+
10+
a&b==c; // NOT OK

0 commit comments

Comments
 (0)