mirror of
https://github.com/github/codeql.git
synced 2026-04-30 19:26:02 +02:00
Merge pull request #8254 from asgerf/ruby/mad-prototype
Ruby: initial prototype of models-as-data
This commit is contained in:
@@ -266,6 +266,41 @@ module API {
|
||||
/** A node corresponding to the method being invoked at a method call. */
|
||||
class MethodAccessNode extends Node, Impl::MkMethodAccessNode {
|
||||
override string toString() { result = "MethodAccessNode " + tryGetPath(this) }
|
||||
|
||||
/** Gets the call node corresponding to this method access. */
|
||||
DataFlow::CallNode getCallNode() { this = Impl::MkMethodAccessNode(result) }
|
||||
}
|
||||
|
||||
/**
|
||||
* An API entry point.
|
||||
*
|
||||
* By default, API graph nodes are only created for nodes that come from an external
|
||||
* library or escape into an external library. The points where values are cross the boundary
|
||||
* between codebases are called "entry points".
|
||||
*
|
||||
* Anything in the global scope is considered to be an entry point, but
|
||||
* additional entry points may be added by extending this class.
|
||||
*/
|
||||
abstract class EntryPoint extends string {
|
||||
bindingset[this]
|
||||
EntryPoint() { any() }
|
||||
|
||||
/** Gets a data-flow node corresponding to a use-node for this entry point. */
|
||||
DataFlow::LocalSourceNode getAUse() { none() }
|
||||
|
||||
/** Gets a data-flow node corresponding to a def-node for this entry point. */
|
||||
DataFlow::Node getARhs() { none() }
|
||||
|
||||
/** Gets a call corresponding to a method access node for this entry point. */
|
||||
DataFlow::CallNode getACall() { none() }
|
||||
|
||||
/** Gets an API-node for this entry point. */
|
||||
API::Node getANode() { result = root().getASuccessor(Label::entryPoint(this)) }
|
||||
}
|
||||
|
||||
// Ensure all entry points are imported from ApiGraphs.qll
|
||||
private module ImportEntryPoints {
|
||||
private import codeql.ruby.frameworks.data.ModelsAsData
|
||||
}
|
||||
|
||||
/** Gets the root node. */
|
||||
@@ -324,7 +359,7 @@ module API {
|
||||
|
||||
/**
|
||||
* Holds if `ref` is a use of a node that should have an incoming edge from the root
|
||||
* node labeled `lbl` in the API graph.
|
||||
* node labeled `lbl` in the API graph (not including those from API::EntryPoint).
|
||||
*/
|
||||
pragma[nomagic]
|
||||
private predicate useRoot(Label::ApiLabel lbl, DataFlow::Node ref) {
|
||||
@@ -371,6 +406,10 @@ module API {
|
||||
useCandFwd().flowsTo(nd.(DataFlow::CallNode).getReceiver())
|
||||
or
|
||||
parameterStep(_, defCand(), nd)
|
||||
or
|
||||
nd = any(EntryPoint entry).getAUse()
|
||||
or
|
||||
nd = any(EntryPoint entry).getACall()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -416,6 +455,8 @@ module API {
|
||||
private predicate isDef(DataFlow::Node rhs) {
|
||||
// If a call node is relevant as a use-node, treat its arguments as def-nodes
|
||||
argumentStep(_, useCandFwd(), rhs)
|
||||
or
|
||||
rhs = any(EntryPoint entry).getARhs()
|
||||
}
|
||||
|
||||
/** Gets a data flow node that flows to the RHS of a def-node. */
|
||||
@@ -590,6 +631,17 @@ module API {
|
||||
)
|
||||
)
|
||||
)
|
||||
or
|
||||
exists(EntryPoint entry |
|
||||
pred = root() and
|
||||
lbl = Label::entryPoint(entry)
|
||||
|
|
||||
succ = MkDef(entry.getARhs())
|
||||
or
|
||||
succ = MkUse(entry.getAUse())
|
||||
or
|
||||
succ = MkMethodAccessNode(entry.getACall())
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -619,7 +671,8 @@ module API {
|
||||
or
|
||||
any(DataFlowDispatch::ParameterPosition c).isPositional(n)
|
||||
} or
|
||||
MkLabelBlockParameter()
|
||||
MkLabelBlockParameter() or
|
||||
MkLabelEntryPoint(EntryPoint name)
|
||||
}
|
||||
|
||||
/** Provides classes modeling the various edges (labels) in the API graph. */
|
||||
@@ -710,6 +763,18 @@ module API {
|
||||
|
||||
override string toString() { result = "getBlock()" }
|
||||
}
|
||||
|
||||
/** A label from the root node to a custom entry point. */
|
||||
class LabelEntryPoint extends ApiLabel {
|
||||
private API::EntryPoint name;
|
||||
|
||||
LabelEntryPoint() { this = MkLabelEntryPoint(name) }
|
||||
|
||||
override string toString() { result = name }
|
||||
|
||||
/** Gets the name of the entry point. */
|
||||
API::EntryPoint getName() { result = name }
|
||||
}
|
||||
}
|
||||
|
||||
/** Gets the `member` edge label for member `m`. */
|
||||
@@ -735,5 +800,8 @@ module API {
|
||||
|
||||
/** Gets the label representing the block argument/parameter. */
|
||||
LabelBlockParameter blockParameter() { any() }
|
||||
|
||||
/** Gets the label for the edge from the root node to a custom entry point of the given name. */
|
||||
LabelEntryPoint entryPoint(API::EntryPoint name) { result.getName() = name }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
import ruby
|
||||
import codeql.ruby.DataFlow
|
||||
private import codeql.ruby.frameworks.data.ModelsAsData
|
||||
private import codeql.ruby.ApiGraphs
|
||||
private import internal.FlowSummaryImpl as Impl
|
||||
private import internal.DataFlowDispatch
|
||||
private import internal.DataFlowPrivate
|
||||
@@ -165,3 +167,33 @@ private class SummarizedCallableAdapter extends Impl::Public::SummarizedCallable
|
||||
}
|
||||
|
||||
class RequiredSummaryComponentStack = Impl::Public::RequiredSummaryComponentStack;
|
||||
|
||||
private class SummarizedCallableFromModel extends SummarizedCallable {
|
||||
string package;
|
||||
string type;
|
||||
string path;
|
||||
|
||||
SummarizedCallableFromModel() {
|
||||
ModelOutput::relevantSummaryModel(package, type, path, _, _, _) and
|
||||
this = package + ";" + type + ";" + path
|
||||
}
|
||||
|
||||
override Call getACall() {
|
||||
exists(API::MethodAccessNode base |
|
||||
ModelOutput::resolvedSummaryBase(package, type, path, base) and
|
||||
result = base.getCallNode().asExpr().getExpr()
|
||||
)
|
||||
}
|
||||
|
||||
override predicate propagatesFlowExt(string input, string output, boolean preservesValue) {
|
||||
exists(string kind |
|
||||
ModelOutput::relevantSummaryModel(package, type, path, input, output, kind)
|
||||
|
|
||||
kind = "value" and
|
||||
preservesValue = true
|
||||
or
|
||||
kind = "taint" and
|
||||
preservesValue = false
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ private import codeql.ruby.ApiGraphs
|
||||
private import codeql.ruby.Concepts
|
||||
private import codeql.ruby.DataFlow
|
||||
private import codeql.ruby.dataflow.FlowSummary
|
||||
private import codeql.ruby.frameworks.data.ModelsAsData
|
||||
|
||||
/** A call to `ActiveStorage::Filename#sanitized`, considered as a path sanitizer. */
|
||||
class ActiveStorageFilenameSanitizedCall extends Path::PathSanitization::Range, DataFlow::CallNode {
|
||||
@@ -17,43 +18,13 @@ class ActiveStorageFilenameSanitizedCall extends Path::PathSanitization::Range,
|
||||
}
|
||||
}
|
||||
|
||||
/** The taint summary for `ActiveStorage::Filename.new`. */
|
||||
class ActiveStorageFilenameNewSummary extends SummarizedCallable {
|
||||
ActiveStorageFilenameNewSummary() { this = "ActiveStorage::Filename.new" }
|
||||
|
||||
override MethodCall getACall() {
|
||||
result =
|
||||
API::getTopLevelMember("ActiveStorage")
|
||||
.getMember("Filename")
|
||||
.getAnInstantiation()
|
||||
.asExpr()
|
||||
.getExpr()
|
||||
}
|
||||
|
||||
override predicate propagatesFlowExt(string input, string output, boolean preservesValue) {
|
||||
input = "Argument[0]" and
|
||||
output = "ReturnValue" and
|
||||
preservesValue = false
|
||||
}
|
||||
}
|
||||
|
||||
/** The taint summary for `ActiveStorage::Filename#sanitized`. */
|
||||
class ActiveStorageFilenameSanitizedSummary extends SummarizedCallable {
|
||||
ActiveStorageFilenameSanitizedSummary() { this = "ActiveStorage::Filename#sanitized" }
|
||||
|
||||
override MethodCall getACall() {
|
||||
result =
|
||||
API::getTopLevelMember("ActiveStorage")
|
||||
.getMember("Filename")
|
||||
.getInstance()
|
||||
.getAMethodCall("sanitized")
|
||||
.asExpr()
|
||||
.getExpr()
|
||||
}
|
||||
|
||||
override predicate propagatesFlowExt(string input, string output, boolean preservesValue) {
|
||||
input = "Argument[-1]" and
|
||||
output = "ReturnValue" and
|
||||
preservesValue = false
|
||||
/** Taint related to `ActiveStorage::Filename`. */
|
||||
private class Summaries extends ModelInput::SummaryModelCsv {
|
||||
override predicate row(string row) {
|
||||
row =
|
||||
[
|
||||
"activestorage;;Member[ActiveStorage].Member[Filename].Method[new];Argument[0];ReturnValue;taint",
|
||||
"activestorage;;Member[ActiveStorage].Member[Filename].Instance.Method[sanitized];Receiver;ReturnValue;taint",
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,24 +4,16 @@
|
||||
|
||||
private import codeql.ruby.ApiGraphs
|
||||
private import codeql.ruby.dataflow.FlowSummary
|
||||
private import codeql.ruby.frameworks.data.ModelsAsData
|
||||
|
||||
/**
|
||||
* Provides modeling for the `Regexp` class.
|
||||
*/
|
||||
module Regexp {
|
||||
/** A flow summary for `Regexp.escape` and its alias, `Regexp.quote`. */
|
||||
class RegexpEscapeSummary extends SummarizedCallable {
|
||||
RegexpEscapeSummary() { this = "Regexp.escape" }
|
||||
|
||||
override MethodCall getACall() {
|
||||
result =
|
||||
API::getTopLevelMember("Regexp").getAMethodCall(["escape", "quote"]).asExpr().getExpr()
|
||||
}
|
||||
|
||||
override predicate propagatesFlowExt(string input, string output, boolean preservesValue) {
|
||||
input = "Argument[0]" and
|
||||
output = "ReturnValue" and
|
||||
preservesValue = false
|
||||
class RegexpEscapeSummary extends ModelInput::SummaryModelCsv {
|
||||
override predicate row(string row) {
|
||||
row = ";;Member[Regexp].Method[escape,quote];Argument[0];ReturnValue;taint"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
31
ruby/ql/lib/codeql/ruby/frameworks/data/ModelsAsData.qll
Normal file
31
ruby/ql/lib/codeql/ruby/frameworks/data/ModelsAsData.qll
Normal file
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* 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` part of a CSV row should be the name of a Ruby gem, or the empty
|
||||
* string if it's referring to the standard library.
|
||||
*
|
||||
* The `type` part can be one of the following:
|
||||
* - the empty string, referring to the global scope,
|
||||
* - the string `any`, referring to any expression, or
|
||||
* - the name of a type definition from `ModelInput::TypeModelCsv`
|
||||
*/
|
||||
|
||||
private import ruby
|
||||
private import internal.ApiGraphModels as Shared
|
||||
private import internal.ApiGraphModelsSpecific as Specific
|
||||
import Shared::ModelInput as ModelInput
|
||||
import Shared::ModelOutput as ModelOutput
|
||||
private import codeql.ruby.dataflow.RemoteFlowSources
|
||||
|
||||
/**
|
||||
* A remote flow source originating from a CSV source row.
|
||||
*/
|
||||
private class RemoteFlowSourceFromCsv extends RemoteFlowSource::Range {
|
||||
RemoteFlowSourceFromCsv() { this = ModelOutput::getASourceNode("remote").getAnImmediateUse() }
|
||||
|
||||
override string getSourceType() { result = "Remote flow (from model)" }
|
||||
}
|
||||
@@ -0,0 +1,522 @@
|
||||
/**
|
||||
* 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`.
|
||||
*
|
||||
* Every language supports the following tokens:
|
||||
* - 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
|
||||
* - WithArity[n]: match a call with the given arity. May be a range of form `x..y` (inclusive) and/or a comma-separated list.
|
||||
*
|
||||
* The following tokens are common and should be implemented for languages where it makes sense:
|
||||
* - Member[x]: a member named `x`; exactly what a "member" is depends on the language. May be a comma-separated list of names.
|
||||
* - Instance: an instance of a class
|
||||
* - Subclass: a subclass of a class
|
||||
* - ArrayElement: an element of array
|
||||
* - Element: an element of a collection-like object
|
||||
* - MapKey: a key in map-like object
|
||||
* - MapValue: a value in a map-like object
|
||||
* - Awaited: the value from a resolved promise/future-like object
|
||||
*
|
||||
* For the time being, please consult `ApiGraphModelsSpecific.qll` to see which language-specific tokens are currently supported.
|
||||
*
|
||||
* 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 ApiGraphModelsSpecific as Specific
|
||||
|
||||
private class Unit = Specific::Unit;
|
||||
|
||||
private module API = Specific::API;
|
||||
|
||||
private import Specific::AccessPathSyntax
|
||||
|
||||
/** 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
|
||||
|
||||
/**
|
||||
* An empty class, except in specific tests.
|
||||
*
|
||||
* If this is non-empty, all models are parsed even if the package is not
|
||||
* considered relevant for the current database.
|
||||
*/
|
||||
abstract class TestAllModels extends Unit { }
|
||||
|
||||
/**
|
||||
* 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)) }
|
||||
|
||||
/** 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
|
||||
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
|
||||
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
|
||||
row.splitAt(";", 2) = path and
|
||||
row.splitAt(";", 3) = input and
|
||||
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
|
||||
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) {
|
||||
(
|
||||
sourceModel(package, _, _, _) or
|
||||
sinkModel(package, _, _, _) or
|
||||
summaryModel(package, _, _, _, _, _) or
|
||||
typeModel(package, _, _, _, _)
|
||||
) and
|
||||
(
|
||||
Specific::isPackageUsed(package)
|
||||
or
|
||||
exists(TestAllModels t)
|
||||
)
|
||||
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 from a CSV row that should be parsed as an access path. */
|
||||
private class AccessPathRange extends AccessPath::Range {
|
||||
AccessPathRange() {
|
||||
isRelevantFullPath(_, _, this)
|
||||
or
|
||||
exists(string package | isRelevantPackage(package) |
|
||||
summaryModel(package, _, _, this, _, _) or
|
||||
summaryModel(package, _, _, _, this, _)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a successor of `node` in the API graph.
|
||||
*/
|
||||
bindingset[token]
|
||||
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(AccessPath::parseIntUnbounded(token.getAnArgument()))
|
||||
or
|
||||
token.getName() = "ReturnValue" and
|
||||
result = node.getReturn()
|
||||
or
|
||||
// Language-specific tokens
|
||||
result = Specific::getExtraSuccessorFromNode(node, token)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an API-graph successor for the given invocation.
|
||||
*/
|
||||
bindingset[token]
|
||||
API::Node getSuccessorFromInvoke(Specific::InvokeNode invoke, AccessPathToken token) {
|
||||
token.getName() = "Argument" and
|
||||
result =
|
||||
invoke
|
||||
.getParameter(AccessPath::parseIntWithArity(token.getAnArgument(), invoke.getNumArgument()))
|
||||
or
|
||||
token.getName() = "ReturnValue" and
|
||||
result = invoke.getReturn()
|
||||
or
|
||||
// Language-specific tokens
|
||||
result = Specific::getExtraSuccessorFromInvoke(invoke, token)
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if `invoke` invokes a call-site filter given by `token`.
|
||||
*/
|
||||
pragma[inline]
|
||||
private predicate invocationMatchesCallSiteFilter(Specific::InvokeNode invoke, AccessPathToken token) {
|
||||
token.getName() = "WithArity" and
|
||||
invoke.getNumArgument() = AccessPath::parseIntUnbounded(token.getAnArgument())
|
||||
or
|
||||
Specific::invocationMatchesExtraCallSiteFilter(invoke, token)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the API node identified by the first `n` tokens of `path` in the given `(package, type, path)` tuple.
|
||||
*/
|
||||
pragma[nomagic]
|
||||
private API::Node getNodeFromPath(string package, string type, AccessPath path, int n) {
|
||||
isRelevantFullPath(package, type, path) and
|
||||
(
|
||||
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 = Specific::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.
|
||||
*/
|
||||
Specific::InvokeNode getInvocationFromPath(string package, string type, AccessPath path, int n) {
|
||||
result = Specific::getAnInvocationOf(getNodeFromPath(package, type, path, n))
|
||||
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. */
|
||||
Specific::InvokeNode getInvocationFromPath(string package, string type, AccessPath path) {
|
||||
result = getInvocationFromPath(package, type, path, path.getNumToken())
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if `name` is a valid name for an access path token in the identifying access path.
|
||||
*/
|
||||
bindingset[name]
|
||||
predicate isValidTokenNameInIdentifyingAccessPath(string name) {
|
||||
name = ["Argument", "Parameter", "ReturnValue", "WithArity"]
|
||||
or
|
||||
Specific::isExtraValidTokenNameInIdentifyingAccessPath(name)
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if `name` is a valid name for an access path token with no arguments, occuring
|
||||
* in an identifying access path.
|
||||
*/
|
||||
bindingset[name]
|
||||
predicate isValidNoArgumentTokenInIdentifyingAccessPath(string name) {
|
||||
name = "ReturnValue"
|
||||
or
|
||||
Specific::isExtraValidNoArgumentTokenInIdentifyingAccessPath(name)
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if `argument` is a valid argument to an access path token with the given `name`, occurring
|
||||
* in an identifying access path.
|
||||
*/
|
||||
bindingset[name, argument]
|
||||
predicate isValidTokenArgumentInIdentifyingAccessPath(string name, string argument) {
|
||||
name = ["Argument", "Parameter"] and
|
||||
argument.regexpMatch("(N-|-)?\\d+(\\.\\.(N-|-)?\\d+)?")
|
||||
or
|
||||
name = "WithArity" and
|
||||
argument.regexpMatch("\\d+(\\.\\.\\d+)?")
|
||||
or
|
||||
Specific::isExtraValidTokenArgumentInIdentifyingAccessPath(name, argument)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 relevant CSV summary exists for these parameters.
|
||||
*/
|
||||
predicate relevantSummaryModel(
|
||||
string package, string type, string path, string input, string output, string kind
|
||||
) {
|
||||
isRelevantPackage(package) and
|
||||
summaryModel(package, type, path, input, output, kind)
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if a `baseNode` is an invocation identified by the `package,type,path` part of a summary row.
|
||||
*/
|
||||
predicate resolvedSummaryBase(
|
||||
string package, string type, string path, Specific::InvokeNode baseNode
|
||||
) {
|
||||
summaryModel(package, type, path, _, _, _) and
|
||||
baseNode = getInvocationFromPath(package, type, path)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an error message relating to an invalid CSV row in a model.
|
||||
*/
|
||||
string getAWarning() {
|
||||
// Check number of columns
|
||||
exists(string row, string kind, int expectedArity, int actualArity |
|
||||
any(SourceModelCsv csv).row(row) and kind = "source" and expectedArity = 4
|
||||
or
|
||||
any(SinkModelCsv csv).row(row) and kind = "sink" and expectedArity = 4
|
||||
or
|
||||
any(SummaryModelCsv csv).row(row) and kind = "summary" and expectedArity = 6
|
||||
or
|
||||
any(TypeModelCsv csv).row(row) and kind = "type" and expectedArity = 5
|
||||
|
|
||||
actualArity = count(row.indexOf(";")) + 1 and
|
||||
actualArity != expectedArity and
|
||||
result =
|
||||
"CSV " + kind + " row should have " + expectedArity + " columns but has " + actualArity +
|
||||
": " + row
|
||||
)
|
||||
or
|
||||
// Check names and arguments of access path tokens
|
||||
exists(AccessPath path, AccessPathToken token |
|
||||
isRelevantFullPath(_, _, path) and
|
||||
token = path.getToken(_)
|
||||
|
|
||||
not isValidTokenNameInIdentifyingAccessPath(token.getName()) and
|
||||
result = "Invalid token name '" + token.getName() + "' in access path: " + path
|
||||
or
|
||||
isValidTokenNameInIdentifyingAccessPath(token.getName()) and
|
||||
exists(string argument |
|
||||
argument = token.getAnArgument() and
|
||||
not isValidTokenArgumentInIdentifyingAccessPath(token.getName(), argument) and
|
||||
result =
|
||||
"Invalid argument '" + argument + "' in token '" + token + "' in access path: " + path
|
||||
)
|
||||
or
|
||||
isValidTokenNameInIdentifyingAccessPath(token.getName()) and
|
||||
token.getNumArgument() = 0 and
|
||||
not isValidNoArgumentTokenInIdentifyingAccessPath(token.getName()) and
|
||||
result = "Invalid token '" + token + "' is missing its arguments, in access path: " + path
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
/**
|
||||
* Contains the language-specific part of the models-as-data implementation found in `ApiGraphModels.qll`.
|
||||
*
|
||||
* It must export the following members:
|
||||
* ```ql
|
||||
* class Unit // a unit type
|
||||
* class InvokeNode // a type representing an invocation connected to the API graph
|
||||
* 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(InvokeNode node, AccessPathToken token)
|
||||
* predicate invocationMatchesExtraCallSiteFilter(InvokeNode invoke, AccessPathToken token)
|
||||
* InvokeNode getAnInvocationOf(API::Node node)
|
||||
* ```
|
||||
*/
|
||||
|
||||
private import ruby
|
||||
private import codeql.ruby.DataFlow
|
||||
private import codeql.ruby.dataflow.internal.DataFlowPrivate as DataFlowPrivate
|
||||
private import ApiGraphModels
|
||||
|
||||
class Unit = DataFlowPrivate::Unit;
|
||||
|
||||
// Re-export libraries needed by ApiGraphModels.qll
|
||||
import codeql.ruby.ApiGraphs
|
||||
import codeql.ruby.dataflow.internal.AccessPathSyntax as AccessPathSyntax
|
||||
private import AccessPathSyntax
|
||||
|
||||
/**
|
||||
* Holds if models describing `package` may be relevant for the analysis of this database.
|
||||
*
|
||||
* In the context of Ruby, this is the name of a Ruby gem.
|
||||
*/
|
||||
bindingset[package]
|
||||
predicate isPackageUsed(string package) {
|
||||
// For now everything is modelled as an access path starting at any top-level, so the package name has no effect.
|
||||
//
|
||||
// We allow an arbitrary package name so that the model can record the name of the package in case it's needed in the future.
|
||||
//
|
||||
// In principle we should consider a package to be "used" if there is a transitive dependency on it, but we can only
|
||||
// reliably see the direct dependencies.
|
||||
//
|
||||
// In practice, packages try to use unique top-level module names, which mitigates the precision loss of not checking
|
||||
// the package name.
|
||||
any()
|
||||
}
|
||||
|
||||
/** Gets a Ruby-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) {
|
||||
isRelevantFullPath(package, type, path) and
|
||||
exists(package) and // Allow any package name, see `isPackageUsed`.
|
||||
type = "" and
|
||||
n = 0 and
|
||||
result = API::root()
|
||||
or
|
||||
// A row of form `;any;Method[foo]` should match any method named `foo`.
|
||||
exists(package) and
|
||||
type = "any" and
|
||||
n = 1 and
|
||||
exists(EntryPointFromAnyType entry |
|
||||
methodMatchedByName(path, entry.getName()) and
|
||||
result = entry.getANode()
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if `path` occurs in a CSV row with type `any`, meaning it can start
|
||||
* matching anywhere, and the path begins with `Method[methodName]`.
|
||||
*/
|
||||
private predicate methodMatchedByName(AccessPath path, string methodName) {
|
||||
isRelevantFullPath(_, "any", path) and
|
||||
exists(AccessPathToken token |
|
||||
token = path.getToken(0) and
|
||||
token.getName() = "Method" and
|
||||
methodName = token.getAnArgument()
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* An API graph entry point corresponding to a method name such as `foo` in `;any;Method[foo]`.
|
||||
*
|
||||
* This ensures that the API graph rooted in that method call is materialized.
|
||||
*/
|
||||
private class EntryPointFromAnyType extends API::EntryPoint {
|
||||
string name;
|
||||
|
||||
EntryPointFromAnyType() { this = "AnyMethod[" + name + "]" and methodMatchedByName(_, name) }
|
||||
|
||||
override DataFlow::CallNode getACall() { result.getMethodName() = name }
|
||||
|
||||
string getName() { result = name }
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a Ruby-specific API graph successor of `node` reachable by resolving `token`.
|
||||
*/
|
||||
bindingset[token]
|
||||
API::Node getExtraSuccessorFromNode(API::Node node, AccessPathToken token) {
|
||||
token.getName() = "Member" and
|
||||
result = node.getMember(token.getAnArgument())
|
||||
or
|
||||
token.getName() = "Method" and
|
||||
result = node.getMethod(token.getAnArgument())
|
||||
or
|
||||
token.getName() = "Instance" and
|
||||
result = node.getInstance()
|
||||
or
|
||||
token.getName() = "BlockArgument" and
|
||||
result = node.getBlock()
|
||||
// Note: The "ArrayElement" token is not implemented yet, as it ultimately requires type-tracking and
|
||||
// API graphs to be aware of the steps involving ArrayElement contributed by the standard library model.
|
||||
// Type-tracking cannot summarize function calls on its own, so it doesn't benefit from synthesized callables.
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a Ruby-specific API graph successor of `node` reachable by resolving `token`.
|
||||
*/
|
||||
bindingset[token]
|
||||
API::Node getExtraSuccessorFromInvoke(InvokeNode node, AccessPathToken token) { none() }
|
||||
|
||||
/**
|
||||
* Holds if `invoke` matches the Ruby-specific call site filter in `token`.
|
||||
*/
|
||||
bindingset[token]
|
||||
predicate invocationMatchesExtraCallSiteFilter(InvokeNode invoke, AccessPathToken token) {
|
||||
token.getName() = "WithBlock" and
|
||||
exists(invoke.getBlock())
|
||||
or
|
||||
token.getName() = "WithoutBlock" and
|
||||
not exists(invoke.getBlock())
|
||||
}
|
||||
|
||||
/** An API graph node representing a method call. */
|
||||
class InvokeNode extends API::MethodAccessNode {
|
||||
/** Gets the number of arguments to the call. */
|
||||
int getNumArgument() { result = getCallNode().getNumberOfArguments() }
|
||||
}
|
||||
|
||||
/** Gets the `InvokeNode` corresponding to a specific invocation of `node`. */
|
||||
InvokeNode getAnInvocationOf(API::Node node) { result = node }
|
||||
|
||||
/**
|
||||
* Holds if `name` is a valid name for an access path token in the identifying access path.
|
||||
*/
|
||||
bindingset[name]
|
||||
predicate isExtraValidTokenNameInIdentifyingAccessPath(string name) {
|
||||
name = ["Member", "Method", "Instance", "WithBlock", "WithoutBlock", "BlockArgument"]
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if `name` is a valid name for an access path token with no arguments, occuring
|
||||
* in an identifying access path.
|
||||
*/
|
||||
predicate isExtraValidNoArgumentTokenInIdentifyingAccessPath(string name) {
|
||||
name = ["Instance", "WithBlock", "WithoutBlock", "BlockArgument"]
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if `argument` is a valid argument to an access path token with the given `name`, occurring
|
||||
* in an identifying access path.
|
||||
*/
|
||||
bindingset[name, argument]
|
||||
predicate isExtraValidTokenArgumentInIdentifyingAccessPath(string name, string argument) {
|
||||
name = ["Member", "Method"] and
|
||||
exists(argument)
|
||||
}
|
||||
@@ -62,3 +62,9 @@ M1::C1.new.m #$ use=getMember("M1").getMember("C1").getMethod("new").getReturn()
|
||||
M2::C3.new.m #$ use=getMember("M2").getMember("C3").getMethod("new").getReturn().getMethod("m").getReturn()
|
||||
|
||||
Foo.foo(a,b:c) #$ use=getMember("Foo").getMethod("foo").getReturn() def=getMember("Foo").getMethod("foo").getParameter(0) def=getMember("Foo").getMethod("foo").getKeywordParameter("b")
|
||||
|
||||
def userDefinedFunction(x, y)
|
||||
x.noApiGraph(y)
|
||||
x.customEntryPointCall(y) #$ call=CustomEntryPointCall use=CustomEntryPointCall.getReturn() rhs=CustomEntryPointCall.getParameter(0)
|
||||
x.customEntryPointUse(y) #$ use=CustomEntryPointUse
|
||||
end
|
||||
|
||||
@@ -3,6 +3,20 @@ import codeql.ruby.DataFlow
|
||||
import TestUtilities.InlineExpectationsTest
|
||||
import codeql.ruby.ApiGraphs
|
||||
|
||||
class CustomEntryPointCall extends API::EntryPoint {
|
||||
CustomEntryPointCall() { this = "CustomEntryPointCall" }
|
||||
|
||||
override DataFlow::CallNode getACall() { result.getMethodName() = "customEntryPointCall" }
|
||||
}
|
||||
|
||||
class CustomEntryPointUse extends API::EntryPoint {
|
||||
CustomEntryPointUse() { this = "CustomEntryPointUse" }
|
||||
|
||||
override DataFlow::LocalSourceNode getAUse() {
|
||||
result.(DataFlow::CallNode).getMethodName() = "customEntryPointUse"
|
||||
}
|
||||
}
|
||||
|
||||
class ApiUseTest extends InlineExpectationsTest {
|
||||
ApiUseTest() { this = "ApiUseTest" }
|
||||
|
||||
@@ -16,6 +30,9 @@ class ApiUseTest extends InlineExpectationsTest {
|
||||
or
|
||||
tag = "def" and
|
||||
n = a.getARhs()
|
||||
or
|
||||
tag = "call" and
|
||||
n = a.(API::MethodAccessNode).getCallNode()
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,14 @@ edges
|
||||
| summaries.rb:1:11:1:26 | call to identity : | summaries.rb:2:6:2:12 | tainted |
|
||||
| summaries.rb:1:11:1:26 | call to identity : | summaries.rb:4:24:4:30 | tainted : |
|
||||
| summaries.rb:1:11:1:26 | call to identity : | summaries.rb:16:36:16:42 | tainted : |
|
||||
| summaries.rb:1:11:1:26 | call to identity : | summaries.rb:20:25:20:31 | tainted : |
|
||||
| summaries.rb:1:11:1:26 | call to identity : | summaries.rb:26:31:26:37 | tainted : |
|
||||
| summaries.rb:1:11:1:26 | call to identity : | summaries.rb:30:24:30:30 | tainted : |
|
||||
| summaries.rb:1:11:1:26 | call to identity : | summaries.rb:31:27:31:33 | tainted : |
|
||||
| summaries.rb:1:11:1:26 | call to identity : | summaries.rb:34:16:34:22 | tainted |
|
||||
| summaries.rb:1:11:1:26 | call to identity : | summaries.rb:35:16:35:22 | tainted |
|
||||
| summaries.rb:1:11:1:26 | call to identity : | summaries.rb:36:21:36:27 | tainted |
|
||||
| summaries.rb:1:11:1:26 | call to identity : | summaries.rb:37:36:37:42 | tainted |
|
||||
| summaries.rb:1:20:1:26 | "taint" : | summaries.rb:1:11:1:26 | call to identity : |
|
||||
| summaries.rb:4:12:7:3 | call to apply_block : | summaries.rb:9:6:9:13 | tainted2 |
|
||||
| summaries.rb:4:24:4:30 | tainted : | summaries.rb:4:12:7:3 | call to apply_block : |
|
||||
@@ -11,6 +19,19 @@ edges
|
||||
| summaries.rb:16:12:16:43 | call to apply_lambda : | summaries.rb:18:6:18:13 | tainted3 |
|
||||
| summaries.rb:16:36:16:42 | tainted : | summaries.rb:11:17:11:17 | x : |
|
||||
| summaries.rb:16:36:16:42 | tainted : | summaries.rb:16:12:16:43 | call to apply_lambda : |
|
||||
| summaries.rb:20:12:20:32 | call to firstArg : | summaries.rb:21:6:21:13 | tainted4 |
|
||||
| summaries.rb:20:25:20:31 | tainted : | summaries.rb:20:12:20:32 | call to firstArg : |
|
||||
| summaries.rb:26:12:26:38 | call to secondArg : | summaries.rb:27:6:27:13 | tainted5 |
|
||||
| summaries.rb:26:31:26:37 | tainted : | summaries.rb:26:12:26:38 | call to secondArg : |
|
||||
| summaries.rb:30:24:30:30 | tainted : | summaries.rb:30:6:30:42 | call to onlyWithBlock |
|
||||
| summaries.rb:31:27:31:33 | tainted : | summaries.rb:31:6:31:34 | call to onlyWithoutBlock |
|
||||
| summaries.rb:40:7:40:13 | "taint" : | summaries.rb:41:24:41:24 | t : |
|
||||
| summaries.rb:40:7:40:13 | "taint" : | summaries.rb:42:24:42:24 | t : |
|
||||
| summaries.rb:40:7:40:13 | "taint" : | summaries.rb:44:8:44:8 | t : |
|
||||
| summaries.rb:41:24:41:24 | t : | summaries.rb:41:8:41:25 | call to matchedByName |
|
||||
| summaries.rb:42:24:42:24 | t : | summaries.rb:42:8:42:25 | call to matchedByName |
|
||||
| summaries.rb:44:8:44:8 | t : | summaries.rb:44:8:44:27 | call to matchedByNameRcv |
|
||||
| summaries.rb:48:24:48:30 | "taint" : | summaries.rb:48:8:48:31 | call to preserveTaint |
|
||||
nodes
|
||||
| summaries.rb:1:11:1:26 | call to identity : | semmle.label | call to identity : |
|
||||
| summaries.rb:1:20:1:26 | "taint" : | semmle.label | "taint" : |
|
||||
@@ -25,6 +46,29 @@ nodes
|
||||
| summaries.rb:16:12:16:43 | call to apply_lambda : | semmle.label | call to apply_lambda : |
|
||||
| summaries.rb:16:36:16:42 | tainted : | semmle.label | tainted : |
|
||||
| summaries.rb:18:6:18:13 | tainted3 | semmle.label | tainted3 |
|
||||
| summaries.rb:20:12:20:32 | call to firstArg : | semmle.label | call to firstArg : |
|
||||
| summaries.rb:20:25:20:31 | tainted : | semmle.label | tainted : |
|
||||
| summaries.rb:21:6:21:13 | tainted4 | semmle.label | tainted4 |
|
||||
| summaries.rb:26:12:26:38 | call to secondArg : | semmle.label | call to secondArg : |
|
||||
| summaries.rb:26:31:26:37 | tainted : | semmle.label | tainted : |
|
||||
| summaries.rb:27:6:27:13 | tainted5 | semmle.label | tainted5 |
|
||||
| summaries.rb:30:6:30:42 | call to onlyWithBlock | semmle.label | call to onlyWithBlock |
|
||||
| summaries.rb:30:24:30:30 | tainted : | semmle.label | tainted : |
|
||||
| summaries.rb:31:6:31:34 | call to onlyWithoutBlock | semmle.label | call to onlyWithoutBlock |
|
||||
| summaries.rb:31:27:31:33 | tainted : | semmle.label | tainted : |
|
||||
| summaries.rb:34:16:34:22 | tainted | semmle.label | tainted |
|
||||
| summaries.rb:35:16:35:22 | tainted | semmle.label | tainted |
|
||||
| summaries.rb:36:21:36:27 | tainted | semmle.label | tainted |
|
||||
| summaries.rb:37:36:37:42 | tainted | semmle.label | tainted |
|
||||
| summaries.rb:40:7:40:13 | "taint" : | semmle.label | "taint" : |
|
||||
| summaries.rb:41:8:41:25 | call to matchedByName | semmle.label | call to matchedByName |
|
||||
| summaries.rb:41:24:41:24 | t : | semmle.label | t : |
|
||||
| summaries.rb:42:8:42:25 | call to matchedByName | semmle.label | call to matchedByName |
|
||||
| summaries.rb:42:24:42:24 | t : | semmle.label | t : |
|
||||
| summaries.rb:44:8:44:8 | t : | semmle.label | t : |
|
||||
| summaries.rb:44:8:44:27 | call to matchedByNameRcv | semmle.label | call to matchedByNameRcv |
|
||||
| summaries.rb:48:8:48:31 | call to preserveTaint | semmle.label | call to preserveTaint |
|
||||
| summaries.rb:48:24:48:30 | "taint" : | semmle.label | "taint" : |
|
||||
subpaths
|
||||
invalidSpecComponent
|
||||
invalidOutputSpecComponent
|
||||
@@ -34,3 +78,23 @@ invalidOutputSpecComponent
|
||||
| summaries.rb:9:6:9:13 | tainted2 | summaries.rb:1:20:1:26 | "taint" : | summaries.rb:9:6:9:13 | tainted2 | $@ | summaries.rb:1:20:1:26 | "taint" : | "taint" : |
|
||||
| summaries.rb:12:8:12:8 | x | summaries.rb:1:20:1:26 | "taint" : | summaries.rb:12:8:12:8 | x | $@ | summaries.rb:1:20:1:26 | "taint" : | "taint" : |
|
||||
| summaries.rb:18:6:18:13 | tainted3 | summaries.rb:1:20:1:26 | "taint" : | summaries.rb:18:6:18:13 | tainted3 | $@ | summaries.rb:1:20:1:26 | "taint" : | "taint" : |
|
||||
| summaries.rb:21:6:21:13 | tainted4 | summaries.rb:1:20:1:26 | "taint" : | summaries.rb:21:6:21:13 | tainted4 | $@ | summaries.rb:1:20:1:26 | "taint" : | "taint" : |
|
||||
| summaries.rb:27:6:27:13 | tainted5 | summaries.rb:1:20:1:26 | "taint" : | summaries.rb:27:6:27:13 | tainted5 | $@ | summaries.rb:1:20:1:26 | "taint" : | "taint" : |
|
||||
| summaries.rb:30:6:30:42 | call to onlyWithBlock | summaries.rb:1:20:1:26 | "taint" : | summaries.rb:30:6:30:42 | call to onlyWithBlock | $@ | summaries.rb:1:20:1:26 | "taint" : | "taint" : |
|
||||
| summaries.rb:31:6:31:34 | call to onlyWithoutBlock | summaries.rb:1:20:1:26 | "taint" : | summaries.rb:31:6:31:34 | call to onlyWithoutBlock | $@ | summaries.rb:1:20:1:26 | "taint" : | "taint" : |
|
||||
| summaries.rb:34:16:34:22 | tainted | summaries.rb:1:20:1:26 | "taint" : | summaries.rb:34:16:34:22 | tainted | $@ | summaries.rb:1:20:1:26 | "taint" : | "taint" : |
|
||||
| summaries.rb:35:16:35:22 | tainted | summaries.rb:1:20:1:26 | "taint" : | summaries.rb:35:16:35:22 | tainted | $@ | summaries.rb:1:20:1:26 | "taint" : | "taint" : |
|
||||
| summaries.rb:36:21:36:27 | tainted | summaries.rb:1:20:1:26 | "taint" : | summaries.rb:36:21:36:27 | tainted | $@ | summaries.rb:1:20:1:26 | "taint" : | "taint" : |
|
||||
| summaries.rb:37:36:37:42 | tainted | summaries.rb:1:20:1:26 | "taint" : | summaries.rb:37:36:37:42 | tainted | $@ | summaries.rb:1:20:1:26 | "taint" : | "taint" : |
|
||||
| summaries.rb:41:8:41:25 | call to matchedByName | summaries.rb:40:7:40:13 | "taint" : | summaries.rb:41:8:41:25 | call to matchedByName | $@ | summaries.rb:40:7:40:13 | "taint" : | "taint" : |
|
||||
| summaries.rb:42:8:42:25 | call to matchedByName | summaries.rb:40:7:40:13 | "taint" : | summaries.rb:42:8:42:25 | call to matchedByName | $@ | summaries.rb:40:7:40:13 | "taint" : | "taint" : |
|
||||
| summaries.rb:44:8:44:27 | call to matchedByNameRcv | summaries.rb:40:7:40:13 | "taint" : | summaries.rb:44:8:44:27 | call to matchedByNameRcv | $@ | summaries.rb:40:7:40:13 | "taint" : | "taint" : |
|
||||
| summaries.rb:48:8:48:31 | call to preserveTaint | summaries.rb:48:24:48:30 | "taint" : | summaries.rb:48:8:48:31 | call to preserveTaint | $@ | summaries.rb:48:24:48:30 | "taint" : | "taint" : |
|
||||
warning
|
||||
| CSV type row should have 5 columns but has 2: test;TooFewColumns |
|
||||
| CSV type row should have 5 columns but has 8: test;TooManyColumns;;;Member[Foo].Instance;too;many;columns |
|
||||
| Invalid argument '0-1' in token 'Argument[0-1]' in access path: Method[foo].Argument[0-1] |
|
||||
| Invalid argument '*' in token 'Argument[*]' in access path: Method[foo].Argument[*] |
|
||||
| Invalid token 'Argument' is missing its arguments, in access path: Method[foo].Argument |
|
||||
| Invalid token 'Member' is missing its arguments, in access path: Method[foo].Member |
|
||||
| Invalid token name 'Arg' in access path: Method[foo].Arg[0] |
|
||||
|
||||
@@ -8,12 +8,15 @@ import DataFlow::PathGraph
|
||||
import codeql.ruby.TaintTracking
|
||||
import codeql.ruby.dataflow.internal.FlowSummaryImpl
|
||||
import codeql.ruby.dataflow.internal.AccessPathSyntax
|
||||
import codeql.ruby.frameworks.data.ModelsAsData
|
||||
|
||||
query predicate invalidSpecComponent(SummarizedCallable sc, string s, string c) {
|
||||
(sc.propagatesFlowExt(s, _, _) or sc.propagatesFlowExt(_, s, _)) and
|
||||
Private::External::invalidSpecComponent(s, c)
|
||||
}
|
||||
|
||||
query predicate warning = ModelOutput::getAWarning/0;
|
||||
|
||||
query predicate invalidOutputSpecComponent(SummarizedCallable sc, AccessPath s, AccessPathToken c) {
|
||||
sc.propagatesFlowExt(_, s, _) and
|
||||
c = s.getToken(_) and
|
||||
@@ -64,6 +67,51 @@ private class SummarizedCallableApplyLambda extends SummarizedCallable {
|
||||
}
|
||||
}
|
||||
|
||||
private class StepsFromModel extends ModelInput::SummaryModelCsv {
|
||||
override predicate row(string row) {
|
||||
row =
|
||||
[
|
||||
";;Member[Foo].Method[firstArg];Argument[0];ReturnValue;taint",
|
||||
";;Member[Foo].Method[secondArg];Argument[1];ReturnValue;taint",
|
||||
";;Member[Foo].Method[onlyWithoutBlock].WithoutBlock;Argument[0];ReturnValue;taint",
|
||||
";;Member[Foo].Method[onlyWithBlock].WithBlock;Argument[0];ReturnValue;taint",
|
||||
";;Member[Foo].Method[blockArg].BlockArgument.Parameter[0].Method[preserveTaint];Argument[0];ReturnValue;taint",
|
||||
";any;Method[matchedByName];Argument[0];ReturnValue;taint",
|
||||
";any;Method[matchedByNameRcv];Receiver;ReturnValue;taint",
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
private class TypeFromModel extends ModelInput::TypeModelCsv {
|
||||
override predicate row(string row) {
|
||||
row =
|
||||
[
|
||||
"test;FooOrBar;;;Member[Foo].Instance", //
|
||||
"test;FooOrBar;;;Member[Bar].Instance", //
|
||||
"test;FooOrBar;test;FooOrBar;Method[next].ReturnValue",
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
private class InvalidTypeModel extends ModelInput::TypeModelCsv {
|
||||
override predicate row(string row) {
|
||||
row =
|
||||
[
|
||||
"test;TooManyColumns;;;Member[Foo].Instance;too;many;columns", //
|
||||
"test;TooFewColumns", //
|
||||
"test;X;test;Y;Method[foo].Arg[0]", //
|
||||
"test;X;test;Y;Method[foo].Argument[0-1]", //
|
||||
"test;X;test;Y;Method[foo].Argument[*]", //
|
||||
"test;X;test;Y;Method[foo].Argument", //
|
||||
"test;X;test;Y;Method[foo].Member", //
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
private class SinkFromModel extends ModelInput::SinkModelCsv {
|
||||
override predicate row(string row) { row = "test;FooOrBar;Method[method].Argument[0];test-sink" }
|
||||
}
|
||||
|
||||
class Conf extends TaintTracking::Configuration {
|
||||
Conf() { this = "FlowSummaries" }
|
||||
|
||||
@@ -76,6 +124,8 @@ class Conf extends TaintTracking::Configuration {
|
||||
mc.getMethodName() = "sink" and
|
||||
mc.getAnArgument() = sink.asExpr().getExpr()
|
||||
)
|
||||
or
|
||||
sink = ModelOutput::getASinkNode("test-sink").getARhs()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,3 +16,34 @@ my_lambda = -> (x) {
|
||||
tainted3 = apply_lambda(my_lambda, tainted)
|
||||
|
||||
sink(tainted3)
|
||||
|
||||
tainted4 = Foo.firstArg(tainted)
|
||||
sink(tainted4)
|
||||
|
||||
notTainted = Foo.firstArg(nil, tainted))
|
||||
sink(notTainted)
|
||||
|
||||
tainted5 = Foo.secondArg(nil, tainted)
|
||||
sink(tainted5)
|
||||
|
||||
sink(Foo.onlyWithBlock(tainted))
|
||||
sink(Foo.onlyWithBlock(tainted) do |x| end)
|
||||
sink(Foo.onlyWithoutBlock(tainted))
|
||||
sink(Foo.onlyWithoutBlock(tainted) do |x| end)
|
||||
|
||||
Foo.new.method(tainted)
|
||||
Bar.new.method(tainted)
|
||||
Bar.new.next.method(tainted)
|
||||
Bar.new.next.next.next.next.method(tainted)
|
||||
|
||||
def userDefinedFunction(x, y)
|
||||
t = "taint"
|
||||
sink(x.matchedByName(t))
|
||||
sink(y.matchedByName(t))
|
||||
sink(x.unmatchedName(t))
|
||||
sink(t.matchedByNameRcv())
|
||||
end
|
||||
|
||||
Foo.blockArg do |x|
|
||||
sink(x.preserveTaint("taint"))
|
||||
end
|
||||
|
||||
@@ -22,6 +22,9 @@ edges
|
||||
| tainted_path.rb:47:12:47:63 | call to join : | tainted_path.rb:48:26:48:29 | path |
|
||||
| tainted_path.rb:47:43:47:48 | call to params : | tainted_path.rb:47:43:47:55 | ...[...] : |
|
||||
| tainted_path.rb:47:43:47:55 | ...[...] : | tainted_path.rb:47:12:47:63 | call to join : |
|
||||
| tainted_path.rb:59:12:59:53 | call to new : | tainted_path.rb:60:26:60:29 | path |
|
||||
| tainted_path.rb:59:40:59:45 | call to params : | tainted_path.rb:59:40:59:52 | ...[...] : |
|
||||
| tainted_path.rb:59:40:59:52 | ...[...] : | tainted_path.rb:59:12:59:53 | call to new : |
|
||||
nodes
|
||||
| tainted_path.rb:4:12:4:17 | call to params : | semmle.label | call to params : |
|
||||
| tainted_path.rb:4:12:4:24 | ...[...] : | semmle.label | ...[...] : |
|
||||
@@ -54,6 +57,10 @@ nodes
|
||||
| tainted_path.rb:47:43:47:48 | call to params : | semmle.label | call to params : |
|
||||
| tainted_path.rb:47:43:47:55 | ...[...] : | semmle.label | ...[...] : |
|
||||
| tainted_path.rb:48:26:48:29 | path | semmle.label | path |
|
||||
| tainted_path.rb:59:12:59:53 | call to new : | semmle.label | call to new : |
|
||||
| tainted_path.rb:59:40:59:45 | call to params : | semmle.label | call to params : |
|
||||
| tainted_path.rb:59:40:59:52 | ...[...] : | semmle.label | ...[...] : |
|
||||
| tainted_path.rb:60:26:60:29 | path | semmle.label | path |
|
||||
subpaths
|
||||
#select
|
||||
| tainted_path.rb:5:26:5:29 | path | tainted_path.rb:4:12:4:17 | call to params : | tainted_path.rb:5:26:5:29 | path | This path depends on $@. | tainted_path.rb:4:12:4:17 | call to params | a user-provided value |
|
||||
@@ -64,3 +71,4 @@ subpaths
|
||||
| tainted_path.rb:35:26:35:29 | path | tainted_path.rb:34:29:34:34 | call to params : | tainted_path.rb:35:26:35:29 | path | This path depends on $@. | tainted_path.rb:34:29:34:34 | call to params | a user-provided value |
|
||||
| tainted_path.rb:41:26:41:29 | path | tainted_path.rb:40:26:40:31 | call to params : | tainted_path.rb:41:26:41:29 | path | This path depends on $@. | tainted_path.rb:40:26:40:31 | call to params | a user-provided value |
|
||||
| tainted_path.rb:48:26:48:29 | path | tainted_path.rb:47:43:47:48 | call to params : | tainted_path.rb:48:26:48:29 | path | This path depends on $@. | tainted_path.rb:47:43:47:48 | call to params | a user-provided value |
|
||||
| tainted_path.rb:60:26:60:29 | path | tainted_path.rb:59:40:59:45 | call to params : | tainted_path.rb:60:26:60:29 | path | This path depends on $@. | tainted_path.rb:59:40:59:45 | call to params | a user-provided value |
|
||||
|
||||
@@ -54,8 +54,14 @@ class FooController < ActionController::Base
|
||||
@content = File.read path
|
||||
end
|
||||
|
||||
# GOOD - explicitly sanitized
|
||||
# BAD
|
||||
def route9
|
||||
path = ActiveStorage::Filename.new(params[:path])
|
||||
@content = File.read path
|
||||
end
|
||||
|
||||
# GOOD - explicitly sanitized
|
||||
def route10
|
||||
path = ActiveStorage::Filename.new(params[:path]).sanitized
|
||||
@content = File.read path
|
||||
end
|
||||
|
||||
@@ -5,6 +5,8 @@ edges
|
||||
| CodeInjection.rb:3:12:3:24 | ...[...] : | CodeInjection.rb:21:21:21:24 | code |
|
||||
| CodeInjection.rb:3:12:3:24 | ...[...] : | CodeInjection.rb:27:15:27:18 | code |
|
||||
| CodeInjection.rb:3:12:3:24 | ...[...] : | CodeInjection.rb:30:19:30:22 | code |
|
||||
| CodeInjection.rb:3:12:3:24 | ...[...] : | CodeInjection.rb:36:24:36:27 | code : |
|
||||
| CodeInjection.rb:36:24:36:27 | code : | CodeInjection.rb:36:10:36:28 | call to escape |
|
||||
nodes
|
||||
| CodeInjection.rb:3:12:3:17 | call to params : | semmle.label | call to params : |
|
||||
| CodeInjection.rb:3:12:3:24 | ...[...] : | semmle.label | ...[...] : |
|
||||
@@ -14,6 +16,8 @@ nodes
|
||||
| CodeInjection.rb:21:21:21:24 | code | semmle.label | code |
|
||||
| CodeInjection.rb:27:15:27:18 | code | semmle.label | code |
|
||||
| CodeInjection.rb:30:19:30:22 | code | semmle.label | code |
|
||||
| CodeInjection.rb:36:10:36:28 | call to escape | semmle.label | call to escape |
|
||||
| CodeInjection.rb:36:24:36:27 | code : | semmle.label | code : |
|
||||
subpaths
|
||||
#select
|
||||
| CodeInjection.rb:6:10:6:13 | code | CodeInjection.rb:3:12:3:17 | call to params : | CodeInjection.rb:6:10:6:13 | code | This code execution depends on $@. | CodeInjection.rb:3:12:3:17 | call to params | a user-provided value |
|
||||
@@ -22,3 +26,4 @@ subpaths
|
||||
| CodeInjection.rb:21:21:21:24 | code | CodeInjection.rb:3:12:3:17 | call to params : | CodeInjection.rb:21:21:21:24 | code | This code execution depends on $@. | CodeInjection.rb:3:12:3:17 | call to params | a user-provided value |
|
||||
| CodeInjection.rb:27:15:27:18 | code | CodeInjection.rb:3:12:3:17 | call to params : | CodeInjection.rb:27:15:27:18 | code | This code execution depends on $@. | CodeInjection.rb:3:12:3:17 | call to params | a user-provided value |
|
||||
| CodeInjection.rb:30:19:30:22 | code | CodeInjection.rb:3:12:3:17 | call to params : | CodeInjection.rb:30:19:30:22 | code | This code execution depends on $@. | CodeInjection.rb:3:12:3:17 | call to params | a user-provided value |
|
||||
| CodeInjection.rb:36:10:36:28 | call to escape | CodeInjection.rb:3:12:3:17 | call to params : | CodeInjection.rb:36:10:36:28 | call to escape | This code execution depends on $@. | CodeInjection.rb:3:12:3:17 | call to params | a user-provided value |
|
||||
|
||||
@@ -31,6 +31,9 @@ class UsersController < ActionController::Base
|
||||
|
||||
# GOOD
|
||||
Bar.const_get(code)
|
||||
|
||||
# BAD
|
||||
eval(Regexp.escape(code))
|
||||
end
|
||||
|
||||
def update
|
||||
|
||||
Reference in New Issue
Block a user