Skip to content

Commit 80df398

Browse files
committed
JS: Initial support for models as data
1 parent 9ffa236 commit 80df398

File tree

9 files changed

+1073
-160
lines changed

9 files changed

+1073
-160
lines changed

javascript/ql/lib/javascript.qll

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ import semmle.javascript.frameworks.CookieLibraries
8484
import semmle.javascript.frameworks.Credentials
8585
import semmle.javascript.frameworks.CryptoLibraries
8686
import semmle.javascript.frameworks.D3
87+
import semmle.javascript.frameworks.data.ModelsAsData
8788
import semmle.javascript.frameworks.DateFunctions
8889
import semmle.javascript.frameworks.DigitalOcean
8990
import semmle.javascript.frameworks.Electron

javascript/ql/lib/semmle/javascript/frameworks/Credentials.qll

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,13 @@ abstract class CredentialsExpr extends Expr {
1616
*/
1717
abstract string getCredentialsKind();
1818
}
19+
20+
private class CredentialsFromModel extends CredentialsExpr {
21+
string kind;
22+
23+
CredentialsFromModel() {
24+
this = ModelOutput::getASinkNode("credentials[" + kind + "]").getARhs().asExpr()
25+
}
26+
27+
override string getCredentialsKind() { result = kind }
28+
}

javascript/ql/lib/semmle/javascript/frameworks/SQL.qll

Lines changed: 53 additions & 160 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ module SQL {
88
/** A string-valued expression that is interpreted as a SQL command. */
99
abstract class SqlString extends Expr { }
1010

11+
private class SqlStringFromModel extends SqlString {
12+
SqlStringFromModel() { this = ModelOutput::getASinkNode("sql-injection").getARhs().asExpr() }
13+
}
14+
1115
/**
1216
* An expression that sanitizes a string to make it safe to embed into
1317
* a SQL command.
@@ -474,176 +478,65 @@ private module MsSql {
474478
* Provides classes modelling the `sequelize` package.
475479
*/
476480
private module Sequelize {
477-
/** Gets an import of the `sequelize` module or one that re-exports it. */
478-
API::Node sequelize() { result = API::moduleImport(["sequelize", "sequelize-typescript"]) }
479-
480-
/** Gets an expression that creates an instance of the `Sequelize` class. */
481-
API::Node instance() {
482-
result = [sequelize(), sequelize().getMember("Sequelize")].getInstance()
483-
or
484-
result = API::Node::ofType(["sequelize", "sequelize-typescript"], ["Sequelize", "default"])
485-
}
486-
487-
/** A call to `Sequelize.query`. */
488-
private class QueryCall extends DatabaseAccess, DataFlow::MethodCallNode {
489-
QueryCall() { this = instance().getMember("query").getACall() }
490-
491-
override DataFlow::Node getAQueryArgument() {
492-
result = this.getArgument(0)
493-
or
494-
result = this.getOptionArgument(0, "query")
481+
class SequelizeModel extends ModelInput::TypeModelCsv {
482+
override predicate row(string row) {
483+
// package1;type1;package2;type2;path
484+
row =
485+
[
486+
"sequelize;;sequelize-typescript;;", //
487+
"sequelize;Sequelize;sequelize;default;", //
488+
"sequelize;Sequelize;sequelize;;Instance",
489+
"sequelize;Sequelize;sequelize;;Member[Sequelize].Instance",
490+
]
495491
}
496492
}
497493

498-
/** An expression that is passed to `Sequelize.query` method and hence interpreted as SQL. */
499-
class QueryString extends SQL::SqlString {
500-
QueryString() {
501-
this = any(QueryCall qc).getAQueryArgument().asExpr()
502-
or
503-
this = sequelize().getMember(["literal", "asIs"]).getParameter(0).getARhs().asExpr()
494+
class SequelizeSink extends ModelInput::SinkModelCsv {
495+
override predicate row(string row) {
496+
row =
497+
[
498+
"sequelize;Sequelize;Member[query].Argument[0];sql-injection",
499+
"sequelize;Sequelize;Member[query].Argument[0].Member[query];sql-injection",
500+
"sequelize;;Member[literal,asIs].Argument[0];sql-injection",
501+
"sequelize;;Argument[1];credentials[user name]",
502+
"sequelize;;Argument[2];credentials[password]",
503+
"sequelize;;Argument[0..].Member[username];credentials[user name]",
504+
"sequelize;;Argument[0..].Member[password];credentials[password]"
505+
]
504506
}
505507
}
506-
507-
/**
508-
* An expression that is passed as user name or password when creating an instance of the
509-
* `Sequelize` class.
510-
*/
511-
class Credentials extends CredentialsExpr {
512-
string kind;
513-
514-
Credentials() {
515-
exists(NewExpr ne, string prop |
516-
ne = sequelize().getAnInstantiation().asExpr() and
517-
(
518-
this = ne.getArgument(1) and prop = "username"
519-
or
520-
this = ne.getArgument(2) and prop = "password"
521-
or
522-
ne.hasOptionArgument(ne.getNumArgument() - 1, prop, this)
523-
) and
524-
(
525-
prop = "username" and kind = "user name"
526-
or
527-
prop = "password" and kind = prop
528-
)
529-
)
530-
}
531-
532-
override string getCredentialsKind() { result = kind }
533-
}
534508
}
535509

536-
/**
537-
* Provides classes modelling the Google Cloud Spanner library.
538-
*/
539-
private module Spanner {
540-
/**
541-
* Gets a node that refers to the `Spanner` class
542-
*/
543-
API::Node spanner() {
544-
// older versions
545-
result = API::moduleImport("@google-cloud/spanner")
546-
or
547-
// newer versions
548-
result = API::moduleImport("@google-cloud/spanner").getMember("Spanner")
549-
}
550-
551-
/**
552-
* Gets a node that refers to an instance of the `Database` class.
553-
*/
554-
API::Node database() {
555-
result =
556-
spanner().getReturn().getMember("instance").getReturn().getMember("database").getReturn()
557-
or
558-
result = API::Node::ofType("@google-cloud/spanner", "Database")
559-
}
560-
561-
/**
562-
* Gets a node that refers to an instance of the `v1.SpannerClient` class.
563-
*/
564-
API::Node v1SpannerClient() {
565-
result = spanner().getMember("v1").getMember("SpannerClient").getInstance()
566-
or
567-
result = API::Node::ofType("@google-cloud/spanner", "v1.SpannerClient")
568-
}
569-
570-
/**
571-
* Gets a node that refers to a transaction object.
572-
*/
573-
API::Node transaction() {
574-
result =
575-
database()
576-
.getMember(["runTransaction", "runTransactionAsync"])
577-
.getParameter([0, 1])
578-
.getParameter(1)
579-
or
580-
result = API::Node::ofType("@google-cloud/spanner", "Transaction")
581-
}
582-
583-
/** Gets an API node referring to a `BatchTransaction` object. */
584-
API::Node batchTransaction() {
585-
result = database().getMember("batchTransaction").getReturn()
586-
or
587-
result = database().getMember("createBatchTransaction").getReturn().getPromised()
588-
or
589-
result = API::Node::ofType("@google-cloud/spanner", "BatchTransaction")
590-
}
591-
592-
/**
593-
* A call to a Spanner method that executes a SQL query.
594-
*/
595-
abstract class SqlExecution extends DatabaseAccess, DataFlow::InvokeNode { }
596-
597-
/**
598-
* A SQL execution that takes the input directly in the first argument or in the `sql` option.
599-
*/
600-
class SqlExecutionDirect extends SqlExecution {
601-
SqlExecutionDirect() {
602-
this = database().getMember(["run", "runPartitionedUpdate", "runStream"]).getACall()
603-
or
604-
this = transaction().getMember(["run", "runStream", "runUpdate"]).getACall()
605-
or
606-
this = batchTransaction().getMember("createQueryPartitions").getACall()
607-
}
608-
609-
override DataFlow::Node getAQueryArgument() {
610-
result = this.getArgument(0)
611-
or
612-
result = this.getOptionArgument(0, "sql")
510+
private module SpannerCsv {
511+
class SpannerTypes extends ModelInput::TypeModelCsv {
512+
override predicate row(string row) {
513+
// package1; type1; package2; type2; path
514+
row =
515+
[
516+
"@google-cloud/spanner;;@google-cloud/spanner;;Member[Spanner]",
517+
"@google-cloud/spanner;Database;@google-cloud/spanner;;ReturnValue.Member[instance].ReturnValue.Member[database].ReturnValue",
518+
"@google-cloud/spanner;v1.SpannerClient;@google-cloud/spanner;;Member[v1].Member[SpannerClient].Instance",
519+
"@google-cloud/spanner;Transaction;@google-cloud/spanner;Database;Member[runTransaction,runTransactionAsync].Argument[0..1].Parameter[1]",
520+
"@google-cloud/spanner;BatchTransaction;@google-cloud/spanner;Database;Member[batchTransaction].ReturnValue",
521+
"@google-cloud/spanner;BatchTransaction;@google-cloud/spanner;Database;Member[createBatchTransaction].ReturnValue.Awaited",
522+
"@google-cloud/spanner;~SqlExecutorDirect;@google-cloud/spanner;Database;Member[run,runPartitionedUpdate,runStream]",
523+
"@google-cloud/spanner;~SqlExecutorDirect;@google-cloud/spanner;Transaction;Member[run,runStream,runUpdate]",
524+
"@google-cloud/spanner;~SqlExecutorDirect;@google-cloud/spanner;BatchTransaction;Member[createQueryPartitions]",
525+
]
613526
}
614527
}
615528

616-
/**
617-
* A SQL execution that takes an array of SQL strings or { sql: string } objects.
618-
*/
619-
class SqlExecutionBatch extends SqlExecution, API::CallNode {
620-
SqlExecutionBatch() { this = transaction().getMember("batchUpdate").getACall() }
621-
622-
override DataFlow::Node getAQueryArgument() {
623-
// just use the whole array as the query argument, as arrays becomes tainted if one of the elements
624-
// are tainted
625-
result = this.getArgument(0)
626-
or
627-
result = this.getParameter(0).getUnknownMember().getMember("sql").getARhs()
529+
class SpannerSinks extends ModelInput::SinkModelCsv {
530+
override predicate row(string row) {
531+
// package; type; path; kind
532+
row =
533+
[
534+
"@google-cloud/spanner;~SqlExecutorDirect;Argument[0];sql-injection",
535+
"@google-cloud/spanner;~SqlExecutorDirect;Argument[0].Member[sql];sql-injection",
536+
"@google-cloud/spanner;Transaction;Member[batchUpdate].Argument[0];sql-injection",
537+
"@google-cloud/spanner;Transaction;Member[batchUpdate].Argument[0].ArrayElement.Member[sql];sql-injection",
538+
"@google-cloud/spanner;v1.SpannerClient;Member[executeSql,executeStreamingSql].Argument[0].Member[sql];sql-injection",
539+
]
628540
}
629541
}
630-
631-
/**
632-
* A SQL execution that only takes the input in the `sql` option, and do not accept query strings
633-
* directly.
634-
*/
635-
class SqlExecutionWithOption extends SqlExecution {
636-
SqlExecutionWithOption() {
637-
this = v1SpannerClient().getMember(["executeSql", "executeStreamingSql"]).getACall()
638-
}
639-
640-
override DataFlow::Node getAQueryArgument() { result = this.getOptionArgument(0, "sql") }
641-
}
642-
643-
/**
644-
* An expression that is interpreted as a SQL string.
645-
*/
646-
class QueryString extends SQL::SqlString {
647-
QueryString() { this = any(SqlExecution se).getAQueryArgument().asExpr() }
648-
}
649542
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/**
2+
* Provides classes for contributing a model, or using the interpreted results
3+
* of a model represented as data.
4+
*
5+
* - Use the `ModelInput` module to contribute new models.
6+
* - Use the `ModelOutput` module to access the model results in terms of API nodes.
7+
*
8+
* The package name refers to an NPM package name or a path within a package name such as `lodash/extend`.
9+
* The string `global` refers to the global object (whether it came from the `global` package or not).
10+
*
11+
* The following tokens have a language-specific interpretation:
12+
* - `Instance`: the value returned by a `new`-call to a function
13+
* - `Awaited`: the value from a resolved promise
14+
*
15+
* A `(package, type)` tuple may refer to the exported type named `type` from the NPM package `package`.
16+
* For example, `(express, Request)` would match a parameter below due to the type annotation:
17+
* ```ts
18+
* import * as express from 'express';
19+
* export function handler(req: express.Request) { ... }
20+
* ```
21+
*/
22+
23+
private import javascript
24+
private import internal.Shared as Shared
25+
import Shared::ModelInput as ModelInput
26+
import Shared::ModelOutput as ModelOutput
27+
28+
/**
29+
* A remote flow source originating from a CSV source row.
30+
*/
31+
private class RemoteFlowSourceFromCsv extends RemoteFlowSource {
32+
RemoteFlowSourceFromCsv() { this = ModelOutput::getASourceNode("remote").getAnImmediateUse() }
33+
34+
override string getSourceType() { result = "Remote flow" }
35+
}
36+
37+
/**
38+
* Like `ModelOutput::summaryStep` but with API nodes mapped to data-flow nodes.
39+
*/
40+
private predicate summaryStepNodes(DataFlow::Node pred, DataFlow::Node succ, string kind) {
41+
exists(API::Node predNode, API::Node succNode |
42+
ModelOutput::summaryStep(predNode, succNode, kind) and
43+
pred = predNode.getARhs() and
44+
succ = succNode.getAnImmediateUse()
45+
)
46+
}
47+
48+
/** Data flow steps induced by summary models of kind `value`. */
49+
private class DataFlowStepFromSummary extends DataFlow::SharedFlowStep {
50+
override predicate step(DataFlow::Node pred, DataFlow::Node succ) {
51+
summaryStepNodes(pred, succ, "value")
52+
}
53+
}
54+
55+
/** Taint steps induced by summary models of kind `taint`. */
56+
private class TaintStepFromSummary extends TaintTracking::SharedTaintStep {
57+
override predicate step(DataFlow::Node pred, DataFlow::Node succ) {
58+
summaryStepNodes(pred, succ, "taint")
59+
}
60+
}

0 commit comments

Comments
 (0)