Skip to content

Commit 4aca8f4

Browse files
authored
Merge pull request #201 from asger-semmle/string-concatenation-squashed
Approved by esben-semmle
2 parents 2f4aa64 + 1d793c0 commit 4aca8f4

File tree

12 files changed

+283
-54
lines changed

12 files changed

+283
-54
lines changed

javascript/ql/src/javascript.qll

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import semmle.javascript.Regexp
3838
import semmle.javascript.SSA
3939
import semmle.javascript.StandardLibrary
4040
import semmle.javascript.Stmt
41+
import semmle.javascript.StringConcatenation
4142
import semmle.javascript.Templates
4243
import semmle.javascript.Tokens
4344
import semmle.javascript.TypeScript
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
/**
2+
* Provides predicates for analyzing string concatenations and their operands.
3+
*/
4+
import javascript
5+
6+
module StringConcatenation {
7+
/** Gets a data flow node referring to the result of the given concatenation. */
8+
private DataFlow::Node getAssignAddResult(AssignAddExpr expr) {
9+
result = expr.flow()
10+
or
11+
exists (SsaExplicitDefinition def | def.getDef() = expr |
12+
result = DataFlow::valueNode(def.getVariable().getAUse()))
13+
}
14+
15+
/** Gets the `n`th operand to the string concatenation defining `node`. */
16+
DataFlow::Node getOperand(DataFlow::Node node, int n) {
17+
exists (AddExpr add | node = add.flow() |
18+
n = 0 and result = add.getLeftOperand().flow()
19+
or
20+
n = 1 and result = add.getRightOperand().flow())
21+
or
22+
exists (TemplateLiteral template | node = template.flow() |
23+
result = template.getElement(n).flow() and
24+
not exists (TaggedTemplateExpr tag | template = tag.getTemplate()))
25+
or
26+
exists (AssignAddExpr assign | node = getAssignAddResult(assign) |
27+
n = 0 and result = assign.getLhs().flow()
28+
or
29+
n = 1 and result = assign.getRhs().flow())
30+
or
31+
exists (DataFlow::ArrayCreationNode array, DataFlow::MethodCallNode call |
32+
call = array.getAMethodCall("join") and
33+
call.getArgument(0).mayHaveStringValue("") and
34+
(
35+
// step from array element to array
36+
result = array.getElement(n) and
37+
node = array
38+
or
39+
// step from array to join call
40+
node = call and
41+
result = array and
42+
n = 0
43+
))
44+
}
45+
46+
/** Gets an operand to the string concatenation defining `node`. */
47+
DataFlow::Node getAnOperand(DataFlow::Node node) {
48+
result = getOperand(node, _)
49+
}
50+
51+
/** Gets the number of operands to the given concatenation. */
52+
int getNumOperand(DataFlow::Node node) {
53+
result = strictcount(getAnOperand(node))
54+
}
55+
56+
/** Gets the first operand to the string concatenation defining `node`. */
57+
DataFlow::Node getFirstOperand(DataFlow::Node node) {
58+
result = getOperand(node, 0)
59+
}
60+
61+
/** Gets the last operand to the string concatenation defining `node`. */
62+
DataFlow::Node getLastOperand(DataFlow::Node node) {
63+
result = getOperand(node, getNumOperand(node) - 1)
64+
}
65+
66+
/**
67+
* Holds if `src` flows to `dst` through the `n`th operand of the given concatenation operator.
68+
*/
69+
predicate taintStep(DataFlow::Node src, DataFlow::Node dst, DataFlow::Node operator, int n) {
70+
src = getOperand(dst, n) and
71+
operator = dst
72+
}
73+
74+
/**
75+
* Holds if there is a taint step from `src` to `dst` through string concatenation.
76+
*/
77+
predicate taintStep(DataFlow::Node src, DataFlow::Node dst) {
78+
taintStep(src, dst, _, _)
79+
}
80+
}

javascript/ql/src/semmle/javascript/dataflow/Nodes.qll

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,47 @@ class ArrayLiteralNode extends DataFlow::ValueNode, DataFlow::DefaultSourceNode
304304

305305
}
306306

307+
/** A data flow node corresponding to a `new Array()` or `Array()` invocation. */
308+
class ArrayConstructorInvokeNode extends DataFlow::InvokeNode {
309+
ArrayConstructorInvokeNode() {
310+
getCallee() = DataFlow::globalVarRef("Array")
311+
}
312+
313+
/** Gets the `i`th initial element of this array, if one is provided. */
314+
DataFlow::ValueNode getElement(int i) {
315+
getNumArgument() > 1 and // A single-argument invocation specifies the array length, not an element.
316+
result = getArgument(i)
317+
}
318+
319+
/** Gets an initial element of this array, if one is provided. */
320+
DataFlow::ValueNode getAnElement() {
321+
getNumArgument() > 1 and
322+
result = getAnArgument()
323+
}
324+
}
325+
326+
/**
327+
* A data flow node corresponding to the creation or a new array, either through an array literal
328+
* or an invocation of the `Array` constructor.
329+
*/
330+
class ArrayCreationNode extends DataFlow::ValueNode, DataFlow::DefaultSourceNode {
331+
ArrayCreationNode() {
332+
this instanceof ArrayLiteralNode or
333+
this instanceof ArrayConstructorInvokeNode
334+
}
335+
336+
/** Gets the `i`th initial element of this array, if one is provided. */
337+
DataFlow::ValueNode getElement(int i) {
338+
result = this.(ArrayLiteralNode).getElement(i) or
339+
result = this.(ArrayConstructorInvokeNode).getElement(i)
340+
}
341+
342+
/** Gets an initial element of this array, if one if provided. */
343+
DataFlow::ValueNode getAnElement() {
344+
result = getElement(_)
345+
}
346+
}
347+
307348
/**
308349
* A data flow node corresponding to a `default` import from a module, or a
309350
* (AMD or CommonJS) `require` of a module.

javascript/ql/src/semmle/javascript/dataflow/TaintTracking.qll

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -358,19 +358,8 @@ module TaintTracking {
358358
*/
359359
class StringConcatenationTaintStep extends AdditionalTaintStep, DataFlow::ValueNode {
360360
override predicate step(DataFlow::Node pred, DataFlow::Node succ) {
361-
succ = this and
362-
(
363-
// addition propagates taint
364-
astNode.(AddExpr).getAnOperand() = pred.asExpr() or
365-
astNode.(AssignAddExpr).getAChildExpr() = pred.asExpr() or
366-
exists (SsaExplicitDefinition ssa |
367-
astNode = ssa.getVariable().getAUse() and
368-
pred.asExpr().(AssignAddExpr) = ssa.getDef()
369-
)
370-
or
371-
// templating propagates taint
372-
astNode.(TemplateLiteral).getAnElement() = pred.asExpr()
373-
)
361+
succ = this and
362+
StringConcatenation::taintStep(pred, succ)
374363
}
375364
}
376365

javascript/ql/src/semmle/javascript/security/dataflow/DomBasedXss.qll

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -77,9 +77,9 @@ module DomBasedXss {
7777
or
7878
// or it doesn't start with something other than `<`, and so at least
7979
// _may_ be interpreted as HTML
80-
not exists (Expr prefix, string strval |
80+
not exists (DataFlow::Node prefix, string strval |
8181
isPrefixOfJQueryHtmlString(astNode, prefix) and
82-
strval = prefix.getStringValue() and
82+
strval = prefix.asExpr().getStringValue() and
8383
not strval.regexpMatch("\\s*<.*")
8484
)
8585
)
@@ -93,13 +93,14 @@ module DomBasedXss {
9393
* Holds if `prefix` is a prefix of `htmlString`, which may be intepreted as
9494
* HTML by a jQuery method.
9595
*/
96-
private predicate isPrefixOfJQueryHtmlString(Expr htmlString, Expr prefix) {
96+
private predicate isPrefixOfJQueryHtmlString(Expr htmlString, DataFlow::Node prefix) {
9797
any(JQueryMethodCall call).interpretsArgumentAsHtml(htmlString) and
98-
prefix = htmlString
98+
prefix = htmlString.flow()
9999
or
100-
exists (Expr pred | isPrefixOfJQueryHtmlString(htmlString, pred) |
101-
prefix = pred.(AddExpr).getLeftOperand() or
102-
prefix = pred.(ParExpr).getExpression()
100+
exists (DataFlow::Node pred | isPrefixOfJQueryHtmlString(htmlString, pred) |
101+
prefix = StringConcatenation::getFirstOperand(pred)
102+
or
103+
prefix = pred.getAPredecessor()
103104
)
104105
}
105106

javascript/ql/src/semmle/javascript/security/dataflow/ServerSideUrlRedirect.qll

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -34,33 +34,23 @@ module ServerSideUrlRedirect {
3434
)
3535
}
3636
}
37-
38-
/**
39-
* Gets the left operand of `nd` if it is a concatenation.
40-
*/
41-
private DataFlow::Node getPrefixOperand(DataFlow::Node nd) {
42-
exists (Expr e | e instanceof AddExpr or e instanceof AssignAddExpr |
43-
nd = DataFlow::valueNode(e) and
44-
result = DataFlow::valueNode(e.getChildExpr(0))
45-
)
46-
}
4737

4838
/**
4939
* Gets a node that is transitively reachable from `nd` along prefix predecessor edges.
5040
*/
5141
private DataFlow::Node prefixCandidate(Sink sink) {
5242
result = sink or
53-
result = getPrefixOperand(prefixCandidate(sink)) or
54-
result = prefixCandidate(sink).getAPredecessor()
43+
result = prefixCandidate(sink).getAPredecessor() or
44+
result = StringConcatenation::getFirstOperand(prefixCandidate(sink))
5545
}
56-
46+
5747
/**
5848
* Gets an expression that may end up being a prefix of the string concatenation `nd`.
5949
*/
6050
private Expr getAPrefix(Sink sink) {
6151
exists (DataFlow::Node prefix |
6252
prefix = prefixCandidate(sink) and
63-
not exists(getPrefixOperand(prefix)) and
53+
not exists(StringConcatenation::getFirstOperand(prefix)) and
6454
not exists(prefix.getAPredecessor()) and
6555
result = prefix.asExpr()
6656
)

javascript/ql/src/semmle/javascript/security/dataflow/UrlConcatenation.qll

Lines changed: 7 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,13 @@ import javascript
1111
* `nd` or one of its operands, assuming that it is a concatenation.
1212
*/
1313
private predicate hasSanitizingSubstring(DataFlow::Node nd) {
14-
exists (Expr e | e = nd.asExpr() |
15-
(e instanceof AddExpr or e instanceof AssignAddExpr) and
16-
hasSanitizingSubstring(DataFlow::valueNode(e.getAChildExpr()))
17-
or
18-
e.getStringValue().regexpMatch(".*[?#].*")
19-
)
14+
nd.asExpr().getStringValue().regexpMatch(".*[?#].*")
2015
or
21-
nd.isIncomplete(_)
16+
hasSanitizingSubstring(StringConcatenation::getAnOperand(nd))
2217
or
2318
hasSanitizingSubstring(nd.getAPredecessor())
19+
or
20+
nd.isIncomplete(_)
2421
}
2522

2623
/**
@@ -30,17 +27,7 @@ private predicate hasSanitizingSubstring(DataFlow::Node nd) {
3027
* This is considered as a sanitizing edge for the URL redirection queries.
3128
*/
3229
predicate sanitizingPrefixEdge(DataFlow::Node source, DataFlow::Node sink) {
33-
exists (AddExpr add, DataFlow::Node left |
34-
source.asExpr() = add.getRightOperand() and
35-
sink.asExpr() = add and
36-
left.asExpr() = add.getLeftOperand() and
37-
hasSanitizingSubstring(left)
38-
)
39-
or
40-
exists (TemplateLiteral tl, int i, DataFlow::Node elt |
41-
source.asExpr() = tl.getElement(i) and
42-
sink.asExpr() = tl and
43-
elt.asExpr() = tl.getElement([0..i-1]) and
44-
hasSanitizingSubstring(elt)
45-
)
30+
exists (DataFlow::Node operator, int n |
31+
StringConcatenation::taintStep(source, sink, operator, n) and
32+
hasSanitizingSubstring(StringConcatenation::getOperand(operator, [0..n-1])))
4633
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
| tst.js:3:3:3:12 | x += "two" |
2+
| tst.js:3:8:3:12 | "two" |
3+
| tst.js:4:3:4:3 | x |
4+
| tst.js:4:3:4:14 | x += "three" |
5+
| tst.js:5:3:5:3 | x |
6+
| tst.js:5:3:5:13 | x += "four" |
7+
| tst.js:6:10:6:10 | x |
8+
| tst.js:12:5:12:26 | x += "o ... + "two" |
9+
| tst.js:12:10:12:26 | "one" + y + "two" |
10+
| tst.js:12:22:12:26 | "two" |
11+
| tst.js:19:11:19:23 | "one" + "two" |
12+
| tst.js:19:19:19:23 | "two" |
13+
| tst.js:20:3:20:3 | x |
14+
| tst.js:20:3:20:25 | x += (" ... "four") |
15+
| tst.js:21:10:21:10 | x |
16+
| tst.js:21:10:21:19 | x + "five" |
17+
| tst.js:25:10:25:32 | ["one", ... three"] |
18+
| tst.js:25:10:25:41 | ["one", ... oin("") |
19+
| tst.js:25:18:25:22 | "two" |
20+
| tst.js:29:10:29:37 | Array(" ... three") |
21+
| tst.js:29:10:29:46 | Array(" ... oin("") |
22+
| tst.js:29:23:29:27 | "two" |
23+
| tst.js:33:10:33:41 | new Arr ... three") |
24+
| tst.js:33:10:33:50 | new Arr ... oin("") |
25+
| tst.js:33:27:33:31 | "two" |
26+
| tst.js:38:11:38:15 | "two" |
27+
| tst.js:46:23:46:27 | "two" |
28+
| tst.js:53:10:53:34 | `one ${ ... three` |
29+
| tst.js:53:19:53:23 | two |
30+
| tst.js:71:14:71:18 | "two" |
31+
| tst.js:77:23:77:27 | "two" |
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import javascript
2+
3+
// Select all expressions whose string value contains the word "two"
4+
5+
predicate containsTwo(DataFlow::Node node) {
6+
node.asExpr().getStringValue().regexpMatch(".*two.*")
7+
or
8+
containsTwo(node.getAPredecessor())
9+
or
10+
containsTwo(StringConcatenation::getAnOperand(node))
11+
}
12+
13+
from Expr e
14+
where containsTwo(e.flow())
15+
select e

0 commit comments

Comments
 (0)