Skip to content

Commit e2cdf5d

Browse files
committed
JavaScript: add string concatenation library
1 parent a3562aa commit e2cdf5d

File tree

10 files changed

+259
-54
lines changed

10 files changed

+259
-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: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
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 |
32+
node = array.getAMethodCall("join") and
33+
node.(DataFlow::MethodCallNode).getArgument(0).mayHaveStringValue("") and
34+
result = array.getElement(n))
35+
}
36+
37+
/** Gets an operand to the string concatenation defining `node`. */
38+
DataFlow::Node getAnOperand(DataFlow::Node node) {
39+
result = getOperand(node, _)
40+
}
41+
42+
/** Gets the number of operands to the given concatenation. */
43+
int getNumOperand(DataFlow::Node node) {
44+
result = strictcount(getAnOperand(node))
45+
}
46+
47+
/** Gets the first operand to the string concatenation defining `node`. */
48+
DataFlow::Node getFirstOperand(DataFlow::Node node) {
49+
result = getOperand(node, 0)
50+
}
51+
52+
/** Gets the last operand to the string concatenation defining `node`. */
53+
DataFlow::Node getLastOperand(DataFlow::Node node) {
54+
result = getOperand(node, getNumOperand(node) - 1)
55+
}
56+
57+
/**
58+
* Holds if `src` flows to `dst` through the `n`th operand of the given concatenation operator.
59+
*/
60+
predicate taintStep(DataFlow::Node src, DataFlow::Node dst, DataFlow::Node operator, int n) {
61+
src = getOperand(dst, n) and
62+
operator = dst
63+
}
64+
65+
/**
66+
* Holds if there is a taint step from `src` to `dst` through string concatenation.
67+
*/
68+
predicate taintStep(DataFlow::Node src, DataFlow::Node dst) {
69+
taintStep(src, dst, _, _)
70+
}
71+
}

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: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
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:41 | ["one", ... oin("") |
18+
| tst.js:25:18:25:22 | "two" |
19+
| tst.js:29:10:29:46 | Array(" ... oin("") |
20+
| tst.js:29:23:29:27 | "two" |
21+
| tst.js:33:10:33:50 | new Arr ... oin("") |
22+
| tst.js:33:27:33:31 | "two" |
23+
| tst.js:38:11:38:15 | "two" |
24+
| tst.js:46:23:46:27 | "two" |
25+
| tst.js:53:10:53:34 | `one ${ ... three` |
26+
| tst.js:53:19:53:23 | two |
27+
| tst.js:71:14:71:18 | "two" |
28+
| 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
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
function append() {
2+
let x = "one";
3+
x += "two";
4+
x += "three"
5+
x += "four"
6+
return x;
7+
}
8+
9+
function appendClosure(ys) {
10+
let x = "first";
11+
ys.forEach(y => {
12+
x += "one" + y + "two";
13+
});
14+
x += "last";
15+
return x;
16+
}
17+
18+
function appendMixed() {
19+
let x = "one" + "two";
20+
x += ("three" + "four");
21+
return x + "five";
22+
}
23+
24+
function joinArrayLiteral() {
25+
return ["one", "two", "three"].join("");
26+
}
27+
28+
function joinArrayCall() {
29+
return Array("one", "two", "three").join("");
30+
}
31+
32+
function joinArrayNewCall() {
33+
return new Array("one", "two", "three").join("");
34+
}
35+
36+
function push() {
37+
let xs = ["one"];
38+
xs.push("two");
39+
xs.push("three", "four");
40+
return xs.join("");
41+
}
42+
43+
function pushClosure(ys) {
44+
let xs = ["first"];
45+
ys.forEach(y => {
46+
xs.push("one", y, "two");
47+
});
48+
xs.push("last");
49+
return xs.join("");
50+
}
51+
52+
function template(x) {
53+
return `one ${x} two ${x} three`;
54+
}
55+
56+
function taggedTemplate(mid) {
57+
return someTag`first ${mid} last`;
58+
}
59+
60+
function templateRepeated(x) {
61+
return `first ${x}${x}${x} last`;
62+
}
63+
64+
function makeArray() {
65+
return [];
66+
}
67+
68+
function pushNoLocalCreation() {
69+
let array = makeArray();
70+
array.push("one");
71+
array.push("two");
72+
array.push("three");
73+
return array.join("");
74+
}
75+
76+
function joinInClosure() {
77+
let array = ["one", "two", "three"];
78+
function f() {
79+
return array.join();
80+
}
81+
return f();
82+
}

0 commit comments

Comments
 (0)