diff --git a/change-notes/1.19/analysis-javascript.md b/change-notes/1.19/analysis-javascript.md index bf36867b2eb3..be3a02fc34b5 100644 --- a/change-notes/1.19/analysis-javascript.md +++ b/change-notes/1.19/analysis-javascript.md @@ -8,6 +8,7 @@ * Support for popular libraries has been improved. Consequently, queries may produce more results on code bases that use the following features: - file system access, for example through [fs-extra](https://github.com/jprichardson/node-fs-extra) or [globby](https://www.npmjs.com/package/globby) + - the [Google Cloud Spanner](https://cloud.google.com/spanner) database * The type inference now handles nested imports (that is, imports not appearing at the toplevel). This may yield fewer false-positive results on projects that use this non-standard language feature. diff --git a/javascript/ql/src/semmle/javascript/frameworks/SQL.qll b/javascript/ql/src/semmle/javascript/frameworks/SQL.qll index d31df8973f10..f933b25b4776 100644 --- a/javascript/ql/src/semmle/javascript/frameworks/SQL.qll +++ b/javascript/ql/src/semmle/javascript/frameworks/SQL.qll @@ -387,3 +387,105 @@ private module Sequelize { } } } + +/** + * Provides classes modelling the Google Cloud Spanner library. + */ +private module Spanner { + /** + * Gets a node that refers to the `Spanner` class + */ + DataFlow::SourceNode spanner() { + // older versions + result = DataFlow::moduleImport("@google-cloud/spanner") + or + // newer versions + result = DataFlow::moduleMember("@google-cloud/spanner", "Spanner") + } + + /** + * Gets a node that refers to an instance of the `Database` class. + */ + DataFlow::SourceNode database() { + result = spanner().getAnInvocation().getAMethodCall("instance").getAMethodCall("database") + } + + /** + * Gets a node that refers to an instance of the `v1.SpannerClient` class. + */ + DataFlow::SourceNode v1SpannerClient() { + result = spanner().getAPropertyRead("v1").getAPropertyRead("SpannerClient").getAnInstantiation() + } + + /** + * Gets a node that refers to a transaction object. + */ + DataFlow::SourceNode transaction() { + result = database().getAMethodCall("runTransaction").getCallback(0).getParameter(1) + } + + /** + * A call to a Spanner method that executes a SQL query. + */ + abstract class SqlExecution extends DatabaseAccess, DataFlow::InvokeNode { + /** + * Gets the position of the query argument; default is zero, which can be overridden + * by subclasses. + */ + int getQueryArgumentPosition() { + result = 0 + } + + override DataFlow::Node getAQueryArgument() { + result = getArgument(getQueryArgumentPosition()) or + result = getOptionArgument(getQueryArgumentPosition(), "sql") + } + } + + /** + * A call to `Database.run`, `Database.runPartitionedUpdate` or `Database.runStream`. + */ + class DatabaseRunCall extends SqlExecution { + DatabaseRunCall() { + exists (string run | run = "run" or run = "runPartitionedUpdate" or run = "runStream" | + this = database().getAMethodCall(run) + ) + } + } + + /** + * A call to `Transaction.run`, `Transaction.runStream` or `Transaction.runUpdate`. + */ + class TransactionRunCall extends SqlExecution { + TransactionRunCall() { + exists (string run | run = "run" or run = "runStream" or run = "runUpdate" | + this = transaction().getAMethodCall(run) + ) + } + } + + /** + * A call to `v1.SpannerClient.executeSql` or `v1.SpannerClient.executeStreamingSql`. + */ + class ExecuteSqlCall extends SqlExecution { + ExecuteSqlCall() { + exists (string exec | exec = "executeSql" or exec = "executeStreamingSql" | + this = v1SpannerClient().getAMethodCall(exec) + ) + } + + override DataFlow::Node getAQueryArgument() { + // `executeSql` and `executeStreamingSql` do not accept query strings directly + result = getOptionArgument(0, "sql") + } + } + + /** + * An expression that is interpreted as a SQL string. + */ + class QueryString extends SQL::SqlString { + QueryString() { + this = any(SqlExecution se).getAQueryArgument().asExpr() + } + } +} diff --git a/javascript/ql/test/library-tests/frameworks/SQL/SqlString.expected b/javascript/ql/test/library-tests/frameworks/SQL/SqlString.expected index d4866f842e94..ace4b4fa5488 100644 --- a/javascript/ql/test/library-tests/frameworks/SQL/SqlString.expected +++ b/javascript/ql/test/library-tests/frameworks/SQL/SqlString.expected @@ -14,4 +14,24 @@ | postgres3.js:15:16:15:40 | 'SELECT ... s name' | | sequelize2.js:10:17:10:118 | 'SELECT ... Y name' | | sequelize.js:8:17:8:118 | 'SELECT ... Y name' | +| spanner2.js:5:26:5:35 | "SQL code" | +| spanner2.js:7:35:7:44 | "SQL code" | +| spanner.js:6:8:6:17 | "SQL code" | +| spanner.js:7:8:7:26 | { sql: "SQL code" } | +| spanner.js:7:15:7:24 | "SQL code" | +| spanner.js:8:25:8:34 | "SQL code" | +| spanner.js:9:25:9:43 | { sql: "SQL code" } | +| spanner.js:9:32:9:41 | "SQL code" | +| spanner.js:10:14:10:23 | "SQL code" | +| spanner.js:11:14:11:31 | { sql: "SQL code"} | +| spanner.js:11:21:11:30 | "SQL code" | +| spanner.js:14:10:14:19 | "SQL code" | +| spanner.js:15:10:15:28 | { sql: "SQL code" } | +| spanner.js:15:17:15:26 | "SQL code" | +| spanner.js:16:16:16:25 | "SQL code" | +| spanner.js:17:16:17:34 | { sql: "SQL code" } | +| spanner.js:17:23:17:32 | "SQL code" | +| spanner.js:18:16:18:25 | "SQL code" | +| spanner.js:19:16:19:34 | { sql: "SQL code" } | +| spanner.js:19:23:19:32 | "SQL code" | | sqlite.js:7:8:7:45 | "UPDATE ... id = ?" | diff --git a/javascript/ql/test/library-tests/frameworks/SQL/spanner.js b/javascript/ql/test/library-tests/frameworks/SQL/spanner.js new file mode 100644 index 000000000000..d42762230062 --- /dev/null +++ b/javascript/ql/test/library-tests/frameworks/SQL/spanner.js @@ -0,0 +1,20 @@ +const { Spanner } = require("@google-cloud/spanner"); +const spanner = new Spanner(); +const instance = spanner.instance('inst'); +const db = instance.database('db'); + +db.run("SQL code", (err, rows) => {}); +db.run({ sql: "SQL code" }, (err, rows) => {}); +db.runPartitionedUpdate("SQL code", (err, rows) => {}); +db.runPartitionedUpdate({ sql: "SQL code" }, (err, rows) => {}); +db.runStream("SQL code"); +db.runStream({ sql: "SQL code"}); + +db.runTransaction((err, tx) => { + tx.run("SQL code"); + tx.run({ sql: "SQL code" }); + tx.runStream("SQL code"); + tx.runStream({ sql: "SQL code" }); + tx.runUpdate("SQL code"); + tx.runUpdate({ sql: "SQL code" }); +}); \ No newline at end of file diff --git a/javascript/ql/test/library-tests/frameworks/SQL/spanner2.js b/javascript/ql/test/library-tests/frameworks/SQL/spanner2.js new file mode 100644 index 000000000000..9795d0f3a39e --- /dev/null +++ b/javascript/ql/test/library-tests/frameworks/SQL/spanner2.js @@ -0,0 +1,7 @@ +const spanner = require("@google-cloud/spanner"); +const client = new spanner.v1.SpannerClient({}); + +client.executeSql("not SQL code", (err, rows) => {}); +client.executeSql({ sql: "SQL code" }, (err, rows) => {}); +client.executeStreamingSql("not SQL code", (err, rows) => {}); +client.executeStreamingSql({ sql: "SQL code" }, (err, rows) => {});