diff --git a/javascript/ql/src/javascript.qll b/javascript/ql/src/javascript.qll index 9f497f83b640..d8c4396a394a 100644 --- a/javascript/ql/src/javascript.qll +++ b/javascript/ql/src/javascript.qll @@ -69,6 +69,7 @@ import semmle.javascript.frameworks.CryptoLibraries import semmle.javascript.frameworks.DigitalOcean import semmle.javascript.frameworks.Electron import semmle.javascript.frameworks.Files +import semmle.javascript.frameworks.Firebase import semmle.javascript.frameworks.jQuery import semmle.javascript.frameworks.LodashUnderscore import semmle.javascript.frameworks.Logging diff --git a/javascript/ql/src/semmle/javascript/StandardLibrary.qll b/javascript/ql/src/semmle/javascript/StandardLibrary.qll index 68ad116f7f91..6afc5e82b863 100644 --- a/javascript/ql/src/semmle/javascript/StandardLibrary.qll +++ b/javascript/ql/src/semmle/javascript/StandardLibrary.qll @@ -189,7 +189,7 @@ private class PromiseFlowStep extends DataFlow::AdditionalFlowStep { /** * Holds if taint propagates from `pred` to `succ` through promises. */ -private predicate promiseTaintStep(DataFlow::Node pred, DataFlow::Node succ) { +predicate promiseTaintStep(DataFlow::Node pred, DataFlow::Node succ) { // from `x` to `new Promise((res, rej) => res(x))` pred = succ.(PromiseDefinition).getResolveParameter().getACall().getArgument(0) or diff --git a/javascript/ql/src/semmle/javascript/frameworks/Firebase.qll b/javascript/ql/src/semmle/javascript/frameworks/Firebase.qll new file mode 100644 index 000000000000..a343be69f084 --- /dev/null +++ b/javascript/ql/src/semmle/javascript/frameworks/Firebase.qll @@ -0,0 +1,296 @@ +/** + * Provides classes and predicates for reasoning about code using the Firebase API. + */ +import javascript + +module Firebase { + /** Gets a reference to the Firebase API object. */ + private DataFlow::SourceNode firebase(DataFlow::TypeTracker t) { + t.start() and + ( + result = DataFlow::moduleImport("firebase/app") + or + result = DataFlow::moduleImport("firebase-admin") + or + result = DataFlow::globalVarRef("firebase") + ) + or + exists (DataFlow::TypeTracker t2 | + result = firebase(t2).track(t2, t) + ) + } + + /** Gets a reference to the `firebase/app` or `firebase-admin` API object. */ + DataFlow::SourceNode firebase() { + result = firebase(DataFlow::TypeTracker::end()) + } + + /** Gets a reference to a Firebase app created with `initializeApp`. */ + private DataFlow::SourceNode initApp(DataFlow::TypeTracker t) { + result = firebase().getAMethodCall("initializeApp") and t.start() + or + exists (DataFlow::TypeTracker t2 | + result = initApp(t2).track(t2, t) + ) + } + + /** + * Gets a reference to a Firebase app, either the `firebase` object or an + * app created explicitly with `initializeApp()`. + */ + DataFlow::SourceNode app() { + result = firebase(DataFlow::TypeTracker::end()) or result = initApp(DataFlow::TypeTracker::end()) + } + + module Database { + + /** Gets a reference to a Firebase database object, such as `firebase.database()`. */ + private DataFlow::SourceNode database(DataFlow::TypeTracker t) { + result = app().getAMethodCall("database") and t.start() + or + exists (DataFlow::TypeTracker t2 | + result = database(t2).track(t2, t) + ) + } + + /** Gets a reference to a Firebase database object, such as `firebase.database()`. */ + DataFlow::SourceNode database() { + result = database(DataFlow::TypeTracker::end()) + } + + /** Gets a node that refers to a `Reference` object, such as `firebase.database().ref()`. */ + private DataFlow::SourceNode ref(DataFlow::TypeTracker t) { + t.start() and + ( + exists (string name | result = database().getAMethodCall(name) | + name = "ref" or + name = "refFromURL" + ) + or + exists (string name | result = ref().getAMethodCall(name) | + name = "push" or + name = "child" + ) + or + exists (string name | result = ref().getAPropertyRead(name) | + name = "parent" or + name = "root" + ) + or + result = snapshot().getAPropertyRead("ref") + ) + or + exists (DataFlow::TypeTracker t2 | + result = ref(t2).track(t2, t) + ) + } + + /** Gets a node that refers to a `Reference` object, such as `firebase.database().ref()`. */ + DataFlow::SourceNode ref() { + result = ref(DataFlow::TypeTracker::end()) + } + + /** Gets a node that refers to a `Query` or `Reference` object. */ + private DataFlow::SourceNode query(DataFlow::TypeTracker t) { + t.start() and + ( + result = ref(t) // a Reference can be used as a Query + or + exists (string name | result = query().getAMethodCall(name) | + name = "endAt" or + name = "limitTo" + any(string s) or + name = "orderBy" + any(string s) or + name = "startAt" + ) + ) + or + exists (DataFlow::TypeTracker t2 | + result = query(t2).track(t2, t) + ) + } + + /** Gets a node that refers to a `Query` or `Reference` object. */ + DataFlow::SourceNode query() { + result = query(DataFlow::TypeTracker::end()) + } + + /** + * A call of form `query.on(...)` or `query.once(...)`. + */ + class QueryListenCall extends DataFlow::MethodCallNode { + QueryListenCall() { + this = query().getAMethodCall() and + (getMethodName() = "on" or getMethodName() = "once") + } + + /** + * Gets the argument in which the callback is passed. + */ + DataFlow::Node getCallbackNode() { + result = getArgument(1) + } + } + + /** + * Gets a node that is passed as the callback to a `Reference.transaction` call. + */ + private DataFlow::SourceNode transactionCallback(DataFlow::TypeBackTracker t) { + t.start() and + result = ref().getAMethodCall("transaction").getArgument(0).getALocalSource() + or + exists (DataFlow::TypeBackTracker t2 | + result = transactionCallback(t2).backtrack(t2, t) + ) + } + + /** + * Gets a node that is passed as the callback to a `Reference.transaction` call. + */ + DataFlow::SourceNode transactionCallback() { + result = transactionCallback(DataFlow::TypeBackTracker::end()) + } + } + + /** + * Provides predicates for reasoning about the the Firebase Cloud Functions API, + * sometimes referred to just as just "Firebase Functions". + */ + module CloudFunctions { + /** Gets a reference to the Cloud Functions namespace. */ + private DataFlow::SourceNode namespace(DataFlow::TypeTracker t) { + t.start() and + result = DataFlow::moduleImport("firebase-functions") + or + exists (DataFlow::TypeTracker t2 | + result = namespace(t2).track(t2, t) + ) + } + + /** Gets a reference to the Cloud Functions namespace. */ + DataFlow::SourceNode namespace() { + result = namespace(DataFlow::TypeTracker::end()) + } + + /** Gets a reference to a Cloud Functions database object. */ + private DataFlow::SourceNode database(DataFlow::TypeTracker t) { + t.start() and + result = namespace().getAPropertyRead("database") + or + exists (DataFlow::TypeTracker t2 | + result = database(t2).track(t2, t) + ) + } + + /** Gets a reference to a Cloud Functions database object. */ + DataFlow::SourceNode database() { + result = database(DataFlow::TypeTracker::end()) + } + + /** Gets a data flow node holding a `RefBuilder` object. */ + private DataFlow::SourceNode refBuilder(DataFlow::TypeTracker t) { + t.start() and + result = database().getAMethodCall("ref") + or + exists (DataFlow::TypeTracker t2 | + result = refBuilder(t2).track(t2, t) + ) + } + + /** Gets a data flow node holding a `RefBuilder` object. */ + DataFlow::SourceNode ref() { + result = refBuilder(DataFlow::TypeTracker::end()) + } + + /** Gets a call that registers a listener on a `RefBuilder`, such as `ref.onCreate(...)`. */ + class RefBuilderListenCall extends DataFlow::MethodCallNode { + RefBuilderListenCall() { + this = ref().getAMethodCall() and + getMethodName() = "on" + any(string s) + } + + /** + * Gets the data flow node holding the listener callback. + */ + DataFlow::Node getCallbackNode() { + result = getArgument(0) + } + } + } + + /** + * Gets a value that will be invoked with a `DataSnapshot` value as its first parameter. + */ + private DataFlow::SourceNode snapshotCallback(DataFlow::TypeBackTracker t) { + t.start() and + ( + result = any(Database::QueryListenCall call).getCallbackNode().getALocalSource() + or + result = any(CloudFunctions::RefBuilderListenCall call).getCallbackNode().getALocalSource() + ) + or + exists (DataFlow::TypeBackTracker t2 | + result = snapshotCallback(t2).backtrack(t2, t) + ) + } + + /** + * Gets a value that will be invoked with a `DataSnapshot` value as its first parameter. + */ + DataFlow::SourceNode snapshotCallback() { + result = snapshotCallback(DataFlow::TypeBackTracker::end()) + } + + /** + * Gets a node that refers to a `DataSnapshot` value or a promise or `Change` + * object containing `DataSnapshot`s. + */ + private DataFlow::SourceNode snapshot(DataFlow::TypeTracker t) { + t.start() and + ( + result = snapshotCallback().(DataFlow::FunctionNode).getParameter(0) + or + result instanceof Database::QueryListenCall // returns promise + or + result = snapshot().getAMethodCall("child") + or + result = snapshot().getAMethodCall("forEach").getCallback(0).getParameter(0) + or + exists (string prop | result = snapshot().getAPropertyRead(prop) | + prop = "before" or // only defined on Change objects + prop = "after" + ) + ) + or + promiseTaintStep(snapshot(t), result) + or + exists (DataFlow::TypeTracker t2 | + result = snapshot(t2).track(t2, t) + ) + } + + /** + * Gets a node that refers to a `DataSnapshot` value, such as `x` in + * `firebase.database().ref().on('value', x => {...})`. + */ + DataFlow::SourceNode snapshot() { + result = snapshot(DataFlow::TypeTracker::end()) + } + + /** + * A reference to a value obtained from a Firebase database. + */ + class FirebaseVal extends RemoteFlowSource { + FirebaseVal() { + exists (string name | this = snapshot().getAMethodCall(name) | + name = "val" or + name = "exportVal" + ) + or + this = Database::transactionCallback().(DataFlow::FunctionNode).getParameter(0) + } + + override string getSourceType() { + result = "Firebase database" + } + } +} diff --git a/javascript/ql/test/library-tests/frameworks/Firebase/FirebaseRef.expected b/javascript/ql/test/library-tests/frameworks/Firebase/FirebaseRef.expected new file mode 100644 index 000000000000..2bcf33333c49 --- /dev/null +++ b/javascript/ql/test/library-tests/frameworks/Firebase/FirebaseRef.expected @@ -0,0 +1,17 @@ +| tst.js:5:1:5:22 | fb.data ... ef('x') | +| tst.js:7:3:7:7 | x.ref | +| tst.js:7:3:7:14 | x.ref.parent | +| tst.js:10:1:10:25 | admin.d ... ef('x') | +| tst.js:12:3:12:7 | x.ref | +| tst.js:12:3:12:14 | x.ref.parent | +| tst.js:17:3:17:7 | x.ref | +| tst.js:17:3:17:14 | x.ref.parent | +| tst.js:23:3:23:7 | x.ref | +| tst.js:23:3:23:14 | x.ref.parent | +| tst.js:32:12:32:42 | this.fi ... .ref(x) | +| tst.js:46:12:46:42 | this.fi ... .ref(x) | +| tst.js:50:12:50:25 | this.getRef(x) | +| tst.js:50:12:50:34 | this.ge ... hild(x) | +| tst.js:54:5:54:37 | this.fi ... ef('x') | +| tst.js:58:1:58:61 | new Fir ... /news') | +| tst.js:59:1:59:38 | new Fir ... /news') | diff --git a/javascript/ql/test/library-tests/frameworks/Firebase/FirebaseRef.ql b/javascript/ql/test/library-tests/frameworks/Firebase/FirebaseRef.ql new file mode 100644 index 000000000000..49e3890869eb --- /dev/null +++ b/javascript/ql/test/library-tests/frameworks/Firebase/FirebaseRef.ql @@ -0,0 +1,3 @@ +import javascript + +select Firebase::Database::ref() diff --git a/javascript/ql/test/library-tests/frameworks/Firebase/FirebaseSnapshot.expected b/javascript/ql/test/library-tests/frameworks/Firebase/FirebaseSnapshot.expected new file mode 100644 index 000000000000..a2d57b9f5ef8 --- /dev/null +++ b/javascript/ql/test/library-tests/frameworks/Firebase/FirebaseSnapshot.expected @@ -0,0 +1,10 @@ +| tst.js:5:1:8:2 | fb.data ... ent;\\n}) | +| tst.js:5:38:5:38 | x | +| tst.js:10:1:13:2 | admin.d ... ent;\\n}) | +| tst.js:10:41:10:41 | x | +| tst.js:15:38:15:38 | x | +| tst.js:20:38:20:38 | x | +| tst.js:21:3:21:10 | x.before | +| tst.js:22:3:22:9 | x.after | +| tst.js:50:12:50:48 | this.ge ... value') | +| tst.js:60:1:60:39 | new Fir ... em('x') | diff --git a/javascript/ql/test/library-tests/frameworks/Firebase/FirebaseSnapshot.ql b/javascript/ql/test/library-tests/frameworks/Firebase/FirebaseSnapshot.ql new file mode 100644 index 000000000000..6b20bd3944e4 --- /dev/null +++ b/javascript/ql/test/library-tests/frameworks/Firebase/FirebaseSnapshot.ql @@ -0,0 +1,3 @@ +import javascript + +select Firebase::snapshot() diff --git a/javascript/ql/test/library-tests/frameworks/Firebase/FirebaseVal.expected b/javascript/ql/test/library-tests/frameworks/Firebase/FirebaseVal.expected new file mode 100644 index 000000000000..1d22bbfed8b6 --- /dev/null +++ b/javascript/ql/test/library-tests/frameworks/Firebase/FirebaseVal.expected @@ -0,0 +1,6 @@ +| tst.js:6:3:6:9 | x.val() | +| tst.js:11:3:11:9 | x.val() | +| tst.js:16:3:16:9 | x.val() | +| tst.js:21:3:21:16 | x.before.val() | +| tst.js:22:3:22:15 | x.after.val() | +| tst.js:61:36:61:36 | x | diff --git a/javascript/ql/test/library-tests/frameworks/Firebase/FirebaseVal.ql b/javascript/ql/test/library-tests/frameworks/Firebase/FirebaseVal.ql new file mode 100644 index 000000000000..cf541fd3ca0c --- /dev/null +++ b/javascript/ql/test/library-tests/frameworks/Firebase/FirebaseVal.ql @@ -0,0 +1,4 @@ +import javascript + +from Firebase::FirebaseVal val +select val diff --git a/javascript/ql/test/library-tests/frameworks/Firebase/tst.js b/javascript/ql/test/library-tests/frameworks/Firebase/tst.js new file mode 100644 index 000000000000..b0b7d025b539 --- /dev/null +++ b/javascript/ql/test/library-tests/frameworks/Firebase/tst.js @@ -0,0 +1,70 @@ +import * as fb from 'firebase/app'; +import * as admin from 'firebase-admin'; +import * as functions from 'firebase-functions'; + +fb.database().ref('x').once('value', x => { + x.val(); + x.ref.parent; +}); + +admin.database().ref('x').once('value', x => { + x.val(); + x.ref.parent; +}); + +functions.database.ref('x').onCreate(x => { + x.val(); + x.ref.parent; +}); + +functions.database.ref('x').onUpdate(x => { + x.before.val(); + x.after.val(); + x.ref.parent; +}); + +class FirebaseWrapper { + constructor(firebase) { + this.firebase = firebase; + } + + getRef(x) { + return this.firebase.database().ref(x); + } +} + +class FirebaseWrapper2 { + constructor() { + this.init(); + } + + init() { + this.firebase = fb.initializeApp(); + } + + getRef(x) { + return this.firebase.database().ref(x); + } + + getNewsItem(x) { + return this.getRef(x).child(x).once('value'); + } + + adjustValue(fn) { + this.firebase.database().ref('x').transaction(fn); + } +} + +new FirebaseWrapper(firebase.initializeApp()).getRef('/news'); +new FirebaseWrapper2().getRef('/news'); +new FirebaseWrapper2().getNewsItem('x'); +new FirebaseWrapper2().adjustValue(x => x + 1); + +class Box { + constructor(x) { + this.x = x; + } +} +let box1 = new Box(fb.database()); +let box2 = new Box(whatever()); +box2.x.ref(); // not a firebase ref