Skip to content

Commit

Permalink
Merge pull request #14405 from erik-krogh/tagCall
Browse files Browse the repository at this point in the history
JS: recognize tagged template literals as `DataFlow::CallNode`
  • Loading branch information
erik-krogh authored Oct 11, 2023
2 parents aa7a667 + 6377e92 commit 85bb14f
Show file tree
Hide file tree
Showing 25 changed files with 132 additions and 61 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
category: minorAnalysis
---
* Tagged template literals have been added to `DataFlow::CallNode`. This allows the analysis to find flow into functions called with a tagged template literal,
and the arguments to a tagged template literal are part of the API-graph in `ApiGraphs.qll`.
35 changes: 35 additions & 0 deletions javascript/ql/lib/semmle/javascript/dataflow/DataFlow.qll
Original file line number Diff line number Diff line change
Expand Up @@ -1269,6 +1269,41 @@ module DataFlow {
result >= 0 and kind = "call" and result = originalCall.getNumArgument() - 1
}
}

/**
* A data flow node representing a call with a tagged template literal.
*/
private class TaggedTemplateLiteralCallNode extends CallNodeDef, ValueNode {
override TaggedTemplateExpr astNode;

override InvokeExpr getInvokeExpr() { none() } // There is no InvokeExpr for this.

override string getCalleeName() {
result = astNode.getTag().getUnderlyingValue().(Identifier).getName()
}

override DataFlow::Node getCalleeNode() { result = DataFlow::valueNode(astNode.getTag()) }

override DataFlow::Node getArgument(int i) {
// the first argument sent to the function is the array of string parts, which we don't model.
// rank is 1-indexed, which is perfect here.
result =
DataFlow::valueNode(rank[i](Expr e, int index |
e = astNode.getTemplate().getElement(index) and not e instanceof TemplateElement
|
e order by index
))
}

override DataFlow::Node getAnArgument() { result = this.getArgument(_) }

override DataFlow::Node getASpreadArgument() { none() }

// we don't model the string constants as arguments, but we still count them.
override int getNumArgument() { result = count(this.getArgument(_)) + 1 }

override DataFlow::Node getReceiver() { none() }
}
}

/**
Expand Down
13 changes: 10 additions & 3 deletions javascript/ql/lib/semmle/javascript/dataflow/Nodes.qll
Original file line number Diff line number Diff line change
Expand Up @@ -92,13 +92,20 @@ class InvokeNode extends DataFlow::SourceNode instanceof DataFlow::Impl::InvokeN
* but the position of `z` cannot be determined, hence there are no first and second
* argument nodes.
*/
DataFlow::Node getArgument(int i) { result = super.getArgument(i) }
cached
DataFlow::Node getArgument(int i) {
result = super.getArgument(i) and Stages::DataFlowStage::ref()
}

/** Gets the data flow node corresponding to an argument of this invocation. */
DataFlow::Node getAnArgument() { result = super.getAnArgument() }
cached
DataFlow::Node getAnArgument() { result = super.getAnArgument() and Stages::DataFlowStage::ref() }

/** Gets the data flow node corresponding to the last argument of this invocation. */
DataFlow::Node getLastArgument() { result = this.getArgument(this.getNumArgument() - 1) }
cached
DataFlow::Node getLastArgument() {
result = this.getArgument(this.getNumArgument() - 1) and Stages::DataFlowStage::ref()
}

/**
* Gets a data flow node corresponding to an array of values being passed as
Expand Down
6 changes: 6 additions & 0 deletions javascript/ql/lib/semmle/javascript/internal/CachedStages.qll
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,12 @@ module Stages {
exists(any(DataFlow::PropRef ref).getBase())
or
exists(any(DataFlow::ClassNode cls))
or
exists(any(DataFlow::CallNode node).getArgument(_))
or
exists(any(DataFlow::CallNode node).getAnArgument())
or
exists(any(DataFlow::CallNode node).getLastArgument())
}
}

Expand Down
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
import ApiGraphs.VerifyAssertions
9 changes: 9 additions & 0 deletions javascript/ql/test/ApiGraphs/tagged-template/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
const tag = require("tag");

tag.string`string1
${23}` // def=moduleImport("tag").getMember("exports").getMember("string").getParameter(1)

tag.highlight`string2
${23}
morestring
${42}` // def=moduleImport("tag").getMember("exports").getMember("highlight").getParameter(2)
3 changes: 3 additions & 0 deletions javascript/ql/test/ApiGraphs/tagged-template/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"name": "tagged-template"
}

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
function fooTag(strings, par1, par2) {

}

fooTag`hello ${arg1} world ${arg2}`
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,8 @@ test_getAFunctionValue
| strict.js:1:1:8:2 | (functi ... ode.\\n}) | strict.js:1:2:8:1 | functio ... mode.\\n} |
| strict.js:1:2:8:1 | functio ... mode.\\n} | strict.js:1:2:8:1 | functio ... mode.\\n} |
| strict.js:3:5:5:5 | functio ... ;\\n } | strict.js:3:5:5:5 | functio ... ;\\n } |
| taggedTemplate.js:1:1:3:1 | functio ... 2) {\\n\\n} | taggedTemplate.js:1:1:3:1 | functio ... 2) {\\n\\n} |
| taggedTemplate.js:5:1:5:6 | fooTag | taggedTemplate.js:1:1:3:1 | functio ... 2) {\\n\\n} |
| tst3.js:1:1:1:22 | functio ... fn() {} | tst3.js:1:1:1:22 | functio ... fn() {} |
| tst3.js:2:1:2:23 | functio ... n2() {} | tst3.js:2:1:2:23 | functio ... n2() {} |
| tst.js:1:1:1:15 | function f() {} | tst.js:1:1:1:15 | function f() {} |
Expand Down Expand Up @@ -221,6 +223,8 @@ test_getArgument
| reflection.js:7:1:7:22 | reflective call | 1 | reflection.js:7:20:7:21 | 19 |
| reflection.js:8:1:8:25 | add.app ... 3, 19]) | 0 | reflection.js:8:11:8:14 | null |
| reflection.js:8:1:8:25 | add.app ... 3, 19]) | 1 | reflection.js:8:17:8:24 | [23, 19] |
| taggedTemplate.js:5:1:5:35 | fooTag` ... {arg2}` | 1 | taggedTemplate.js:5:16:5:19 | arg1 |
| taggedTemplate.js:5:1:5:35 | fooTag` ... {arg2}` | 2 | taggedTemplate.js:5:30:5:33 | arg2 |
| tst.js:22:1:22:4 | l(k) | 0 | tst.js:22:3:22:3 | k |
| tst.js:42:2:42:29 | functio ... x; }(o) | 0 | tst.js:42:28:42:28 | o |
test_getNumArgument
Expand Down Expand Up @@ -259,6 +263,7 @@ test_getNumArgument
| strict2.js:9:10:9:14 | foo() | 0 |
| strict.js:1:1:8:4 | (functi ... e.\\n})() | 0 |
| strict.js:7:10:7:14 | foo() | 0 |
| taggedTemplate.js:5:1:5:35 | fooTag` ... {arg2}` | 3 |
| tst.js:6:1:6:3 | f() | 0 |
| tst.js:7:1:7:3 | g() | 0 |
| tst.js:8:1:8:3 | h() | 0 |
Expand Down Expand Up @@ -362,6 +367,7 @@ test_getCalleeNode
| strict2.js:9:10:9:14 | foo() | strict2.js:9:10:9:12 | foo |
| strict.js:1:1:8:4 | (functi ... e.\\n})() | strict.js:1:1:8:2 | (functi ... ode.\\n}) |
| strict.js:7:10:7:14 | foo() | strict.js:7:10:7:12 | foo |
| taggedTemplate.js:5:1:5:35 | fooTag` ... {arg2}` | taggedTemplate.js:5:1:5:6 | fooTag |
| tst.js:6:1:6:3 | f() | tst.js:6:1:6:1 | f |
| tst.js:7:1:7:3 | g() | tst.js:7:1:7:1 | g |
| tst.js:8:1:8:3 | h() | tst.js:8:1:8:1 | h |
Expand Down Expand Up @@ -400,6 +406,7 @@ test_getLastArgument
| reflection.js:7:1:7:22 | add.cal ... 23, 19) | reflection.js:7:20:7:21 | 19 |
| reflection.js:7:1:7:22 | reflective call | reflection.js:7:20:7:21 | 19 |
| reflection.js:8:1:8:25 | add.app ... 3, 19]) | reflection.js:8:17:8:24 | [23, 19] |
| taggedTemplate.js:5:1:5:35 | fooTag` ... {arg2}` | taggedTemplate.js:5:30:5:33 | arg2 |
| tst.js:22:1:22:4 | l(k) | tst.js:22:3:22:3 | k |
| tst.js:42:2:42:29 | functio ... x; }(o) | tst.js:42:28:42:28 | o |
test_getAnArgument
Expand All @@ -420,6 +427,8 @@ test_getAnArgument
| reflection.js:7:1:7:22 | reflective call | reflection.js:7:20:7:21 | 19 |
| reflection.js:8:1:8:25 | add.app ... 3, 19]) | reflection.js:8:11:8:14 | null |
| reflection.js:8:1:8:25 | add.app ... 3, 19]) | reflection.js:8:17:8:24 | [23, 19] |
| taggedTemplate.js:5:1:5:35 | fooTag` ... {arg2}` | taggedTemplate.js:5:16:5:19 | arg1 |
| taggedTemplate.js:5:1:5:35 | fooTag` ... {arg2}` | taggedTemplate.js:5:30:5:33 | arg2 |
| tst.js:22:1:22:4 | l(k) | tst.js:22:3:22:3 | k |
| tst.js:42:2:42:29 | functio ... x; }(o) | tst.js:42:28:42:28 | o |
test_getACallee
Expand Down Expand Up @@ -449,6 +458,7 @@ test_getACallee
| reflection.js:8:1:8:25 | reflective call | reflection.js:1:1:3:1 | functio ... x+y;\\n} |
| strict2.js:2:1:10:4 | (functi ... e.\\n})() | strict2.js:2:2:10:1 | functio ... mode.\\n} |
| strict.js:1:1:8:4 | (functi ... e.\\n})() | strict.js:1:2:8:1 | functio ... mode.\\n} |
| taggedTemplate.js:5:1:5:35 | fooTag` ... {arg2}` | taggedTemplate.js:1:1:3:1 | functio ... 2) {\\n\\n} |
| tst.js:6:1:6:3 | f() | tst.js:1:1:1:15 | function f() {} |
| tst.js:7:1:7:3 | g() | tst.js:2:9:2:21 | function() {} |
| tst.js:8:1:8:3 | h() | tst.js:3:5:3:17 | function() {} |
Expand Down Expand Up @@ -509,6 +519,7 @@ test_getCalleeName
| reflection.js:8:1:8:25 | add.app ... 3, 19]) | apply |
| strict2.js:9:10:9:14 | foo() | foo |
| strict.js:7:10:7:14 | foo() | foo |
| taggedTemplate.js:5:1:5:35 | fooTag` ... {arg2}` | fooTag |
| tst.js:6:1:6:3 | f() | f |
| tst.js:7:1:7:3 | g() | g |
| tst.js:8:1:8:3 | h() | h |
Expand Down
48 changes: 37 additions & 11 deletions javascript/ql/test/library-tests/CallGraphs/FullTest/tests.ql
Original file line number Diff line number Diff line change
@@ -1,11 +1,37 @@
import isUncertain
import getAFunctionValue
import getArgument
import getNumArgument
import isIncomplete
import getCalleeNode
import getLastArgument
import getAnArgument
import getACallee
import getCalleeName
import isImprecise
import javascript

query predicate test_isUncertain(DataFlow::InvokeNode invk) { invk.isUncertain() }

query predicate test_getAFunctionValue(DataFlow::Node node, DataFlow::FunctionNode res) {
res = node.getAFunctionValue()
}

query predicate test_getArgument(DataFlow::InvokeNode invk, int i, DataFlow::Node res) {
res = invk.getArgument(i)
}

query predicate test_getNumArgument(DataFlow::InvokeNode invk, int res) {
res = invk.getNumArgument()
}

query predicate test_isIncomplete(DataFlow::InvokeNode invk) { invk.isIncomplete() }

query predicate test_getCalleeNode(DataFlow::InvokeNode invk, DataFlow::Node res) {
res = invk.getCalleeNode()
}

query predicate test_getLastArgument(DataFlow::InvokeNode invk, DataFlow::Node res) {
res = invk.getLastArgument()
}

query predicate test_getAnArgument(DataFlow::InvokeNode invk, DataFlow::Node res) {
res = invk.getAnArgument()
}

query predicate test_getACallee(DataFlow::InvokeNode c, Function res) { res = c.getACallee() }

query predicate test_getCalleeName(DataFlow::InvokeNode invk, string res) {
res = invk.getCalleeName()
}

query predicate test_isImprecise(DataFlow::InvokeNode invk) { invk.isImprecise() }
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,7 @@ typeInferenceMismatch
| tst.js:2:13:2:20 | source() | tst.js:47:10:47:30 | Buffer. ... 'hex') |
| tst.js:2:13:2:20 | source() | tst.js:48:10:48:22 | new Buffer(x) |
| tst.js:2:13:2:20 | source() | tst.js:51:10:51:31 | seriali ... ript(x) |
| tst.js:2:13:2:20 | source() | tst.js:54:14:54:19 | unsafe |
| xml.js:5:18:5:25 | source() | xml.js:8:14:8:17 | text |
| xml.js:12:17:12:24 | source() | xml.js:13:14:13:19 | result |
| xml.js:23:18:23:25 | source() | xml.js:20:14:20:17 | attr |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,3 +109,4 @@
| thisAssignments.js:4:17:4:24 | source() | thisAssignments.js:5:10:5:18 | obj.field |
| thisAssignments.js:7:19:7:26 | source() | thisAssignments.js:8:10:8:20 | this.field2 |
| tst.js:2:13:2:20 | source() | tst.js:4:10:4:10 | x |
| tst.js:2:13:2:20 | source() | tst.js:54:14:54:19 | unsafe |
8 changes: 8 additions & 0 deletions javascript/ql/test/library-tests/TaintTracking/tst.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,12 @@ function test() {

const serializeJavaScript = require("serialize-javascript");
sink(serializeJavaScript(x)) // NOT OK

function tagged(strings, safe, unsafe) {
sink(unsafe) // NOT OK
sink(safe) // OK
sink(strings) // OK
}

tagged`foo ${"safe"} bar ${x} baz`;
}

0 comments on commit 85bb14f

Please sign in to comment.