JS: introduce CapturedSource

This commit is contained in:
Esben Sparre Andreasen
2019-02-12 15:57:52 +01:00
parent bfbf686d7b
commit 0cf2eaec5e
12 changed files with 312 additions and 0 deletions

View File

@@ -0,0 +1,74 @@
/**
* Provides classes for the nodes that the dataflow library can reason about soundly.
*/
import javascript
/**
* Holds if the dataflow library can not track flow through `escape` due to `cause`.
*/
private predicate isEscape(DataFlow::Node escape, string cause) {
escape = any(DataFlow::InvokeNode invk).getAnArgument() and cause = "argument"
or
escape = any(DataFlow::FunctionNode fun).getAReturn() and cause = "return"
or
escape = any(ThrowStmt t).getExpr().flow() and cause = "throw"
or
escape = any(DataFlow::GlobalVariable v).getAnAssignedExpr().flow() and cause = "global"
or
escape = any(DataFlow::PropWrite write).getRhs() and cause = "heap"
or
escape = any(ExportDeclaration e).getSourceNode(_) and cause = "export"
or
any(WithStmt with).mayAffect(escape.asExpr()) and cause = "heap"
}
private DataFlow::Node getAnEscape() {
isEscape(result, _)
}
/**
* Holds if `n` can flow to a `this`-variable.
*/
private predicate exposedAsReceiver(DataFlow::SourceNode n) {
// pragmatic limitation: guarantee for object literals only
not n instanceof DataFlow::ObjectLiteralNode
or
exists(AbstractValue v | n.getAPropertyWrite().getRhs().analyze().getALocalValue() = v |
v.isIndefinite(_) or
exists(ThisExpr dis | dis.getBinder() = v.(AbstractCallable).getFunction())
)
or
n.flowsToExpr(any(FunctionBindExpr bind).getObject())
or
// technically, the builtin prototypes could have a `this`-using function through which this node escapes, but we ignore that here
// (we also ignore `o['__' + 'proto__"] = ...`)
exists(n.getAPropertyWrite("__proto__"))
or
// could check the assigned value of all affected variables, but it is unlikely to matter in practice
exists(WithStmt with | n.flowsToExpr(with.getExpr()))
}
/**
* A source for which the flow is entirely captured by the dataflow library.
* All uses of the node is represented by `this.flowsTo(_)` and friends.
*/
class CapturedSource extends DataFlow::SourceNode {
CapturedSource() {
// pragmatic limitation: object literals only
this instanceof DataFlow::ObjectLiteralNode and
not flowsTo(getAnEscape()) and
not exposedAsReceiver(this)
}
predicate hasOwnProperty(string name) {
// the property is defined in the initializer,
any(DataFlow::PropWrite write).writes(this, name, _) and
// and it is never deleted
not exists(DeleteExpr del, DataFlow::PropRef ref |
del.getOperand().flow() = ref and
flowsTo(ref.getBase()) and
(ref.getPropertyName() = name or not exists(ref.getPropertyName()))
)
}
}

View File

@@ -0,0 +1,25 @@
| method-calls.js:2:11:7:2 | {\\n\\t\\tm1: ... }; }\\n\\t} |
| method-calls.js:11:23:11:24 | {} |
| method-calls.js:16:11:16:12 | {} |
| method-calls.js:23:11:23:17 | {m: f1} |
| method-calls.js:27:11:27:17 | {m: f2} |
| method-calls.js:32:12:32:18 | {m: f3} |
| method-calls.js:36:15:36:21 | {m: f2} |
| method-calls.js:42:16:42:28 | {m: () => 42} |
| method-calls.js:46:17:46:29 | {m: () => 42} |
| method-calls.js:50:16:50:28 | {m: () => 42} |
| method-calls.js:53:16:53:28 | {m: () => 42} |
| tst.js:3:18:3:19 | {} |
| tst.js:4:18:4:36 | { f: function(){} } |
| tst.js:5:18:5:34 | { [unknown]: 42 } |
| tst.js:38:18:38:26 | { p: 42 } |
| tst.js:42:18:42:33 | { p: 42, q: 42 } |
| tst.js:52:23:52:24 | {} |
| tst.js:56:20:56:35 | { p: 42, p: 42 } |
| tst.js:59:20:59:28 | { p: 42 } |
| tst.js:63:20:63:28 | { p: 42 } |
| tst.js:68:19:68:27 | { p: 42 } |
| tst.js:73:18:73:20 | { } |
| tst.js:76:18:76:26 | { p: 42 } |
| tst.js:77:18:77:28 | { p: true } |
| tst.js:82:20:82:21 | {} |

View File

@@ -0,0 +1,4 @@
import javascript
import semmle.javascript.dataflow.CapturedNodes
select any(CapturedSource n)

View File

@@ -0,0 +1,22 @@
| method-calls.js:2:11:7:2 | {\\n\\t\\tm1: ... }; }\\n\\t} | m1 |
| method-calls.js:2:11:7:2 | {\\n\\t\\tm1: ... }; }\\n\\t} | m2 |
| method-calls.js:2:11:7:2 | {\\n\\t\\tm1: ... }; }\\n\\t} | m3 |
| method-calls.js:2:11:7:2 | {\\n\\t\\tm1: ... }; }\\n\\t} | m4 |
| method-calls.js:23:11:23:17 | {m: f1} | m |
| method-calls.js:27:11:27:17 | {m: f2} | m |
| method-calls.js:32:12:32:18 | {m: f3} | m |
| method-calls.js:36:15:36:21 | {m: f2} | m |
| method-calls.js:42:16:42:28 | {m: () => 42} | m |
| method-calls.js:46:17:46:29 | {m: () => 42} | m |
| method-calls.js:50:16:50:28 | {m: () => 42} | m |
| method-calls.js:53:16:53:28 | {m: () => 42} | m |
| tst.js:4:18:4:36 | { f: function(){} } | f |
| tst.js:38:18:38:26 | { p: 42 } | p |
| tst.js:42:18:42:33 | { p: 42, q: 42 } | p |
| tst.js:42:18:42:33 | { p: 42, q: 42 } | q |
| tst.js:56:20:56:35 | { p: 42, p: 42 } | p |
| tst.js:59:20:59:28 | { p: 42 } | p |
| tst.js:63:20:63:28 | { p: 42 } | p |
| tst.js:68:19:68:27 | { p: 42 } | p |
| tst.js:76:18:76:26 | { p: 42 } | p |
| tst.js:77:18:77:28 | { p: true } | p |

View File

@@ -0,0 +1,6 @@
import javascript
import semmle.javascript.dataflow.CapturedNodes
from CapturedSource src, string name
where src.hasOwnProperty(name)
select src, name

View File

@@ -0,0 +1,12 @@
| method-calls.js:8:2:8:8 | o1.m1() | boolean, class, date, function, null, number, object, regular expression,string or undefined |
| method-calls.js:9:2:9:8 | o1.m2() | boolean, class, date, function, null, number, object, regular expression,string or undefined |
| method-calls.js:12:2:12:7 | o.m3() | boolean, class, date, function, null, number, object, regular expression,string or undefined |
| method-calls.js:19:2:19:7 | o2.m() | boolean, class, date, function, null, number, object, regular expression,string or undefined |
| method-calls.js:23:11:23:21 | {m: f1}.m() | boolean, class, date, function, null, number, object, regular expression,string or undefined |
| method-calls.js:28:11:28:16 | o2.m() | boolean, class, date, function, null, number, object, regular expression,string or undefined |
| method-calls.js:32:11:32:23 | ({m: f3}).m() | boolean, class, date, function, null, number, object, regular expression,string or undefined |
| method-calls.js:37:11:37:16 | o4.m() | boolean, class, date, function, null, number, object, regular expression,string or undefined |
| method-calls.js:43:12:43:16 | o.m() | boolean, class, date, function, null, number, object, regular expression,string or undefined |
| method-calls.js:47:12:47:16 | o.m() | boolean, class, date, function, null, number, object, regular expression,string or undefined |
| method-calls.js:51:12:51:16 | o.m() | boolean, class, date, function, null, number, object, regular expression,string or undefined |
| method-calls.js:54:12:54:16 | o.m() | boolean, class, date, function, null, number, object, regular expression,string or undefined |

View File

@@ -0,0 +1,4 @@
import javascript
from DataFlow::MethodCallNode call
select call, call.analyze().ppTypes()

View File

@@ -0,0 +1,4 @@
| method-calls.js:23:11:23:21 | {m: f1}.m() | method-calls.js:24:2:24:3 | v1 | file://:0:0:0:0 | indefinite value (call) |
| method-calls.js:28:11:28:16 | o2.m() | method-calls.js:29:2:29:3 | v2 | file://:0:0:0:0 | indefinite value (call) |
| method-calls.js:32:11:32:23 | ({m: f3}).m() | method-calls.js:33:2:33:3 | v3 | file://:0:0:0:0 | indefinite value (call) |
| method-calls.js:37:11:37:16 | o4.m() | method-calls.js:38:2:38:3 | v4 | file://:0:0:0:0 | indefinite value (call) |

View File

@@ -0,0 +1,5 @@
import javascript
from DataFlow::MethodCallNode call, DataFlow::Node use
where call.flowsTo(use) and use != call and not exists(use.getASuccessor())
select call, use, use.analyze().getAValue()

View File

@@ -0,0 +1,57 @@
(function() {
var o1 = {
m1: function(){ return {}; },
m2: function(){ return {}; },
m3: function(){ return {}; },
m4: function(){ return {}; }
};
o1.m1();
o1.m2();
unknown(o1.m2);
var o = unknown? o1: {};
o.m3(); // NOT supported
var m4 = o.m4;
m4();
var o2 = {};
o2.m = function() { return {}; };
o2[unknown] = function() { return true; }; // could be __proto__
o2.m();
});
(function(){
function f1(){return {};}
var v1 = {m: f1}.m();
v1 === true;
function f2(){return {};}
var o2 = {m: f2};
var v2 = o2.m();
v2 === true;
function f3(){return {};}
var v3 = ({m: f3}).m();
v3 === true;
function f4(){return {};}
var { o4 } = {m: f2};
var v4 = o4.m();
v4 === true;
});
(function(){
(function(o = {m: () => 42}){
var v1 = o.m();
})(unknown);
function f(o = {m: () => 42}){
var v2 = o.m();
};
f(unknown);
(function(o = {m: () => 42}){
var v3 = o.m();
})({m: unknown});
(function(o = {m: () => 42}){
var v4 = o.m();
})({m: () => true});
});

View File

@@ -0,0 +1,93 @@
(function capturedSource(){
let captured1 = {};
let captured2 = { f: function(){} };
let captured3 = { [unknown]: 42 };
unknown({});
function known(){}
known({});
function known_escaping(e){unknown(e)}
known_escaping({});
(function(){return {}});
(function(){throw {}});
global = {};
this.p = {};
let local_in_with;
with (unknown) {
local_in_with = {};
}
with({}){}
({ m: function(){ this; } });
({ m: unknown });
let indirectlyUnknown = unknown? unknown: function(){};
({ m: indirectlyUnknown });
});
(function capturedProperty(){
let captured1 = { p: 42 };
captured1.p;
captured1.p;
let captured2 = { p: 42, q: 42 };
captured2.p;
captured2.p;
captured2.q = 42;
captured2 = 42;
let nonObject = function(){}
nonObject.p = 42;
nonObject.p;
let nonInitializer = {};
nonInitializer.p = 42;
nonInitializer.p;
let overridden1 = { p: 42, p: 42 };
overridden1.p;
let overridden2 = { p: 42 };
overridden2.p = 42;
overridden2.p;
let overridden3 = { p: 42 };
overridden3[x] = 42;
overridden3.p;
function f(o) {
let captured3 = { p: 42 };
o = o || captured3;
o.p;
}
let captured4 = { };
captured4.p;
let captured5 = { p: 42 },
captured6 = { p: true };
(unknown? captured5: captured6).p; // could support this with a bit of extra work
(function(semiCaptured7){
if(unknown)
semiCaptured7 = {};
semiCaptured7.p = 42;
});
});
(function (){
let bound = {};
bound::unknown();
});
// semmle-extractor-options: --experimental

View File

@@ -0,0 +1,6 @@
class C {
constructor(
private readonly F: { timeout: number } = { timeout: 1500 }
) {
}
}