Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JS: Support a taint tracking for arguments of .apply() function call #6559

Draft
wants to merge 9 commits into
base: main
Choose a base branch
from
4 changes: 1 addition & 3 deletions javascript/ql/lib/semmle/javascript/Arrays.qll
Original file line number Diff line number Diff line change
Expand Up @@ -252,9 +252,7 @@ private module ArrayDataFlow {
exists(DataFlow::ArrayCreationNode array, int i |
element = array.getElement(i) and
obj = array and
if array = any(PromiseAllCreation c).getArrayNode()
then prop = arrayElement(i)
else prop = arrayElement()
prop = arrayElement(i)
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -801,7 +801,7 @@ module PseudoProperties {
*/
bindingset[i]
string arrayElement(int i) {
i < 5 and result = i.toString()
result = i.toString()
or
result = arrayElement()
}
Expand Down
95 changes: 81 additions & 14 deletions javascript/ql/lib/semmle/javascript/dataflow/DataFlow.qll
Original file line number Diff line number Diff line change
Expand Up @@ -1161,14 +1161,47 @@ module DataFlow {
override NewExpr astNode;
}

/**
* A data flow node representing arguments of the `.apply` function call
* to emulate a separate argument for each parameter of a reflective function call.
*/
private class ApplyArgumentNode extends DataFlow::Node {
ExplicitMethodCallNode call;
Node arrayArgument;
int index;

ApplyArgumentNode() {
this = TApplyArgumentNode(call.asExpr(), index) and
arrayArgument = call.getArgument(1)
}

/**
* Gets an explicit call of the `.apply` function call
* that takes an argument represented by this data flow node.
*/
ExplicitMethodCallNode getCall() { result = call }

/** Gets an argument index represented by this data flow node. */
int getIndex() { result = index }

override string toString() {
result = arrayArgument.toString() + "[" + index.toString() + "]"
}

override predicate hasLocationInfo(
string filepath, int startline, int startcolumn, int endline, int endcolumn
) {
arrayArgument.hasLocationInfo(filepath, startline, startcolumn, endline, endcolumn)
}

override BasicBlock getBasicBlock() { result = call.getBasicBlock() }
}

/**
* A data flow node representing a reflective function call.
*/
private class ReflectiveCallNodeDef extends CallNodeDef {
abstract private class ReflectiveCallNodeDef extends CallNodeDef {
ExplicitMethodCallNode originalCall;
string kind;

ReflectiveCallNodeDef() { this = TReflectiveCallNode(originalCall.asExpr(), kind) }

override InvokeExpr getInvokeExpr() { result = originalCall.getInvokeExpr() }

Expand All @@ -1179,25 +1212,59 @@ module DataFlow {
override DataFlow::Node getCalleeNode() { result = originalCall.getReceiver() }

override DataFlow::Node getReceiver() { result = originalCall.getArgument(0) }
}

/**
* A data flow node representing a `.call` reflective function call.
*/
private class CallReflectiveCallNodeDef extends ReflectiveCallNodeDef {
CallReflectiveCallNodeDef() { this = TReflectiveCallNode(originalCall.asExpr(), "call") }

override DataFlow::Node getArgument(int i) {
i >= 0 and kind = "call" and result = originalCall.getArgument(i + 1)
i >= 0 and result = originalCall.getArgument(i + 1)
}

override DataFlow::Node getAnArgument() {
kind = "call" and result = originalCall.getAnArgument() and result != getReceiver()
result = originalCall.getAnArgument() and
result != getReceiver()
}

override DataFlow::Node getASpreadArgument() {
kind = "apply" and
result = originalCall.getArgument(1)
or
kind = "call" and
result = originalCall.getASpreadArgument()
override DataFlow::Node getASpreadArgument() { result = originalCall.getASpreadArgument() }

override int getNumArgument() { result >= 0 and result = originalCall.getNumArgument() - 1 }
}

/**
* A data flow node representing a `.apply` reflective function call.
*/
class ApplyReflectiveCallNodeDef extends ReflectiveCallNodeDef {
ApplyReflectiveCallNodeDef() { this = TReflectiveCallNode(originalCall.asExpr(), "apply") }

ApplyArgumentNode getApplyArgument(int i) {
result.getCall() = originalCall and
result.getIndex() = i
}

override int getNumArgument() {
result >= 0 and kind = "call" and result = originalCall.getNumArgument() - 1
override DataFlow::Node getArgument(int i) { none() }

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

override DataFlow::Node getASpreadArgument() { result = originalCall.getArgument(1) }

override int getNumArgument() { none() }
}

/**
* A step modelling that a call of `.apply()` function with passing an array of arguments via 2nd parameter.
*/
private class ApplyCallStep extends PreCallGraphStep {
override predicate loadStep(DataFlow::Node pred, DataFlow::Node succ, string prop) {
exists(ApplyReflectiveCallNodeDef call, int i |
prop = DataFlow::PseudoProperties::arrayElement(i) and
not prop = DataFlow::PseudoProperties::arrayElement() and
pred = call.getASpreadArgument().getALocalSource() and
succ = call.getApplyArgument(i)
)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,10 @@ newtype TNode =
TExceptionalFunctionReturnNode(Function f) or
TExceptionalInvocationReturnNode(InvokeExpr e) or
TGlobalAccessPathRoot() or
TTemplatePlaceholderTag(Templating::TemplatePlaceholderTag tag)
TTemplatePlaceholderTag(Templating::TemplatePlaceholderTag tag) or
TApplyArgumentNode(MethodCallExpr ce, int i) {
ce.getMethodName() = "apply" and i in [0 .. getMaxNumFunctionParameter()]
}

cached
int getMaxNumFunctionParameter() { result = max(Function f | | f.getNumParameter()) }
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,10 @@ private module CachedSteps {
) {
calls(invk, f) and
(
exists(int i | arg = invk.(DataFlow::InvokeNode).getArgument(i) |
exists(int i |
arg = invk.(DataFlow::InvokeNode).getArgument(i) or
arg = invk.(DataFlow::Impl::ApplyReflectiveCallNodeDef).getApplyArgument(i)
|
exists(Parameter p |
f.getParameter(i) = p and
not p.isRestParameter() and
Expand Down
50 changes: 29 additions & 21 deletions javascript/ql/test/library-tests/Arrays/DataFlow.expected
Original file line number Diff line number Diff line change
@@ -1,21 +1,29 @@
| arrays.js:2:16:2:23 | "source" | arrays.js:5:8:5:14 | obj.foo |
| arrays.js:2:16:2:23 | "source" | arrays.js:11:10:11:15 | arr[i] |
| arrays.js:2:16:2:23 | "source" | arrays.js:15:27:15:27 | e |
| arrays.js:2:16:2:23 | "source" | arrays.js:16:23:16:23 | e |
| arrays.js:2:16:2:23 | "source" | arrays.js:20:8:20:16 | arr.pop() |
| arrays.js:2:16:2:23 | "source" | arrays.js:52:10:52:10 | x |
| arrays.js:2:16:2:23 | "source" | arrays.js:56:10:56:10 | x |
| arrays.js:2:16:2:23 | "source" | arrays.js:60:10:60:10 | x |
| arrays.js:2:16:2:23 | "source" | arrays.js:66:10:66:10 | x |
| arrays.js:2:16:2:23 | "source" | arrays.js:71:10:71:10 | x |
| arrays.js:2:16:2:23 | "source" | arrays.js:74:8:74:29 | arr.fin ... llback) |
| arrays.js:2:16:2:23 | "source" | arrays.js:77:8:77:35 | arrayFi ... llback) |
| arrays.js:2:16:2:23 | "source" | arrays.js:81:10:81:10 | x |
| arrays.js:18:22:18:29 | "source" | arrays.js:18:50:18:50 | e |
| arrays.js:22:15:22:22 | "source" | arrays.js:23:8:23:17 | arr2.pop() |
| arrays.js:25:15:25:22 | "source" | arrays.js:26:8:26:17 | arr3.pop() |
| arrays.js:29:21:29:28 | "source" | arrays.js:30:8:30:17 | arr4.pop() |
| arrays.js:29:21:29:28 | "source" | arrays.js:33:8:33:17 | arr5.pop() |
| arrays.js:29:21:29:28 | "source" | arrays.js:35:8:35:26 | arr5.slice(2).pop() |
| arrays.js:29:21:29:28 | "source" | arrays.js:41:8:41:17 | arr6.pop() |
| arrays.js:44:4:44:11 | "source" | arrays.js:45:10:45:18 | ary.pop() |
| arrays-init.js | arrays-init.js:40:8:40:13 | arr[1] | arrays-init.js:25:16:25:23 | "source" |
| arrays-init.js | arrays-init.js:45:8:45:13 | arr[6] | arrays-init.js:25:16:25:23 | "source" |
| arrays-init.js | arrays-init.js:51:8:51:13 | arr[1] | arrays-init.js:25:16:25:23 | "source" |
| arrays-init.js | arrays-init.js:57:8:57:13 | arr[1] | arrays-init.js:25:16:25:23 | "source" |
| arrays-init.js | arrays-init.js:61:8:61:13 | arr[5] | arrays-init.js:25:16:25:23 | "source" |
| arrays-init.js | arrays-init.js:66:10:66:15 | arr[i] | arrays-init.js:25:16:25:23 | "source" |
| arrays-init.js | arrays-init.js:78:10:78:15 | arr[i] | arrays-init.js:25:16:25:23 | "source" |
| arrays-init.js | arrays-init.js:84:10:84:13 | item | arrays-init.js:25:16:25:23 | "source" |
| arrays.js | arrays.js:5:8:5:14 | obj.foo | arrays.js:2:16:2:23 | "source" |
| arrays.js | arrays.js:11:10:11:15 | arr[i] | arrays.js:2:16:2:23 | "source" |
| arrays.js | arrays.js:15:27:15:27 | e | arrays.js:2:16:2:23 | "source" |
| arrays.js | arrays.js:16:23:16:23 | e | arrays.js:2:16:2:23 | "source" |
| arrays.js | arrays.js:18:50:18:50 | e | arrays.js:18:22:18:29 | "source" |
| arrays.js | arrays.js:20:8:20:16 | arr.pop() | arrays.js:2:16:2:23 | "source" |
| arrays.js | arrays.js:23:8:23:17 | arr2.pop() | arrays.js:22:15:22:22 | "source" |
| arrays.js | arrays.js:26:8:26:17 | arr3.pop() | arrays.js:25:15:25:22 | "source" |
| arrays.js | arrays.js:30:8:30:17 | arr4.pop() | arrays.js:29:21:29:28 | "source" |
| arrays.js | arrays.js:33:8:33:17 | arr5.pop() | arrays.js:29:21:29:28 | "source" |
| arrays.js | arrays.js:35:8:35:26 | arr5.slice(2).pop() | arrays.js:29:21:29:28 | "source" |
| arrays.js | arrays.js:41:8:41:17 | arr6.pop() | arrays.js:29:21:29:28 | "source" |
| arrays.js | arrays.js:45:10:45:18 | ary.pop() | arrays.js:44:4:44:11 | "source" |
| arrays.js | arrays.js:52:10:52:10 | x | arrays.js:2:16:2:23 | "source" |
| arrays.js | arrays.js:56:10:56:10 | x | arrays.js:2:16:2:23 | "source" |
| arrays.js | arrays.js:60:10:60:10 | x | arrays.js:2:16:2:23 | "source" |
| arrays.js | arrays.js:66:10:66:10 | x | arrays.js:2:16:2:23 | "source" |
| arrays.js | arrays.js:71:10:71:10 | x | arrays.js:2:16:2:23 | "source" |
| arrays.js | arrays.js:74:8:74:29 | arr.fin ... llback) | arrays.js:2:16:2:23 | "source" |
| arrays.js | arrays.js:77:8:77:35 | arrayFi ... llback) | arrays.js:2:16:2:23 | "source" |
| arrays.js | arrays.js:81:10:81:10 | x | arrays.js:2:16:2:23 | "source" |
6 changes: 3 additions & 3 deletions javascript/ql/test/library-tests/Arrays/DataFlow.ql
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,6 @@ class ArrayFlowConfig extends DataFlow::Configuration {
}
}

from ArrayFlowConfig config, DataFlow::Node src, DataFlow::Node snk
where config.hasFlow(src, snk)
select src, snk
from ArrayFlowConfig config, DataFlow::Node src, DataFlow::Node snk, string snk_file
where config.hasFlow(src, snk) and snk_file = snk.getAstNode().getFile().getBaseName()
select snk_file, snk, src
86 changes: 86 additions & 0 deletions javascript/ql/test/library-tests/Arrays/arrays-init.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
function sink(arg) {
if (arg !== "source")
return;

const STACK_LINE_REGEX = /(\d+):(\d+)\)?$/;
let err;

try {
throw new Error();
} catch (error) {
err = error;
}

try {
const stacks = err.stack.split('\n');
const [, line] = STACK_LINE_REGEX.exec(stacks[2]);

return console.log(`[${line}]`, arg);
} catch (err) {
return console.log(arg);
}
};

(function () {
let source = "source";

var str = "FALSE";

console.log("=== access by index (init by ctor) ===");
var arr = new Array(2);
arr[0] = str;
arr[1] = source;
arr[2] = 'b';
arr[3] = 'c';
arr[4] = 'd';
arr[5] = 'e';
arr[6] = source;

sink(arr[0]); // FALSE
sink(arr[1]); // TRUE
sink(arr[2]); // FALSE
sink(arr[3]); // FALSE
sink(arr[4]); // FALSE
sink(arr[5]); // FALSE
sink(arr[6]); // TRUE
sink(str); // FALSE

console.log("=== access by index (init by [...]) ===");
var arr = [str, source];
sink(arr[0]); // FALSE
sink(arr[1]); // TRUE
sink(str); // FALSE

console.log("=== access by index (init by [...], array.lenght > 5) ===");
var arr = [str, source, 'b', 'c', 'd', source];
sink(arr[0]); // FALSE
sink(arr[1]); // TRUE
sink(arr[2]); // FALSE
sink(arr[3]); // FALSE
sink(arr[4]); // FALSE
sink(arr[5]); // TRUE

console.log("=== access in for (init by [...]) ===");
var arr = [str, source];
for (let i = 0; i < arr.length; i++) {
sink(arr[i]); // TRUE
}

console.log("=== access in for (init by [...]) w/o source ===");
var arr = [str, 'a'];
for (let i = 0; i < arr.length; i++) {
sink(arr[i]); // FALSE
}

console.log("=== access in for (init by [...], array.lenght > 5) ===");
var arr = [str, 'a', 'b', 'c', 'd', source];
for (let i = 0; i < arr.length; i++) {
sink(arr[i]); // TRUE
}

console.log("=== access in forof (init by [...]) ===");
var arr = [str, source];
for (const item of arr) {
sink(item); // TRUE
}
}());
Loading