Merge branch 'main' into aeisenberg/remove-upgrades

This commit is contained in:
Andrew Eisenberg
2022-01-11 11:25:27 -08:00
216 changed files with 8415 additions and 1980 deletions

View File

@@ -2,5 +2,8 @@ name: codeql/javascript-experimental-atm-lib
version: 0.0.2
extractor: javascript
library: true
groups:
- javascript
- experimental
dependencies:
codeql/javascript-all: "*"

View File

@@ -3,6 +3,9 @@ language: javascript
version: 0.0.2
suites: codeql-suites
defaultSuiteFile: codeql-suites/javascript-atm-code-scanning.qls
groups:
- javascript
- experimental
dependencies:
codeql/javascript-experimental-atm-lib: "*"
mlModels:

View File

@@ -85,6 +85,7 @@ import semmle.javascript.frameworks.CookieLibraries
import semmle.javascript.frameworks.Credentials
import semmle.javascript.frameworks.CryptoLibraries
import semmle.javascript.frameworks.D3
import semmle.javascript.frameworks.data.ModelsAsData
import semmle.javascript.frameworks.DateFunctions
import semmle.javascript.frameworks.DigitalOcean
import semmle.javascript.frameworks.Electron

View File

@@ -16,3 +16,13 @@ abstract class CredentialsExpr extends Expr {
*/
abstract string getCredentialsKind();
}
private class CredentialsFromModel extends CredentialsExpr {
string kind;
CredentialsFromModel() {
this = ModelOutput::getASinkNode("credentials[" + kind + "]").getARhs().asExpr()
}
override string getCredentialsKind() { result = kind }
}

View File

@@ -8,6 +8,10 @@ module SQL {
/** A string-valued expression that is interpreted as a SQL command. */
abstract class SqlString extends Expr { }
private class SqlStringFromModel extends SqlString {
SqlStringFromModel() { this = ModelOutput::getASinkNode("sql-injection").getARhs().asExpr() }
}
/**
* An expression that sanitizes a string to make it safe to embed into
* a SQL command.
@@ -474,176 +478,65 @@ private module MsSql {
* Provides classes modeling the `sequelize` package.
*/
private module Sequelize {
/** Gets an import of the `sequelize` module or one that re-exports it. */
API::Node sequelize() { result = API::moduleImport(["sequelize", "sequelize-typescript"]) }
/** Gets an expression that creates an instance of the `Sequelize` class. */
API::Node instance() {
result = [sequelize(), sequelize().getMember("Sequelize")].getInstance()
or
result = API::Node::ofType(["sequelize", "sequelize-typescript"], ["Sequelize", "default"])
}
/** A call to `Sequelize.query`. */
private class QueryCall extends DatabaseAccess, DataFlow::MethodCallNode {
QueryCall() { this = instance().getMember("query").getACall() }
override DataFlow::Node getAQueryArgument() {
result = this.getArgument(0)
or
result = this.getOptionArgument(0, "query")
class SequelizeModel extends ModelInput::TypeModelCsv {
override predicate row(string row) {
// package1;type1;package2;type2;path
row =
[
"sequelize;;sequelize-typescript;;", //
"sequelize;Sequelize;sequelize;default;", //
"sequelize;Sequelize;sequelize;;Instance",
"sequelize;Sequelize;sequelize;;Member[Sequelize].Instance",
]
}
}
/** An expression that is passed to `Sequelize.query` method and hence interpreted as SQL. */
class QueryString extends SQL::SqlString {
QueryString() {
this = any(QueryCall qc).getAQueryArgument().asExpr()
or
this = sequelize().getMember(["literal", "asIs"]).getParameter(0).getARhs().asExpr()
class SequelizeSink extends ModelInput::SinkModelCsv {
override predicate row(string row) {
row =
[
"sequelize;Sequelize;Member[query].Argument[0];sql-injection",
"sequelize;Sequelize;Member[query].Argument[0].Member[query];sql-injection",
"sequelize;;Member[literal,asIs].Argument[0];sql-injection",
"sequelize;;Argument[1];credentials[user name]",
"sequelize;;Argument[2];credentials[password]",
"sequelize;;Argument[0..].Member[username];credentials[user name]",
"sequelize;;Argument[0..].Member[password];credentials[password]"
]
}
}
/**
* An expression that is passed as user name or password when creating an instance of the
* `Sequelize` class.
*/
class Credentials extends CredentialsExpr {
string kind;
Credentials() {
exists(NewExpr ne, string prop |
ne = sequelize().getAnInstantiation().asExpr() and
(
this = ne.getArgument(1) and prop = "username"
or
this = ne.getArgument(2) and prop = "password"
or
ne.hasOptionArgument(ne.getNumArgument() - 1, prop, this)
) and
(
prop = "username" and kind = "user name"
or
prop = "password" and kind = prop
)
)
}
override string getCredentialsKind() { result = kind }
}
}
/**
* Provides classes modeling the Google Cloud Spanner library.
*/
private module Spanner {
/**
* Gets a node that refers to the `Spanner` class
*/
API::Node spanner() {
// older versions
result = API::moduleImport("@google-cloud/spanner")
or
// newer versions
result = API::moduleImport("@google-cloud/spanner").getMember("Spanner")
}
/**
* Gets a node that refers to an instance of the `Database` class.
*/
API::Node database() {
result =
spanner().getReturn().getMember("instance").getReturn().getMember("database").getReturn()
or
result = API::Node::ofType("@google-cloud/spanner", "Database")
}
/**
* Gets a node that refers to an instance of the `v1.SpannerClient` class.
*/
API::Node v1SpannerClient() {
result = spanner().getMember("v1").getMember("SpannerClient").getInstance()
or
result = API::Node::ofType("@google-cloud/spanner", "v1.SpannerClient")
}
/**
* Gets a node that refers to a transaction object.
*/
API::Node transaction() {
result =
database()
.getMember(["runTransaction", "runTransactionAsync"])
.getParameter([0, 1])
.getParameter(1)
or
result = API::Node::ofType("@google-cloud/spanner", "Transaction")
}
/** Gets an API node referring to a `BatchTransaction` object. */
API::Node batchTransaction() {
result = database().getMember("batchTransaction").getReturn()
or
result = database().getMember("createBatchTransaction").getReturn().getPromised()
or
result = API::Node::ofType("@google-cloud/spanner", "BatchTransaction")
}
/**
* A call to a Spanner method that executes a SQL query.
*/
abstract class SqlExecution extends DatabaseAccess, DataFlow::InvokeNode { }
/**
* A SQL execution that takes the input directly in the first argument or in the `sql` option.
*/
class SqlExecutionDirect extends SqlExecution {
SqlExecutionDirect() {
this = database().getMember(["run", "runPartitionedUpdate", "runStream"]).getACall()
or
this = transaction().getMember(["run", "runStream", "runUpdate"]).getACall()
or
this = batchTransaction().getMember("createQueryPartitions").getACall()
}
override DataFlow::Node getAQueryArgument() {
result = this.getArgument(0)
or
result = this.getOptionArgument(0, "sql")
private module SpannerCsv {
class SpannerTypes extends ModelInput::TypeModelCsv {
override predicate row(string row) {
// package1; type1; package2; type2; path
row =
[
"@google-cloud/spanner;;@google-cloud/spanner;;Member[Spanner]",
"@google-cloud/spanner;Database;@google-cloud/spanner;;ReturnValue.Member[instance].ReturnValue.Member[database].ReturnValue",
"@google-cloud/spanner;v1.SpannerClient;@google-cloud/spanner;;Member[v1].Member[SpannerClient].Instance",
"@google-cloud/spanner;Transaction;@google-cloud/spanner;Database;Member[runTransaction,runTransactionAsync].Argument[0..1].Parameter[1]",
"@google-cloud/spanner;BatchTransaction;@google-cloud/spanner;Database;Member[batchTransaction].ReturnValue",
"@google-cloud/spanner;BatchTransaction;@google-cloud/spanner;Database;Member[createBatchTransaction].ReturnValue.Awaited",
"@google-cloud/spanner;~SqlExecutorDirect;@google-cloud/spanner;Database;Member[run,runPartitionedUpdate,runStream]",
"@google-cloud/spanner;~SqlExecutorDirect;@google-cloud/spanner;Transaction;Member[run,runStream,runUpdate]",
"@google-cloud/spanner;~SqlExecutorDirect;@google-cloud/spanner;BatchTransaction;Member[createQueryPartitions]",
]
}
}
/**
* A SQL execution that takes an array of SQL strings or { sql: string } objects.
*/
class SqlExecutionBatch extends SqlExecution, API::CallNode {
SqlExecutionBatch() { this = transaction().getMember("batchUpdate").getACall() }
override DataFlow::Node getAQueryArgument() {
// just use the whole array as the query argument, as arrays becomes tainted if one of the elements
// are tainted
result = this.getArgument(0)
or
result = this.getParameter(0).getUnknownMember().getMember("sql").getARhs()
class SpannerSinks extends ModelInput::SinkModelCsv {
override predicate row(string row) {
// package; type; path; kind
row =
[
"@google-cloud/spanner;~SqlExecutorDirect;Argument[0];sql-injection",
"@google-cloud/spanner;~SqlExecutorDirect;Argument[0].Member[sql];sql-injection",
"@google-cloud/spanner;Transaction;Member[batchUpdate].Argument[0];sql-injection",
"@google-cloud/spanner;Transaction;Member[batchUpdate].Argument[0].ArrayElement.Member[sql];sql-injection",
"@google-cloud/spanner;v1.SpannerClient;Member[executeSql,executeStreamingSql].Argument[0].Member[sql];sql-injection",
]
}
}
/**
* A SQL execution that only takes the input in the `sql` option, and do not accept query strings
* directly.
*/
class SqlExecutionWithOption extends SqlExecution {
SqlExecutionWithOption() {
this = v1SpannerClient().getMember(["executeSql", "executeStreamingSql"]).getACall()
}
override DataFlow::Node getAQueryArgument() { result = this.getOptionArgument(0, "sql") }
}
/**
* An expression that is interpreted as a SQL string.
*/
class QueryString extends SQL::SqlString {
QueryString() { this = any(SqlExecution se).getAQueryArgument().asExpr() }
}
}

View File

@@ -0,0 +1,60 @@
/**
* Provides classes for contributing a model, or using the interpreted results
* of a model represented as data.
*
* - Use the `ModelInput` module to contribute new models.
* - Use the `ModelOutput` module to access the model results in terms of API nodes.
*
* The package name refers to an NPM package name or a path within a package name such as `lodash/extend`.
* The string `global` refers to the global object (whether it came from the `global` package or not).
*
* The following tokens have a language-specific interpretation:
* - `Instance`: the value returned by a `new`-call to a function
* - `Awaited`: the value from a resolved promise
*
* A `(package, type)` tuple may refer to the exported type named `type` from the NPM package `package`.
* For example, `(express, Request)` would match a parameter below due to the type annotation:
* ```ts
* import * as express from 'express';
* export function handler(req: express.Request) { ... }
* ```
*/
private import javascript
private import internal.Shared as Shared
import Shared::ModelInput as ModelInput
import Shared::ModelOutput as ModelOutput
/**
* A remote flow source originating from a CSV source row.
*/
private class RemoteFlowSourceFromCsv extends RemoteFlowSource {
RemoteFlowSourceFromCsv() { this = ModelOutput::getASourceNode("remote").getAnImmediateUse() }
override string getSourceType() { result = "Remote flow" }
}
/**
* Like `ModelOutput::summaryStep` but with API nodes mapped to data-flow nodes.
*/
private predicate summaryStepNodes(DataFlow::Node pred, DataFlow::Node succ, string kind) {
exists(API::Node predNode, API::Node succNode |
ModelOutput::summaryStep(predNode, succNode, kind) and
pred = predNode.getARhs() and
succ = succNode.getAnImmediateUse()
)
}
/** Data flow steps induced by summary models of kind `value`. */
private class DataFlowStepFromSummary extends DataFlow::SharedFlowStep {
override predicate step(DataFlow::Node pred, DataFlow::Node succ) {
summaryStepNodes(pred, succ, "value")
}
}
/** Taint steps induced by summary models of kind `taint`. */
private class TaintStepFromSummary extends TaintTracking::SharedTaintStep {
override predicate step(DataFlow::Node pred, DataFlow::Node succ) {
summaryStepNodes(pred, succ, "taint")
}
}

View File

@@ -0,0 +1,134 @@
/**
* Contains the language-specific part of the models-as-data implementation found in `Shared.qll`.
*
* It must export the following members:
* ```codeql
* class Unit // a unit type
* module API // the API graph module
* predicate isPackageUsed(string package)
* API::Node getExtraNodeFromPath(string package, string type, string path, int n)
* API::Node getExtraSuccessorFromNode(API::Node node, AccessPathToken token)
* API::Node getExtraSuccessorFromInvoke(API::InvokeNode node, AccessPathToken token)
* predicate invocationMatchesExtraCallSiteFilter(API::InvokeNode invoke, AccessPathToken token)
* ```
*/
private import javascript as js
private import js::DataFlow as DataFlow
private import Shared
class Unit = js::Unit;
module API = js::API;
/**
* Holds if models describing `package` may be relevant for the analysis of this database.
*/
predicate isPackageUsed(string package) {
exists(DataFlow::moduleImport(package))
or
package = "global"
or
exists(API::Node::ofType(package, _))
}
/** Holds if `global` is a global variable referenced via a the `global` package in a CSV row. */
private predicate isRelevantGlobal(string global) {
exists(AccessPath path, AccessPathToken token |
isRelevantFullPath("global", "", path) and
token = path.getToken(0) and
token.getName() = "Member" and
global = token.getAnArgument()
)
}
/** An API graph entry point for global variables mentioned in a model. */
private class GlobalApiEntryPoint extends API::EntryPoint {
string global;
GlobalApiEntryPoint() {
isRelevantGlobal(global) and
this = "GlobalApiEntryPoint:" + global
}
override DataFlow::SourceNode getAUse() { result = DataFlow::globalVarRef(global) }
override DataFlow::Node getARhs() { none() }
/** Gets the name of the global variable. */
string getGlobal() { result = global }
}
/**
* Gets an API node referring to the given global variable (if relevant).
*/
private API::Node getGlobalNode(string globalName) {
result = any(GlobalApiEntryPoint e | e.getGlobal() = globalName).getNode()
}
/** Gets a JavaScript-specific interpretation of the `(package, type, path)` tuple after resolving the first `n` access path tokens. */
bindingset[package, type, path]
API::Node getExtraNodeFromPath(string package, string type, AccessPath path, int n) {
// Global variable accesses is via the 'global' package
exists(AccessPathToken token |
package = getAPackageAlias("global") and
type = "" and
token = path.getToken(0) and
token.getName() = "Member" and
result = getGlobalNode(token.getAnArgument()) and
n = 1
)
or
// Access instance of a type based on type annotations
n = 0 and
result = API::Node::ofType(getAPackageAlias(package), type)
}
/**
* Gets a JavaScript-specific API graph successor of `node` reachable by resolving `token`.
*/
bindingset[token]
API::Node getExtraSuccessorFromNode(API::Node node, AccessPathToken token) {
token.getName() = "Instance" and
result = node.getInstance()
or
token.getName() = "Awaited" and
result = node.getPromised()
or
token.getName() = "ArrayElement" and
result = node.getMember(DataFlow::PseudoProperties::arrayElement())
or
token.getName() = "Element" and
result = node.getMember(DataFlow::PseudoProperties::arrayLikeElement())
or
// Note: MapKey not currently supported
token.getName() = "MapValue" and
result = node.getMember(DataFlow::PseudoProperties::mapValueAll())
or
// Currently we need to include the "unknown member" for ArrayElement and Element since
// API graphs do not use store/load steps for arrays
token.getName() = ["ArrayElement", "Element"] and
result = node.getUnknownMember()
}
/**
* Gets a JavaScript-specific API graph successor of `node` reachable by resolving `token`.
*/
bindingset[token]
API::Node getExtraSuccessorFromInvoke(API::InvokeNode node, AccessPathToken token) {
token.getName() = "Instance" and
result = node.getInstance()
}
/**
* Holds if `invoke` matches the JS-specific call site filter in `token`.
*/
bindingset[token]
predicate invocationMatchesExtraCallSiteFilter(API::InvokeNode invoke, AccessPathToken token) {
token.getName() = "NewCall" and
invoke instanceof API::NewNode
or
token.getName() = "Call" and
invoke instanceof API::CallNode and
invoke instanceof DataFlow::CallNode // Workaround compiler bug
}

View File

@@ -0,0 +1,591 @@
/**
* INTERNAL use only. This is an experimental API subject to change without notice.
*
* Provides classes and predicates for dealing with flow models specified in CSV format.
*
* The CSV specification has the following columns:
* - Sources:
* `package; type; path; kind`
* - Sinks:
* `package; type; path; kind`
* - Summaries:
* `package; type; path; input; output; kind`
* - Types:
* `package1; type1; package2; type2; path`
*
* The interpretation of a row is similar to API-graphs with a left-to-right
* reading.
* 1. The `package` column selects a package name, as it would be referenced in the source code,
* such as an NPM package, PIP package, or Ruby gem. (See `ModelsAsData.qll` for language-specific details).
* It may also be a synthetic package used for a type definition (see type definitions below).
* 2. The `type` column selects all instances of a named type originating from that package,
* or the empty string if referring to the package itself.
* It can also be a synthetic type name defined by a type definition (see type definitions below).
* 3. The `path` column is a `.`-separated list of "access path tokens" to resolve, starting at the node selected by `package` and `type`.
* The possible access path tokens are:
* - Member[x] : a property named `x`. May be a comma-separated list of named.
* - Argument[n]: the n-th argument to a call. May be a range of form `x..y` (inclusive) and/or a comma-separated list.
* Additionally, `N-1` refers to the last argument, `N-2` refers to the second-last, and so on.
* - Parameter[n]: the n-th parameter of a callback. May be a range of form `x..y` (inclusive) and/or a comma-separated list.
* - ReturnValue: the value returned by a function call
* - Instance: the value returned by a constructor call
* - Awaited: the value from a resolved promise/future-like object
* - WithArity[n]: match a call with the given arity. May be a range of form `x..y` (inclusive) and/or a comma-separated list.
* - Other langauge-specific tokens mentioned in `ModelsAsData.qll`.
* 4. The `input` and `output` columns specify how data enters and leaves the element selected by the
* first `(package, type, path)` tuple. Both strings are `.`-separated access paths
* of the same syntax as the `path` column.
* 5. The `kind` column is a tag that can be referenced from QL to determine to
* which classes the interpreted elements should be added. For example, for
* sources `"remote"` indicates a default remote flow source, and for summaries
* `"taint"` indicates a default additional taint step and `"value"` indicates a
* globally applicable value-preserving step.
*
* ### Types
*
* A type row of form `package1; type1; package2; type2; path` indicates that `package2; type2; path`
* should be seen as an instance of the type `package1; type1`.
*
* A `(package,type)` pair may refer to a static type or a synthetic type name used internally in the model.
* Synthetic type names can be used to reuse intermediate sub-paths, when there are multiple ways to access the same
* element.
* See `ModelsAsData.qll` for the langauge-specific interpretation of packages and static type names.
*
* By convention, if one wants to avoid clashes with static types from the package, the type name
* should be prefixed with a tilde character (`~`). For example, `(foo, ~Bar)` can be used to indicate that
* the type is related to the `foo` package but is not intended to match a static type.
*/
private import Impl as Impl
private class Unit = Impl::Unit;
private module API = Impl::API;
/** Module containing hooks for providing input data to be interpreted as a model. */
module ModelInput {
/**
* A unit class for adding additional source model rows.
*
* Extend this class to add additional source definitions.
*/
class SourceModelCsv extends Unit {
/**
* Holds if `row` specifies a source definition.
*
* A row of form
* ```
* package;type;path;kind
* ```
* indicates that the value at `(package, type, path)` should be seen as a flow
* source of the given `kind`.
*
* The kind `remote` represents a general remote flow source.
*/
abstract predicate row(string row);
}
/**
* A unit class for adding additional sink model rows.
*
* Extend this class to add additional sink definitions.
*/
class SinkModelCsv extends Unit {
/**
* Holds if `row` specifies a sink definition.
*
* A row of form
* ```
* package;type;path;kind
* ```
* indicates that the value at `(package, type, path)` should be seen as a sink
* of the given `kind`.
*/
abstract predicate row(string row);
}
/**
* A unit class for adding additional summary model rows.
*
* Extend this class to add additional flow summary definitions.
*/
class SummaryModelCsv extends Unit {
/**
* Holds if `row` specifies a summary definition.
*
* A row of form
* ```
* package;type;path;input;output;kind
* ```
* indicates that for each call to `(package, type, path)`, the value referred to by `input`
* can flow to the value referred to by `output`.
*
* `kind` should be either `value` or `taint`, for value-preserving or taint-preserving steps,
* respectively.
*/
abstract predicate row(string row);
}
/**
* A unit class for adding additional type model rows.
*
* Extend this class to add additional type definitions.
*/
class TypeModelCsv extends Unit {
/**
* Holds if `row` specifies a type definition.
*
* A row of form,
* ```
* package1;type1;package2;type2;path
* ```
* indicates that `(package2, type2, path)` should be seen as an instance of `(package1, type1)`.
*/
abstract predicate row(string row);
}
}
private import ModelInput
/**
* Append `;dummy` to the value of `s` to work around the fact that `string.split(delim,n)`
* does not preserve empty trailing substrings.
*/
bindingset[result]
private string inversePad(string s) { s = result + ";dummy" }
private predicate sourceModel(string row) { any(SourceModelCsv s).row(inversePad(row)) }
private predicate sinkModel(string row) { any(SinkModelCsv s).row(inversePad(row)) }
private predicate summaryModel(string row) { any(SummaryModelCsv s).row(inversePad(row)) }
private predicate typeModel(string row) { any(TypeModelCsv s).row(inversePad(row)) }
/**
* Replaces `..` with `-->` in order to simplify subsequent parsing.
*/
bindingset[path]
private string normalizePath(string path) { result = path.replaceAll("..", "-->") }
/** Holds if a source model exists for the given parameters. */
predicate sourceModel(string package, string type, string path, string kind) {
exists(string row |
sourceModel(row) and
row.splitAt(";", 0) = package and
row.splitAt(";", 1) = type and
normalizePath(row.splitAt(";", 2)) = path and
row.splitAt(";", 3) = kind
)
}
/** Holds if a sink model exists for the given parameters. */
private predicate sinkModel(string package, string type, string path, string kind) {
exists(string row |
sinkModel(row) and
row.splitAt(";", 0) = package and
row.splitAt(";", 1) = type and
normalizePath(row.splitAt(";", 2)) = path and
row.splitAt(";", 3) = kind
)
}
/** Holds if a summary model `row` exists for the given parameters. */
private predicate summaryModel(
string package, string type, string path, string input, string output, string kind
) {
exists(string row |
summaryModel(row) and
row.splitAt(";", 0) = package and
row.splitAt(";", 1) = type and
normalizePath(row.splitAt(";", 2)) = path and
normalizePath(row.splitAt(";", 3)) = input and
normalizePath(row.splitAt(";", 4)) = output and
row.splitAt(";", 5) = kind
)
}
/** Holds if an type model exists for the given parameters. */
private predicate typeModel(
string package1, string type1, string package2, string type2, string path
) {
exists(string row |
typeModel(row) and
row.splitAt(";", 0) = package1 and
row.splitAt(";", 1) = type1 and
row.splitAt(";", 2) = package2 and
row.splitAt(";", 3) = type2 and
normalizePath(row.splitAt(";", 4)) = path
)
}
/**
* Gets a package that should be seen as an alias for the given other `package`,
* or the `package` itself.
*/
bindingset[package]
bindingset[result]
string getAPackageAlias(string package) {
typeModel(package, "", result, "", "")
or
result = package
}
/**
* Holds if CSV rows involving `package` might be relevant for the analysis of this database.
*/
private predicate isRelevantPackage(string package) {
Impl::isPackageUsed(package)
or
exists(string other |
isRelevantPackage(other) and
typeModel(package, _, other, _, _)
)
}
/**
* Holds if `package,type,path` is used in some CSV row.
*/
pragma[nomagic]
predicate isRelevantFullPath(string package, string type, string path) {
isRelevantPackage(package) and
(
sourceModel(package, type, path, _) or
sinkModel(package, type, path, _) or
summaryModel(package, type, path, _, _, _) or
typeModel(_, _, package, type, path)
)
}
/**
* A string that occurs as an access path (either identifying or input/output spec)
* which might be relevant for this database.
*/
class AccessPath extends string {
AccessPath() {
isRelevantFullPath(_, _, this)
or
exists(string package | isRelevantPackage(package) |
summaryModel(package, _, _, this, _, _) or
summaryModel(package, _, _, _, this, _)
)
}
/** Gets the `n`th token on the access path as a string. */
string getRawToken(int n) {
this != "" and // The empty path should have zero tokens, not a single empty token
result = this.splitAt(".", n)
}
/** Gets the `n`th token on the access path. */
AccessPathToken getToken(int n) { result = this.getRawToken(n) }
/** Gets the number of tokens on the path. */
int getNumToken() { result = count(int n | exists(this.getRawToken(n))) }
}
/**
* Gets a successor of `node` in the API graph.
*/
bindingset[token]
private API::Node getSuccessorFromNode(API::Node node, AccessPathToken token) {
// API graphs use the same label for arguments and parameters. An edge originating from a
// use-node represents be an argument, and an edge originating from a def-node represents a parameter.
// We just map both to the same thing.
token.getName() = ["Argument", "Parameter"] and
result = node.getParameter(getAnIntFromStringUnbounded(token.getAnArgument()))
or
token.getName() = "Member" and
result = node.getMember(token.getAnArgument())
or
token.getName() = "ReturnValue" and
result = node.getReturn()
or
// Language-specific tokens
result = Impl::getExtraSuccessorFromNode(node, token)
}
/**
* Gets an API-graph successor for the given invocation.
*/
bindingset[token]
private API::Node getSuccessorFromInvoke(API::InvokeNode invoke, AccessPathToken token) {
token.getName() = "Argument" and
(
result = invoke.getParameter(getAnIntFromStringUnbounded(token.getAnArgument()))
or
result =
invoke
.getParameter(getAnIntFromStringWithArity(token.getAnArgument(), invoke.getNumArgument()))
)
or
token.getName() = "ReturnValue" and
result = invoke.getReturn()
or
// Language-specific tokens
result = Impl::getExtraSuccessorFromInvoke(invoke, token)
}
/**
* Holds if `invoke` invokes a call-site filter given by `token`.
*/
pragma[inline]
private predicate invocationMatchesCallSiteFilter(API::InvokeNode invoke, AccessPathToken token) {
token.getName() = "WithArity" and
invoke.getNumArgument() = getAnIntFromStringUnbounded(token.getAnArgument())
or
Impl::invocationMatchesExtraCallSiteFilter(invoke, token)
}
/**
* Gets the API node identified by the first `n` tokens of `path` in the given `(package, type, path)` tuple.
*/
pragma[nomagic]
API::Node getNodeFromPath(string package, string type, AccessPath path, int n) {
isRelevantFullPath(package, type, path) and
(
type = "" and
n = 0 and
result = API::moduleImport(package)
or
n = 0 and
exists(string package2, string type2, AccessPath path2 |
typeModel(package, type, package2, type2, path2) and
result = getNodeFromPath(package2, type2, path2, path2.getNumToken())
)
or
// Language-specific cases, such as handling of global variables
result = Impl::getExtraNodeFromPath(package, type, path, n)
)
or
result = getSuccessorFromNode(getNodeFromPath(package, type, path, n - 1), path.getToken(n - 1))
or
// Similar to the other recursive case, but where the path may have stepped through one or more call-site filters
result =
getSuccessorFromInvoke(getInvocationFromPath(package, type, path, n - 1), path.getToken(n - 1))
}
/** Gets the node identified by the given `(package, type, path)` tuple. */
API::Node getNodeFromPath(string package, string type, AccessPath path) {
result = getNodeFromPath(package, type, path, path.getNumToken())
}
/**
* Gets an invocation identified by the given `(package, type, path)` tuple.
*
* Unlike `getNodeFromPath`, the `path` may end with one or more call-site filters.
*/
API::InvokeNode getInvocationFromPath(string package, string type, AccessPath path, int n) {
result = getNodeFromPath(package, type, path, n).getAnInvocation()
or
result = getInvocationFromPath(package, type, path, n - 1) and
invocationMatchesCallSiteFilter(result, path.getToken(n - 1))
}
/** Gets an invocation identified by the given `(package, type, path)` tuple. */
API::InvokeNode getInvocationFromPath(string package, string type, AccessPath path) {
result = getInvocationFromPath(package, type, path, path.getNumToken())
}
/**
* Holds if a summary edge with the given `input, output, kind` columns have a `package, type, path` tuple
* that resolves to `baseNode`.
*/
private predicate resolvedSummaryBase(
API::InvokeNode baseNode, AccessPath input, AccessPath output, string kind
) {
exists(string package, string type, AccessPath path |
summaryModel(package, type, path, input, output, kind) and
baseNode = getInvocationFromPath(package, type, path)
)
}
/**
* Holds if `path` is an input or output spec for a summary with the given `base` node.
*/
pragma[nomagic]
private predicate relevantInputOutputPath(API::InvokeNode base, AccessPath path) {
resolvedSummaryBase(base, path, _, _)
or
resolvedSummaryBase(base, _, path, _)
}
/**
* Gets the API node for the first `n` tokens of the given input/output path, evaluated relative to `baseNode`.
*/
private API::Node getNodeFromInputOutputPath(API::InvokeNode baseNode, AccessPath path, int n) {
relevantInputOutputPath(baseNode, path) and
(
n = 1 and
result = getSuccessorFromInvoke(baseNode, path.getToken(0))
or
result =
getSuccessorFromNode(getNodeFromInputOutputPath(baseNode, path, n - 1), path.getToken(n - 1))
)
}
/**
* Gets the API node for the given input/output path, evaluated relative to `baseNode`.
*/
private API::Node getNodeFromInputOutputPath(API::InvokeNode baseNode, AccessPath path) {
result = getNodeFromInputOutputPath(baseNode, path, path.getNumToken())
}
/**
* An access part token such as `Argument[1]` or `ReturnValue`, appearing in one or more access paths.
*/
class AccessPathToken extends string {
AccessPathToken() { this = any(AccessPath path).getRawToken(_) }
private string getPart(int part) {
result = this.regexpCapture("([^\\[]+)(?:\\[([^\\]]*)\\])?", part)
}
/** Gets the name of the token, such as `Member` from `Member[x]` */
string getName() { result = this.getPart(1) }
/**
* Gets the argument list, such as `1,2` from `Member[1,2]`,
* or has no result if there are no arguments.
*/
string getArgumentList() { result = this.getPart(2) }
/** Gets the `n`th argument to this token, such as `x` or `y` from `Member[x,y]`. */
string getArgument(int n) { result = this.getArgumentList().splitAt(",", n) }
/** Gets an argument to this token, such as `x` or `y` from `Member[x,y]`. */
string getAnArgument() { result = this.getArgument(_) }
/** Gets the number of arguments to this token, such as 2 for `Member[x,y]` or zero for `ReturnValue`. */
int getNumArgument() { result = count(int n | exists(this.getArgument(n))) }
}
/**
* Convenience-predicate for extracting two capture groups at once.
*/
bindingset[input, regexp]
private predicate regexpCaptureTwo(string input, string regexp, string capture1, string capture2) {
capture1 = input.regexpCapture(regexp, 1) and
capture2 = input.regexpCapture(regexp, 2)
}
/**
* Parses an integer constant `n` or interval `n1..n2` (inclusive) and gets the value
* of the constant or any value contained in the interval.
*/
bindingset[arg]
private int getAnIntFromString(string arg) {
result = arg.toInt()
or
// Match "n1..n2", where ".." has previously been replaced with "-->" to simplify parsing
exists(string lo, string hi |
regexpCaptureTwo(arg, "(\\d+)-->(\\d+)", lo, hi) and
result = [lo.toInt() .. hi.toInt()]
)
}
/**
* Parses a lower-bounded interval `n..` and gets the lower bound.
*/
bindingset[arg]
private int getLowerBoundFromString(string arg) {
// Match "n..", where ".." has previously been replaced with "-->" to simplify parsing
result = arg.regexpCapture("(\\d+)-->", 1).toInt()
}
/**
* Parses an integer constant or interval (bounded or unbounded) and gets any
* of the integers contained within (of which there may be infinitely many).
*
* Has no result for arguments involving an explicit arity, such as `N-1`.
*/
bindingset[arg, result]
private int getAnIntFromStringUnbounded(string arg) {
result = getAnIntFromString(arg)
or
result >= getLowerBoundFromString(arg)
}
/**
* Parses an integer constant or interval (bounded or unbounded) that explicitly
* references the arity, such as `N-1` or `N-3..N-1`.
*
* Note that such expressions will never resolve to a negative index, even if the
* arity is zero (it will have no result in that case).
*/
bindingset[arg, arity]
private int getAnIntFromStringWithArity(string arg, int arity) {
result >= 0 and // do not allow N-1 to resolve to a negative index
exists(string lo |
// N-x
lo = arg.regexpCapture("N-(\\d+)", 1) and
result = arity - lo.toInt()
or
// N-x..
lo = arg.regexpCapture("N-(\\d+)-->", 1) and
result = [arity - lo.toInt(), arity - 1]
)
or
exists(string lo, string hi |
// x..N-y
regexpCaptureTwo(arg, "(\\d+)-->N-(\\d+)", lo, hi) and
result = [lo.toInt() .. arity - hi.toInt()]
or
// N-x..Ny
regexpCaptureTwo(arg, "N-(\\d+)-->N-(\\d+)", lo, hi) and
result = [arity - lo.toInt() .. arity - hi.toInt()] and
result >= 0
or
// N-x..y
regexpCaptureTwo(arg, "N-(\\d+)-->(\\d+)", lo, hi) and
result = [arity - lo.toInt() .. hi.toInt()] and
result >= 0
)
}
/**
* Module providing access to the imported models in terms of API graph nodes.
*/
module ModelOutput {
/**
* Holds if a CSV source model contributed `source` with the given `kind`.
*/
API::Node getASourceNode(string kind) {
exists(string package, string type, string path |
sourceModel(package, type, path, kind) and
result = getNodeFromPath(package, type, path)
)
}
/**
* Holds if a CSV sink model contributed `sink` with the given `kind`.
*/
API::Node getASinkNode(string kind) {
exists(string package, string type, string path |
sinkModel(package, type, path, kind) and
result = getNodeFromPath(package, type, path)
)
}
/**
* Holds if a CSV summary contributed the step `pred -> succ` of the given `kind`.
*/
predicate summaryStep(API::Node pred, API::Node succ, string kind) {
exists(API::InvokeNode base, AccessPath input, AccessPath output |
resolvedSummaryBase(base, input, output, kind) and
pred = getNodeFromInputOutputPath(base, input) and
succ = getNodeFromInputOutputPath(base, output)
)
}
/**
* Holds if `node` is seen as an instance of `(package,type)` due to a type definition
* contributed by a CSV model.
*/
API::Node getATypeNode(string package, string type) {
exists(string package2, string type2, AccessPath path |
typeModel(package, type, package2, type2, path) and
result = getNodeFromPath(package2, type2, path)
)
}
}

View File

@@ -0,0 +1,73 @@
consistencyIssue
taintFlow
| test.js:5:30:5:37 | source() | test.js:5:8:5:38 | testlib ... urce()) |
| test.js:6:22:6:29 | source() | test.js:6:8:6:30 | preserv ... urce()) |
| test.js:7:41:7:48 | source() | test.js:7:8:7:49 | require ... urce()) |
| test.js:11:38:11:45 | source() | test.js:11:8:11:55 | testlib ... , 1, 1) |
| test.js:13:44:13:51 | source() | test.js:13:8:13:55 | testlib ... e(), 1) |
| test.js:17:47:17:54 | source() | test.js:17:8:17:61 | testlib ... , 1, 1) |
| test.js:18:50:18:57 | source() | test.js:18:8:18:61 | testlib ... e(), 1) |
| test.js:19:53:19:60 | source() | test.js:19:8:19:61 | testlib ... urce()) |
| test.js:21:34:21:41 | source() | test.js:21:8:21:51 | testlib ... , 1, 1) |
| test.js:22:37:22:44 | source() | test.js:22:8:22:51 | testlib ... , 1, 1) |
| test.js:23:40:23:47 | source() | test.js:23:8:23:51 | testlib ... e(), 1) |
| test.js:24:43:24:50 | source() | test.js:24:8:24:51 | testlib ... urce()) |
| test.js:31:29:31:36 | source() | test.js:32:10:32:10 | y |
| test.js:37:29:37:36 | source() | test.js:38:10:38:10 | y |
| test.js:46:18:46:25 | source() | test.js:46:18:46:25 | source() |
| test.js:47:22:47:29 | source() | test.js:47:22:47:29 | source() |
| test.js:49:24:49:31 | source() | test.js:49:24:49:31 | source() |
| test.js:53:27:53:34 | source() | test.js:53:27:53:34 | source() |
| test.js:58:31:58:38 | source() | test.js:58:31:58:38 | source() |
| test.js:62:34:62:41 | source() | test.js:62:34:62:41 | source() |
| test.js:67:31:67:38 | source() | test.js:67:31:67:38 | source() |
| test.js:68:34:68:41 | source() | test.js:68:34:68:41 | source() |
| test.js:72:36:72:43 | source() | test.js:72:36:72:43 | source() |
| test.js:73:39:73:46 | source() | test.js:73:39:73:46 | source() |
| test.js:75:28:75:35 | source() | test.js:75:28:75:35 | source() |
| test.js:76:31:76:38 | source() | test.js:76:31:76:38 | source() |
| test.js:77:34:77:41 | source() | test.js:77:34:77:41 | source() |
| test.js:81:28:81:35 | source() | test.js:81:28:81:35 | source() |
isSink
| test.js:46:18:46:25 | source() | test-sink |
| test.js:47:22:47:29 | source() | test-sink |
| test.js:49:24:49:31 | source() | test-sink |
| test.js:53:27:53:34 | source() | test-sink |
| test.js:55:38:55:38 | 4 | test-sink |
| test.js:56:38:56:38 | 4 | test-sink |
| test.js:57:38:57:38 | 4 | test-sink |
| test.js:58:31:58:38 | source() | test-sink |
| test.js:60:41:60:41 | 3 | test-sink |
| test.js:61:41:61:41 | 3 | test-sink |
| test.js:62:34:62:41 | source() | test-sink |
| test.js:63:34:63:34 | 3 | test-sink |
| test.js:65:38:65:38 | 3 | test-sink |
| test.js:65:41:65:41 | 4 | test-sink |
| test.js:66:38:66:38 | 3 | test-sink |
| test.js:66:41:66:41 | 4 | test-sink |
| test.js:67:31:67:38 | source() | test-sink |
| test.js:67:41:67:41 | 4 | test-sink |
| test.js:68:31:68:31 | 3 | test-sink |
| test.js:68:34:68:41 | source() | test-sink |
| test.js:70:43:70:43 | 3 | test-sink |
| test.js:70:46:70:46 | 4 | test-sink |
| test.js:71:43:71:43 | 3 | test-sink |
| test.js:71:46:71:46 | 4 | test-sink |
| test.js:72:36:72:43 | source() | test-sink |
| test.js:72:46:72:46 | 4 | test-sink |
| test.js:73:36:73:36 | 3 | test-sink |
| test.js:73:39:73:46 | source() | test-sink |
| test.js:75:28:75:35 | source() | test-sink |
| test.js:75:38:75:38 | 2 | test-sink |
| test.js:75:41:75:41 | 3 | test-sink |
| test.js:76:28:76:28 | 1 | test-sink |
| test.js:76:31:76:38 | source() | test-sink |
| test.js:76:41:76:41 | 3 | test-sink |
| test.js:77:28:77:28 | 1 | test-sink |
| test.js:77:31:77:31 | 2 | test-sink |
| test.js:77:34:77:41 | source() | test-sink |
| test.js:78:28:78:28 | 1 | test-sink |
| test.js:78:31:78:31 | 2 | test-sink |
| test.js:78:34:78:34 | 3 | test-sink |
| test.js:81:28:81:35 | source() | test-sink |
| test.js:82:28:82:28 | 1 | test-sink |

View File

@@ -0,0 +1,86 @@
import * as testlib from 'testlib';
import { preserveTaint } from 'testlib';
function testPreserveTaint() {
sink(testlib.preserveTaint(source())); // NOT OK
sink(preserveTaint(source())); // NOT OK
sink(require('testlib').preserveTaint(source())); // NOT OK
sink(require('testlib').preserveTaint('safe')); // OK
sink(require('testlib').preserveTaint(1, source())); // OK
sink(testlib.preserveArgZeroAndTwo(source(), 1, 1, 1)); // NOT OK
sink(testlib.preserveArgZeroAndTwo(1, source(), 1, 1)); // OK
sink(testlib.preserveArgZeroAndTwo(1, 1, source(), 1)); // NOT OK
sink(testlib.preserveArgZeroAndTwo(1, 1, 1, source())); // OK
sink(testlib.preserveAllButFirstArgument(source(), 1, 1, 1)); // OK
sink(testlib.preserveAllButFirstArgument(1, source(), 1, 1)); // NOT OK
sink(testlib.preserveAllButFirstArgument(1, 1, source(), 1)); // NOT OK
sink(testlib.preserveAllButFirstArgument(1, 1, 1, source())); // NOT OK
sink(testlib.preserveAllIfCall(source(), 1, 1, 1)); // NOT OK
sink(testlib.preserveAllIfCall(1, source(), 1, 1)); // NOT OK
sink(testlib.preserveAllIfCall(1, 1, source(), 1)); // NOT OK
sink(testlib.preserveAllIfCall(1, 1, 1, source())); // NOT OK
sink(new testlib.preserveAllIfCall(source(), 1, 1, 1)); // OK
sink(new testlib.preserveAllIfCall(1, source(), 1, 1)); // OK
sink(new testlib.preserveAllIfCall(1, 1, source(), 1)); // OK
sink(new testlib.preserveAllIfCall(1, 1, 1, source())); // OK
testlib.taintIntoCallback(source(), y => {
sink(y); // NOT OK
});
testlib.taintIntoCallback('safe', y => {
sink(y); // OK
});
testlib.taintIntoCallback(source(), undefined, y => {
sink(y); // NOT OK
});
testlib.taintIntoCallback(source(), undefined, undefined, y => {
sink(y); // OK - only callback 1-2 receive taint
});
}
function testSinks() {
testlib.mySink(source()); // NOT OK
new testlib.mySink(source()); // NOT OK
testlib.mySinkIfCall(source()); // NOT OK
new testlib.mySinkIfCall(source()); // OK
testlib.mySinkIfNew(source()); // OK
new testlib.mySinkIfNew(source()); // NOT OK
testlib.mySinkLast(source(), 2, 3, 4); // OK
testlib.mySinkLast(1, source(), 3, 4); // OK
testlib.mySinkLast(1, 2, source(), 4); // OK
testlib.mySinkLast(1, 2, 3, source()); // NOT OK
testlib.mySinkSecondLast(source(), 2, 3, 4); // OK
testlib.mySinkSecondLast(1, source(), 3, 4); // OK
testlib.mySinkSecondLast(1, 2, source(), 4); // NOT OK
testlib.mySinkSecondLast(1, 2, 3, source()); // OK
testlib.mySinkTwoLast(source(), 2, 3, 4); // OK
testlib.mySinkTwoLast(1, source(), 3, 4); // OK
testlib.mySinkTwoLast(1, 2, source(), 4); // NOT OK
testlib.mySinkTwoLast(1, 2, 3, source()); // NOT OK
testlib.mySinkTwoLastRange(source(), 2, 3, 4); // OK
testlib.mySinkTwoLastRange(1, source(), 3, 4); // OK
testlib.mySinkTwoLastRange(1, 2, source(), 4); // NOT OK
testlib.mySinkTwoLastRange(1, 2, 3, source()); // NOT OK
testlib.mySinkExceptLast(source(), 2, 3, 4); // NOT OK
testlib.mySinkExceptLast(1, source(), 3, 4); // NOT OK
testlib.mySinkExceptLast(1, 2, source(), 4); // NOT OK
testlib.mySinkExceptLast(1, 2, 3, source()); // OK
testlib.mySinkIfArityTwo(source()); // OK
testlib.mySinkIfArityTwo(source(), 2); // NOT OK
testlib.mySinkIfArityTwo(1, source()); // OK
testlib.mySinkIfArityTwo(source(), 2, 3); // OK
testlib.mySinkIfArityTwo(1, source(), 3); // OK
testlib.mySinkIfArityTwo(1, 2, source()); // OK
}

View File

@@ -0,0 +1,56 @@
import javascript
import testUtilities.ConsistencyChecking
class Steps extends ModelInput::SummaryModelCsv {
override predicate row(string row) {
// package;type;path;input;output;kind
row =
[
"testlib;;Member[preserveTaint];Argument[0];ReturnValue;taint",
"testlib;;Member[taintIntoCallback];Argument[0];Argument[1..2].Parameter[0];taint",
"testlib;;Member[preserveArgZeroAndTwo];Argument[0,2];ReturnValue;taint",
"testlib;;Member[preserveAllButFirstArgument];Argument[1..];ReturnValue;taint",
"testlib;;Member[preserveAllIfCall].Call;Argument[0..];ReturnValue;taint"
]
}
}
class Sinks extends ModelInput::SinkModelCsv {
override predicate row(string row) {
// package;type;path;kind
row =
[
"testlib;;Member[mySink].Argument[0];test-sink",
"testlib;;Member[mySinkIfCall].Call.Argument[0];test-sink",
"testlib;;Member[mySinkIfNew].NewCall.Argument[0];test-sink",
"testlib;;Member[mySinkLast].Argument[N-1];test-sink",
"testlib;;Member[mySinkSecondLast].Argument[N-2];test-sink",
"testlib;;Member[mySinkTwoLast].Argument[N-1,N-2];test-sink",
"testlib;;Member[mySinkTwoLastRange].Argument[N-2..N-1];test-sink",
"testlib;;Member[mySinkExceptLast].Argument[0..N-2];test-sink",
"testlib;;Member[mySinkIfArityTwo].WithArity[2].Argument[0];test-sink",
]
}
}
class BasicTaintTracking extends TaintTracking::Configuration {
BasicTaintTracking() { this = "BasicTaintTracking" }
override predicate isSource(DataFlow::Node source) {
source.(DataFlow::CallNode).getCalleeName() = "source"
}
override predicate isSink(DataFlow::Node sink) {
sink = any(DataFlow::CallNode call | call.getCalleeName() = "sink").getAnArgument()
or
sink = ModelOutput::getASinkNode("test-sink").getARhs()
}
}
query predicate taintFlow(DataFlow::Node source, DataFlow::Node sink) {
any(BasicTaintTracking tr).hasFlow(source, sink)
}
query predicate isSink(DataFlow::Node node, string kind) {
node = ModelOutput::getASinkNode(kind).getARhs()
}