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
1 change: 1 addition & 0 deletions change-notes/1.19/analysis-javascript.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
102 changes: 102 additions & 0 deletions javascript/ql/src/semmle/javascript/frameworks/SQL.qll
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not actually overridden anywhere and the surrounding module is private. It looks like you opted for overriding getAQueryArgument instead.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this is a tiny bit of speculative generality.

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()
}
}
}
20 changes: 20 additions & 0 deletions javascript/ql/test/library-tests/frameworks/SQL/SqlString.expected
Original file line number Diff line number Diff line change
Expand Up @@ -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 = ?" |
20 changes: 20 additions & 0 deletions javascript/ql/test/library-tests/frameworks/SQL/spanner.js
Original file line number Diff line number Diff line change
@@ -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" });
});
7 changes: 7 additions & 0 deletions javascript/ql/test/library-tests/frameworks/SQL/spanner2.js
Original file line number Diff line number Diff line change
@@ -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) => {});