Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions javascript/change-notes/2021-06-18-promises.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
lgtm,codescanning
* The security queries now track flow through various `Promise` polyfills.
Affected packages are
[kew](https://npmjs.com/package/kew),
[promise](https://npmjs.com/package/promise),
[promise-polyfill](https://npmjs.com/package/promise-polyfill),
[rsvp](https://npmjs.com/package/rsvp),
[es6-promise](https://npmjs.com/package/es6-promise),
[native-promise-only](https://npmjs.com/package/native-promise-only),
[when](https://npmjs.com/package/when),
[pinkie-promise](https://npmjs.com/package/pinkie-promise),
[pinkie](https://npmjs.com/package/pinkie),
[synchronous-promise](https://npmjs.com/package/synchronous-promise),
[any-promise](https://npmjs.com/package/any-promise),
[lie](https://npmjs.com/package/lie),
[promise.allsettled](https://npmjs.com/package/promise.allsettled)
58 changes: 48 additions & 10 deletions javascript/ql/src/semmle/javascript/Promises.qll
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,43 @@ private predicate hasHandler(DataFlow::InvokeNode promise, string m, int i) {
exists(promise.getAMethodCall(m).getCallback(i))
}

/**
* Gets a reference to the `Promise` object.
* Either from the standard library, a polyfill import, or a polyfill that defines the global `Promise` variable.
*/
private DataFlow::SourceNode getAPromiseObject() {
// Standard library, or polyfills like [es6-shim](https://npmjs.org/package/es6-shim).
result = DataFlow::globalVarRef("Promise")
or
// polyfills from the [`promise`](https://npmjs.org/package/promise) library.
result =
DataFlow::moduleImport([
"promise", "promise/domains", "promise/setimmediate", "promise/lib/es6-extensions",
"promise/domains/es6-extensions", "promise/setimmediate/es6-extensions"
])
or
// polyfill from the [`promise-polyfill`](https://npmjs.org/package/promise-polyfill) library.
result = DataFlow::moduleMember(["promise-polyfill", "promise-polyfill/src/polyfill"], "default")
or
result = DataFlow::moduleImport(["promise-polyfill", "promise-polyfill/src/polyfill"])
or
result = DataFlow::moduleMember(["es6-promise", "rsvp"], "Promise")
or
result = DataFlow::moduleImport("native-promise-only")
or
result = DataFlow::moduleImport("when")
or
result = DataFlow::moduleImport("pinkie-promise")
or
result = DataFlow::moduleImport("pinkie")
or
result = DataFlow::moduleMember("synchronous-promise", "SynchronousPromise")
or
result = DataFlow::moduleImport("any-promise")
or
result = DataFlow::moduleImport("lie")
}

/**
* A call that looks like a Promise.
*
Expand All @@ -72,10 +109,11 @@ class PromiseCandidate extends DataFlow::InvokeNode {
}

/**
* A promise object created by the standard ECMAScript 2015 `Promise` constructor.
* A promise object created by the standard ECMAScript 2015 `Promise` constructor,
* or a polyfill implementing a superset of the ECMAScript 2015 `Promise` API.
*/
private class ES2015PromiseDefinition extends PromiseDefinition, DataFlow::NewNode {
ES2015PromiseDefinition() { this = DataFlow::globalVarRef("Promise").getAnInstantiation() }
private class ES2015PromiseDefinition extends PromiseDefinition, DataFlow::InvokeNode {
ES2015PromiseDefinition() { this = getAPromiseObject().getAnInvocation() }

override DataFlow::FunctionNode getExecutor() { result = getCallback(0) }
}
Expand Down Expand Up @@ -109,9 +147,7 @@ abstract class PromiseAllCreation extends PromiseCreationCall {
* A resolved promise created by the standard ECMAScript 2015 `Promise.resolve` function.
*/
class ResolvedES2015PromiseDefinition extends ResolvedPromiseDefinition {
ResolvedES2015PromiseDefinition() {
this = DataFlow::globalVarRef("Promise").getAMemberCall("resolve")
}
ResolvedES2015PromiseDefinition() { this = getAPromiseObject().getAMemberCall("resolve") }

override DataFlow::Node getValue() { result = getArgument(0) }
}
Expand All @@ -121,9 +157,11 @@ class ResolvedES2015PromiseDefinition extends ResolvedPromiseDefinition {
*/
class AggregateES2015PromiseDefinition extends PromiseCreationCall {
AggregateES2015PromiseDefinition() {
exists(string m | m = "all" or m = "race" or m = "any" |
this = DataFlow::globalVarRef("Promise").getAMemberCall(m)
exists(string m | m = "all" or m = "race" or m = "any" or m = "allSettled" |
this = getAPromiseObject().getAMemberCall(m)
)
or
this = DataFlow::moduleImport("promise.allsettled").getACall()
}

override DataFlow::Node getValue() {
Expand Down Expand Up @@ -562,14 +600,14 @@ module Bluebird {
}

/**
* Provides classes for working with the `q` library (https://github.com/kriskowal/q).
* Provides classes for working with the `q` library (https://github.com/kriskowal/q) and the compatible `kew` library (https://github.com/Medium/kew).
*/
module Q {
/**
* A promise object created by the q `Promise` constructor.
*/
private class QPromiseDefinition extends PromiseDefinition, DataFlow::CallNode {
QPromiseDefinition() { this = DataFlow::moduleMember("q", "Promise").getACall() }
QPromiseDefinition() { this = DataFlow::moduleMember(["q", "kew"], "Promise").getACall() }

override DataFlow::FunctionNode getExecutor() { result = getCallback(0) }
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,3 +79,9 @@
| promises.js:71:5:71:27 | Promise ... source) |
| promises.js:72:5:72:41 | new Pro ... ource)) |
| promises.js:79:19:79:41 | Promise ... source) |
| promises.js:88:17:90:4 | Q.Promi ... );\\n }) |
| promises.js:112:17:112:62 | new RSV ... ct) {}) |
| promises.js:124:19:124:30 | when(source) |
| promises.js:130:14:130:69 | new Pro ... s'); }) |
| promises.js:135:3:137:4 | new Pro ... );\\n }) |
| promises.js:148:10:148:49 | new Pro ... ect){}) |
71 changes: 71 additions & 0 deletions javascript/ql/test/library-tests/Promises/promises.js
Original file line number Diff line number Diff line change
Expand Up @@ -80,4 +80,75 @@
promise.then(function (val) {
var sink = val;
});
})();


(function() {
var Q = require("kew");
var promise = Q.Promise(function (resolve, reject) {
resolve(source);
});
promise.then(function (val) {
var sink = val;
});
})();

(function() {
var PromiseA = require('promise');
var PromiseB = require('promise/domains');
PromiseA.resolve(source);
PromiseB.resolve(source);
})();

(function() {
var PromiseA = require('promise-polyfill').default;
import PromiseB from 'promise-polyfill';
PromiseA.resolve(source);
PromiseB.resolve(source);
})();

(function() {
var RSVP = require('rsvp');
var promise = new RSVP.Promise(function(resolve, reject) {});
var Promise = require('es6-promise').Promise;
Promise.resolve(source);
})();

(function() {
var Promise = require('native-promise-only');
Promise.resolve(source);
})();

(function() {
const when = require('when');
const promise = when(source);
const promise2 = when.resolve(source);
})();

(function() {
var Promise = require('pinkie-promise');
var prom = new Promise(function (resolve) { resolve('unicorns'); });
})();

(function() {
var Promise = require('pinkie');
new Promise(function (resolve, reject) {
resolve(data);
});
})();

(function() {
import { SynchronousPromise } from 'synchronous-promise';
// is technically not a promise, but behaves like one.
var promise = SynchronousPromise.resolve(source);
})();

(function() {
var Promise = require('any-promise');
return new Promise(function(resolve, reject){})
})();

(function() {
var Promise = require('lie');
var promise = Promise.resolve(source);
})();
52 changes: 52 additions & 0 deletions javascript/ql/test/library-tests/Promises/tests.expected
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,15 @@ test_ResolvedPromiseDefinition
| promises.js:62:19:62:41 | Promise ... source) | promises.js:62:35:62:40 | source |
| promises.js:71:5:71:27 | Promise ... source) | promises.js:71:21:71:26 | source |
| promises.js:79:19:79:41 | Promise ... source) | promises.js:79:35:79:40 | source |
| promises.js:99:3:99:26 | Promise ... source) | promises.js:99:20:99:25 | source |
| promises.js:100:3:100:26 | Promise ... source) | promises.js:100:20:100:25 | source |
| promises.js:106:3:106:26 | Promise ... source) | promises.js:106:20:106:25 | source |
| promises.js:107:3:107:26 | Promise ... source) | promises.js:107:20:107:25 | source |
| promises.js:114:3:114:25 | Promise ... source) | promises.js:114:19:114:24 | source |
| promises.js:119:3:119:25 | Promise ... source) | promises.js:119:19:119:24 | source |
| promises.js:125:20:125:39 | when.resolve(source) | promises.js:125:33:125:38 | source |
| promises.js:143:17:143:50 | Synchro ... source) | promises.js:143:44:143:49 | source |
| promises.js:153:17:153:39 | Promise ... source) | promises.js:153:33:153:38 | source |
test_PromiseDefinition_getARejectHandler
| flow.js:26:2:26:49 | new Pro ... ource)) | flow.js:26:69:26:80 | y => sink(y) |
| flow.js:32:2:32:49 | new Pro ... ource)) | flow.js:32:57:32:68 | x => sink(x) |
Expand Down Expand Up @@ -82,6 +91,11 @@ test_PromiseDefinition_getExecutor
| promises.js:10:18:17:4 | new Pro ... );\\n }) | promises.js:10:30:17:3 | (res, r ... e);\\n } |
| promises.js:33:19:35:6 | new Pro ... \\n }) | promises.js:33:31:35:5 | functio ... ;\\n } |
| promises.js:43:19:45:6 | Q.Promi ... \\n }) | promises.js:43:29:45:5 | functio ... ;\\n } |
| promises.js:88:17:90:4 | Q.Promi ... );\\n }) | promises.js:88:27:90:3 | functio ... e);\\n } |
| promises.js:112:17:112:62 | new RSV ... ct) {}) | promises.js:112:34:112:61 | functio ... ect) {} |
| promises.js:130:14:130:69 | new Pro ... s'); }) | promises.js:130:26:130:68 | functio ... ns'); } |
| promises.js:135:3:137:4 | new Pro ... );\\n }) | promises.js:135:15:137:3 | functio ... a);\\n } |
| promises.js:148:10:148:49 | new Pro ... ect){}) | promises.js:148:22:148:48 | functio ... ject){} |
test_PromiseDefinition_getAFinallyHandler
| flow.js:105:2:105:48 | new Pro ... "BLA")) | flow.js:105:58:105:76 | x => {throw source} |
| flow.js:109:2:109:48 | new Pro ... "BLA")) | flow.js:109:58:109:70 | x => rejected |
Expand Down Expand Up @@ -117,6 +131,12 @@ test_PromiseDefinition
| promises.js:10:18:17:4 | new Pro ... );\\n }) |
| promises.js:33:19:35:6 | new Pro ... \\n }) |
| promises.js:43:19:45:6 | Q.Promi ... \\n }) |
| promises.js:88:17:90:4 | Q.Promi ... );\\n }) |
| promises.js:112:17:112:62 | new RSV ... ct) {}) |
| promises.js:124:19:124:30 | when(source) |
| promises.js:130:14:130:69 | new Pro ... s'); }) |
| promises.js:135:3:137:4 | new Pro ... );\\n }) |
| promises.js:148:10:148:49 | new Pro ... ect){}) |
test_PromiseDefinition_getAResolveHandler
| flow.js:24:2:24:49 | new Pro ... ource)) | flow.js:24:56:24:67 | x => sink(x) |
| flow.js:26:2:26:49 | new Pro ... ource)) | flow.js:26:56:26:66 | x => foo(x) |
Expand All @@ -134,6 +154,7 @@ test_PromiseDefinition_getAResolveHandler
| promises.js:10:18:17:4 | new Pro ... );\\n }) | promises.js:26:20:28:3 | (v) => ... v;\\n } |
| promises.js:33:19:35:6 | new Pro ... \\n }) | promises.js:36:18:38:5 | functio ... ;\\n } |
| promises.js:43:19:45:6 | Q.Promi ... \\n }) | promises.js:46:18:48:5 | functio ... ;\\n } |
| promises.js:88:17:90:4 | Q.Promi ... );\\n }) | promises.js:91:16:93:3 | functio ... al;\\n } |
test_PromiseDefinition_getRejectParameter
| flow.js:7:11:7:59 | new Pro ... ource)) | flow.js:7:33:7:38 | reject |
| flow.js:10:11:10:58 | new Pro ... ource)) | flow.js:10:33:10:38 | reject |
Expand Down Expand Up @@ -164,6 +185,10 @@ test_PromiseDefinition_getRejectParameter
| promises.js:10:18:17:4 | new Pro ... );\\n }) | promises.js:10:36:10:38 | rej |
| promises.js:33:19:35:6 | new Pro ... \\n }) | promises.js:33:50:33:55 | reject |
| promises.js:43:19:45:6 | Q.Promi ... \\n }) | promises.js:43:48:43:53 | reject |
| promises.js:88:17:90:4 | Q.Promi ... );\\n }) | promises.js:88:46:88:51 | reject |
| promises.js:112:17:112:62 | new RSV ... ct) {}) | promises.js:112:52:112:57 | reject |
| promises.js:135:3:137:4 | new Pro ... );\\n }) | promises.js:135:34:135:39 | reject |
| promises.js:148:10:148:49 | new Pro ... ect){}) | promises.js:148:40:148:45 | reject |
test_PromiseDefinition_getResolveParameter
| flow.js:7:11:7:59 | new Pro ... ource)) | flow.js:7:24:7:30 | resolve |
| flow.js:10:11:10:58 | new Pro ... ource)) | flow.js:10:24:10:30 | resolve |
Expand Down Expand Up @@ -194,6 +219,11 @@ test_PromiseDefinition_getResolveParameter
| promises.js:10:18:17:4 | new Pro ... );\\n }) | promises.js:10:31:10:33 | res |
| promises.js:33:19:35:6 | new Pro ... \\n }) | promises.js:33:41:33:47 | resolve |
| promises.js:43:19:45:6 | Q.Promi ... \\n }) | promises.js:43:39:43:45 | resolve |
| promises.js:88:17:90:4 | Q.Promi ... );\\n }) | promises.js:88:37:88:43 | resolve |
| promises.js:112:17:112:62 | new RSV ... ct) {}) | promises.js:112:43:112:49 | resolve |
| promises.js:130:14:130:69 | new Pro ... s'); }) | promises.js:130:36:130:42 | resolve |
| promises.js:135:3:137:4 | new Pro ... );\\n }) | promises.js:135:25:135:31 | resolve |
| promises.js:148:10:148:49 | new Pro ... ect){}) | promises.js:148:31:148:37 | resolve |
test_PromiseDefinition_getACatchHandler
| flow.js:32:2:32:49 | new Pro ... ource)) | flow.js:32:57:32:68 | x => sink(x) |
| flow.js:48:2:48:36 | new Pro ... urce }) | flow.js:48:44:48:55 | x => sink(x) |
Expand Down Expand Up @@ -400,3 +430,25 @@ typetrack
| promises.js:71:34:71:36 | val | promises.js:71:5:71:27 | Promise ... source) | load $PromiseResolveField$ |
| promises.js:72:48:72:50 | val | promises.js:72:5:72:41 | new Pro ... ource)) | load $PromiseResolveField$ |
| promises.js:75:27:75:29 | val | promises.js:75:5:75:20 | resolver.promise | load $PromiseResolveField$ |
| promises.js:88:17:90:4 | Q.Promi ... );\\n }) | promises.js:89:15:89:20 | source | copy $PromiseResolveField$ |
| promises.js:88:17:90:4 | Q.Promi ... );\\n }) | promises.js:89:15:89:20 | source | store $PromiseResolveField$ |
| promises.js:99:3:99:26 | Promise ... source) | promises.js:99:20:99:25 | source | copy $PromiseResolveField$ |
| promises.js:99:3:99:26 | Promise ... source) | promises.js:99:20:99:25 | source | store $PromiseResolveField$ |
| promises.js:100:3:100:26 | Promise ... source) | promises.js:100:20:100:25 | source | copy $PromiseResolveField$ |
| promises.js:100:3:100:26 | Promise ... source) | promises.js:100:20:100:25 | source | store $PromiseResolveField$ |
| promises.js:106:3:106:26 | Promise ... source) | promises.js:106:20:106:25 | source | copy $PromiseResolveField$ |
| promises.js:106:3:106:26 | Promise ... source) | promises.js:106:20:106:25 | source | store $PromiseResolveField$ |
| promises.js:107:3:107:26 | Promise ... source) | promises.js:107:20:107:25 | source | copy $PromiseResolveField$ |
| promises.js:107:3:107:26 | Promise ... source) | promises.js:107:20:107:25 | source | store $PromiseResolveField$ |
| promises.js:114:3:114:25 | Promise ... source) | promises.js:114:19:114:24 | source | copy $PromiseResolveField$ |
| promises.js:114:3:114:25 | Promise ... source) | promises.js:114:19:114:24 | source | store $PromiseResolveField$ |
| promises.js:119:3:119:25 | Promise ... source) | promises.js:119:19:119:24 | source | copy $PromiseResolveField$ |
| promises.js:119:3:119:25 | Promise ... source) | promises.js:119:19:119:24 | source | store $PromiseResolveField$ |
| promises.js:125:20:125:39 | when.resolve(source) | promises.js:125:33:125:38 | source | copy $PromiseResolveField$ |
| promises.js:125:20:125:39 | when.resolve(source) | promises.js:125:33:125:38 | source | store $PromiseResolveField$ |
| promises.js:135:3:137:4 | new Pro ... );\\n }) | promises.js:136:13:136:16 | data | copy $PromiseResolveField$ |
| promises.js:135:3:137:4 | new Pro ... );\\n }) | promises.js:136:13:136:16 | data | store $PromiseResolveField$ |
| promises.js:143:17:143:50 | Synchro ... source) | promises.js:143:44:143:49 | source | copy $PromiseResolveField$ |
| promises.js:143:17:143:50 | Synchro ... source) | promises.js:143:44:143:49 | source | store $PromiseResolveField$ |
| promises.js:153:17:153:39 | Promise ... source) | promises.js:153:33:153:38 | source | copy $PromiseResolveField$ |
| promises.js:153:17:153:39 | Promise ... source) | promises.js:153:33:153:38 | source | store $PromiseResolveField$ |