Merge branch 'main' into rc/3.7

This commit is contained in:
Andrew Eisenberg
2022-09-20 08:33:58 -07:00
2309 changed files with 133758 additions and 43219 deletions

BIN
ruby/Cargo.lock generated

Binary file not shown.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,28 @@
class AstNode extends @ruby_ast_node_parent {
string toString() { none() }
}
class Location extends @location {
string toString() { none() }
}
class Wrapper = @ruby_body_statement or @ruby_block_body;
predicate astNodeInfo(AstNode child, AstNode parent, int primaryIndex, int secondaryIndex) {
not parent instanceof Wrapper and
exists(AstNode node, Location l |
ruby_ast_node_info(node, parent, primaryIndex, _) and
(
not node instanceof Wrapper and secondaryIndex = 0 and child = node
or
node instanceof Wrapper and ruby_ast_node_info(child, node, secondaryIndex, _)
)
)
}
from AstNode node, AstNode parent, int parent_index, Location loc
where
node =
rank[parent_index + 1](AstNode n, int i, int j | astNodeInfo(n, parent, i, j) | n order by i, j) and
ruby_ast_node_info(node, _, _, loc)
select node, parent, parent_index, loc

View File

@@ -0,0 +1,7 @@
class AstNode extends @ruby_ast_node {
string toString() { none() }
}
from AstNode ruby_block, AstNode body, int index, AstNode child
where ruby_block_body(ruby_block, body) and ruby_block_body_child(body, index, child)
select ruby_block, index, child

View File

@@ -0,0 +1,7 @@
class AstNode extends @ruby_ast_node {
string toString() { none() }
}
from AstNode ruby_class, AstNode body, int index, AstNode child
where ruby_class_body(ruby_class, body) and ruby_body_statement_child(body, index, child)
select ruby_class, index, child

View File

@@ -0,0 +1,8 @@
class AstNode extends @ruby_ast_node {
string toString() { none() }
}
from AstNode ruby_do_block, AstNode body, int index, AstNode child
where ruby_do_block_body(ruby_do_block, body) and ruby_body_statement_child(body, index, child)
select ruby_do_block, index, child

View File

@@ -0,0 +1,14 @@
class AstNode extends @ruby_ast_node {
string toString() { none() }
}
from AstNode ruby_method, int index, AstNode child
where
exists(AstNode body |
ruby_method_body(ruby_method, body) and ruby_body_statement_child(body, index, child)
)
or
ruby_method_body(ruby_method, child) and
not child instanceof @ruby_body_statement and
index = 0
select ruby_method, index, child

View File

@@ -0,0 +1,8 @@
class AstNode extends @ruby_ast_node {
string toString() { none() }
}
from AstNode ruby_module, AstNode body, int index, AstNode child
where ruby_module_body(ruby_module, body) and ruby_body_statement_child(body, index, child)
select ruby_module, index, child

View File

@@ -0,0 +1,8 @@
class AstNode extends @ruby_ast_node {
string toString() { none() }
}
from AstNode ruby_singleton_class, AstNode body, int index, AstNode child
where ruby_singleton_class_body(ruby_singleton_class, body) and ruby_body_statement_child(body, index, child)
select ruby_singleton_class, index, child

View File

@@ -0,0 +1,15 @@
class AstNode extends @ruby_ast_node {
string toString() { none() }
}
from AstNode ruby_singleton_method, int index, AstNode child
where
exists(AstNode body |
ruby_singleton_method_body(ruby_singleton_method, body) and
ruby_body_statement_child(body, index, child)
)
or
ruby_singleton_method_body(ruby_singleton_method, child) and
not child instanceof @ruby_body_statement and
index = 0
select ruby_singleton_method, index, child

View File

@@ -0,0 +1,30 @@
description: Wrap class, module, method, and block bodies in a named node
compatibility: full
ruby_block_body.rel: delete
ruby_block_body_def.rel: delete
ruby_block_body_child.rel: delete
ruby_block_child.rel: run ruby_block_child.qlo
ruby_body_statement_child.rel: delete
ruby_body_statement_def.rel: delete
ruby_class_body.rel: delete
ruby_class_child.rel: run ruby_class_child.qlo
ruby_do_block_body.rel: delete
ruby_do_block_child.rel: run ruby_do_block_child.qlo
ruby_method_body.rel: delete
ruby_method_child.rel: run ruby_method_child.qlo
ruby_module_body.rel: delete
ruby_module_child.rel: run ruby_module_child.qlo
ruby_singleton_class_body.rel: delete
ruby_singleton_class_child.rel: run ruby_singleton_class_child.qlo
ruby_singleton_method_body.rel: delete
ruby_singleton_method_child.rel: run ruby_singleton_method_child.qlo
ruby_ast_node_info.rel: run ruby_ast_node_info.qlo

View File

@@ -11,7 +11,7 @@ flate2 = "1.0"
node-types = { path = "../node-types" }
tree-sitter = "0.19"
tree-sitter-embedded-template = { git = "https://github.com/tree-sitter/tree-sitter-embedded-template.git", rev = "1a538da253d73f896b9f6c0c7d79cda58791ac5c" }
tree-sitter-ruby = { git = "https://github.com/tree-sitter/tree-sitter-ruby.git", rev = "e75d04404c9dd71ad68850d5c672b226d5e694f3" }
tree-sitter-ruby = { git = "https://github.com/tree-sitter/tree-sitter-ruby.git", rev = "ad1043283b1f9daf4aad381b6a81f18a5a27fe7e" }
clap = "3.0"
tracing = "0.1"
tracing-subscriber = { version = "0.3.3", features = ["env-filter"] }

View File

@@ -12,4 +12,4 @@ node-types = { path = "../node-types" }
tracing = "0.1"
tracing-subscriber = { version = "0.3.3", features = ["env-filter"] }
tree-sitter-embedded-template = { git = "https://github.com/tree-sitter/tree-sitter-embedded-template.git", rev = "1a538da253d73f896b9f6c0c7d79cda58791ac5c" }
tree-sitter-ruby = { git = "https://github.com/tree-sitter/tree-sitter-ruby.git", rev = "e75d04404c9dd71ad68850d5c672b226d5e694f3" }
tree-sitter-ruby = { git = "https://github.com/tree-sitter/tree-sitter-ruby.git", rev = "ad1043283b1f9daf4aad381b6a81f18a5a27fe7e" }

View File

@@ -1,3 +1,4 @@
import codeql.ruby.AST
import codeql.ruby.DataFlow::DataFlow
import codeql.ruby.dataflow.internal.DataFlowPrivate
import codeql.ruby.dataflow.internal.DataFlowImplConsistency::Consistency
@@ -11,5 +12,7 @@ private class MyConsistencyConfiguration extends ConsistencyConfiguration {
n instanceof SummaryNode
or
n instanceof SynthHashSplatArgumentNode
or
not isNonConstantExpr(n.asExpr())
}
}

View File

@@ -1,10 +1,18 @@
import codeql.ruby.dataflow.SSA
import codeql.ruby.dataflow.internal.SsaImplCommon::Consistency
import codeql.ruby.dataflow.internal.SsaImpl::Consistency as Consistency
class MyRelevantDefinition extends RelevantDefinition, Ssa::Definition {
class MyRelevantDefinition extends Consistency::RelevantDefinition, Ssa::Definition {
override predicate hasLocationInfo(
string filepath, int startline, int startcolumn, int endline, int endcolumn
) {
this.getLocation().hasLocationInfo(filepath, startline, startcolumn, endline, endcolumn)
}
}
query predicate nonUniqueDef = Consistency::nonUniqueDef/4;
query predicate readWithoutDef = Consistency::readWithoutDef/3;
query predicate deadDef = Consistency::deadDef/2;
query predicate notDominatedByDef = Consistency::notDominatedByDef/4;

View File

@@ -0,0 +1,4 @@
---
category: minorAnalysis
---
* Accesses of ActiveResource models are now recognized as HTTP requests.

View File

@@ -0,0 +1,4 @@
---
category: breaking
---
* Changed the `HTTP::Client::Request` concept from using `MethodCall` as base class, to using `DataFlow::Node` as base class. Any class that extends `HTTP::Client::Request::Range` must be changed, but if you only use the member predicates of `HTTP::Client::Request`, no changes are required.

View File

@@ -0,0 +1,4 @@
---
category: fix
---
* Fixed an issue in the taint tracking analysis where implicit reads were not allowed by default in sinks or additional taint steps that used flow states.

View File

@@ -0,0 +1,5 @@
---
category: deprecated
---
* Some classes/modules with upper-case acronyms in their name have been renamed to follow our style-guide.
The old name still exists as a deprecated alias.

View File

@@ -0,0 +1,4 @@
---
category: minorAnalysis
---
* Uses of `ActionView::FileSystemResolver` are now recognized as filesystem accesses.

View File

@@ -60,10 +60,10 @@ class AstNode extends TAstNode {
final string getPrimaryQlClasses() { result = concat(this.getAPrimaryQlClass(), ",") }
/** Gets the enclosing module, if any. */
ModuleBase getEnclosingModule() { result = getEnclosingModule(scopeOfInclSynth(this)) }
final ModuleBase getEnclosingModule() { result = getEnclosingModule(scopeOfInclSynth(this)) }
/** Gets the enclosing method, if any. */
MethodBase getEnclosingMethod() { result = getEnclosingMethod(scopeOfInclSynth(this)) }
final MethodBase getEnclosingMethod() { result = getEnclosingMethod(scopeOfInclSynth(this)) }
/** Gets a textual representation of this node. */
cached

View File

@@ -33,7 +33,7 @@ module API {
* 3. Map the resulting API graph nodes to data-flow nodes, using `asSource` or `asSink`.
*
* For example, a simplified way to get arguments to `Foo.bar` would be
* ```codeql
* ```ql
* API::getTopLevelMember("Foo").getMethod("bar").getParameter(0).asSink()
* ```
*
@@ -97,6 +97,7 @@ module API {
* This is similar to `asSource()` but additionally includes nodes that are transitively reachable by data flow.
* See `asSource()` for examples.
*/
pragma[inline]
DataFlow::Node getAValueReachableFromSource() {
exists(DataFlow::LocalSourceNode src | Impl::use(this, src) |
Impl::trackUseNode(src).flowsTo(result)
@@ -382,11 +383,17 @@ module API {
bindingset[this]
EntryPoint() { any() }
/** DEPRECATED. This predicate has been renamed to `getASource`. */
deprecated DataFlow::LocalSourceNode getAUse() { none() }
/** DEPRECATED. This predicate has been renamed to `getASink`. */
deprecated DataFlow::Node getARhs() { none() }
/** Gets a data-flow node corresponding to a use-node for this entry point. */
DataFlow::LocalSourceNode getAUse() { none() }
DataFlow::LocalSourceNode getASource() { none() }
/** Gets a data-flow node corresponding to a def-node for this entry point. */
DataFlow::Node getARhs() { none() }
DataFlow::Node getASink() { none() }
/** Gets a call corresponding to a method access node for this entry point. */
DataFlow::CallNode getACall() { none() }
@@ -504,7 +511,7 @@ module API {
or
parameterStep(_, defCand(), nd)
or
nd = any(EntryPoint entry).getAUse()
nd = any(EntryPoint entry).getASource()
or
nd = any(EntryPoint entry).getACall()
}
@@ -553,7 +560,7 @@ module API {
// 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()
rhs = any(EntryPoint entry).getASink()
}
/** Gets a data flow node that flows to the RHS of a def-node. */
@@ -712,9 +719,9 @@ module API {
pred = root() and
lbl = Label::entryPoint(entry)
|
succ = MkDef(entry.getARhs())
succ = MkDef(entry.getASink())
or
succ = MkUse(entry.getAUse())
succ = MkUse(entry.getASource())
or
succ = MkMethodAccessNode(entry.getACall())
)
@@ -832,7 +839,7 @@ module API {
LabelEntryPoint() { this = MkLabelEntryPoint(name) }
override string toString() { result = name }
override string toString() { result = "entryPoint(\"" + name + "\")" }
/** Gets the name of the entry point. */
API::EntryPoint getName() { result = name }

View File

@@ -222,7 +222,7 @@ class HtmlEscaping extends Escaping {
}
/** Provides classes for modeling HTTP-related APIs. */
module HTTP {
module Http {
/** Provides classes for modeling HTTP servers. */
module Server {
/**
@@ -465,7 +465,7 @@ module HTTP {
* Extend this class to model new APIs. If you want to refine existing API models,
* extend `HttpResponse` instead.
*/
abstract class Range extends HTTP::Server::HttpResponse::Range {
abstract class Range extends Http::Server::HttpResponse::Range {
/** Gets the data-flow node that specifies the location of this HTTP redirect response. */
abstract DataFlow::Node getRedirectLocation();
}
@@ -474,13 +474,15 @@ module HTTP {
/** Provides classes for modeling HTTP clients. */
module Client {
import codeql.ruby.internal.ConceptsShared::Http::Client as SC
/**
* A method call that makes an outgoing HTTP request.
*
* Extend this class to refine existing API models. If you want to model new APIs,
* extend `Request::Range` instead.
*/
class Request extends MethodCall instanceof Request::Range {
class Request extends SC::Request instanceof Request::Range {
/** Gets a node which returns the body of the response */
DataFlow::Node getResponseBody() { result = super.getResponseBody() }
@@ -490,24 +492,19 @@ module HTTP {
* Gets a node that contributes to the URL of the request.
* Depending on the framework, a request may have multiple nodes which contribute to the URL.
*/
deprecated DataFlow::Node getURL() { result = super.getURL() or result = super.getAUrlPart() }
/**
* Gets a data-flow node that contributes to the URL of the request.
* Depending on the framework, a request may have multiple nodes which contribute to the URL.
*/
DataFlow::Node getAUrlPart() { result = super.getAUrlPart() }
/** Gets a string that identifies the framework used for this request. */
string getFramework() { result = super.getFramework() }
deprecated DataFlow::Node getURL() {
result = super.getURL() or result = Request::Range.super.getAUrlPart()
}
/**
* Holds if this request is made using a mode that disables SSL/TLS
* certificate validation, where `disablingNode` represents the point at
* which the validation was disabled.
*/
predicate disablesCertificateValidation(DataFlow::Node disablingNode) {
super.disablesCertificateValidation(disablingNode)
deprecated predicate disablesCertificateValidation(DataFlow::Node disablingNode) {
Request::Range.super.disablesCertificateValidation(disablingNode, _)
or
Request::Range.super.disablesCertificateValidation(disablingNode)
}
}
@@ -519,7 +516,7 @@ module HTTP {
* Extend this class to model new APIs. If you want to refine existing API models,
* extend `Request` instead.
*/
abstract class Range extends MethodCall {
abstract class Range extends SC::Request::Range {
/** Gets a node which returns the body of the response */
abstract DataFlow::Node getResponseBody();
@@ -532,20 +529,13 @@ module HTTP {
deprecated DataFlow::Node getURL() { none() }
/**
* Gets a data-flow node that contributes to the URL of the request.
* Depending on the framework, a request may have multiple nodes which contribute to the URL.
*/
abstract DataFlow::Node getAUrlPart();
/** Gets a string that identifies the framework used for this request. */
abstract string getFramework();
/**
* DEPRECATED: override `disablesCertificateValidation/2` instead.
*
* Holds if this request is made using a mode that disables SSL/TLS
* certificate validation, where `disablingNode` represents the point at
* which the validation was disabled.
*/
abstract predicate disablesCertificateValidation(DataFlow::Node disablingNode);
deprecated predicate disablesCertificateValidation(DataFlow::Node disablingNode) { none() }
}
}
@@ -560,6 +550,9 @@ module HTTP {
}
}
/** DEPRECATED: Alias for Http */
deprecated module HTTP = Http;
/**
* A data flow node that executes an operating system command,
* for instance by spawning a new process.
@@ -621,16 +614,12 @@ module CodeExecution {
* Extend this class to refine existing API models. If you want to model new APIs,
* extend `XmlParserCall::Range` instead.
*/
class XmlParserCall extends DataFlow::Node {
XmlParserCall::Range range;
XmlParserCall() { this = range }
class XmlParserCall extends DataFlow::Node instanceof XmlParserCall::Range {
/** Gets the argument that specifies the XML content to be parsed. */
DataFlow::Node getInput() { result = range.getInput() }
DataFlow::Node getInput() { result = super.getInput() }
/** Holds if this XML parser call is configured to process external entities */
predicate externalEntitiesEnabled() { range.externalEntitiesEnabled() }
predicate externalEntitiesEnabled() { super.externalEntitiesEnabled() }
}
/** Provides a class for modeling new XML parsing APIs. */
@@ -800,13 +789,9 @@ module CookieSecurityConfigurationSetting {
* Extend this class to refine existing API models. If you want to model new APIs,
* extend `Logging::Range` instead.
*/
class Logging extends DataFlow::Node {
Logging::Range range;
Logging() { this = range }
class Logging extends DataFlow::Node instanceof Logging::Range {
/** Gets an input that is logged. */
DataFlow::Node getAnInput() { result = range.getAnInput() }
DataFlow::Node getAnInput() { result = super.getAnInput() }
}
/** Provides a class for modeling new logging mechanisms. */

View File

@@ -6,6 +6,7 @@ private import codeql.ruby.frameworks.Core
private import codeql.ruby.frameworks.ActionCable
private import codeql.ruby.frameworks.ActionController
private import codeql.ruby.frameworks.ActiveRecord
private import codeql.ruby.frameworks.ActiveResource
private import codeql.ruby.frameworks.ActiveStorage
private import codeql.ruby.frameworks.ActionView
private import codeql.ruby.frameworks.ActiveSupport

View File

@@ -51,25 +51,35 @@ class Ensure extends StmtSequence, TEnsure {
// Not defined by dispatch, as it should not be exposed
Ruby::AstNode getBodyStmtChild(TBodyStmt b, int i) {
result = any(Ruby::Method g | b = TMethod(g)).getChild(i)
or
result = any(Ruby::SingletonMethod g | b = TSingletonMethod(g)).getChild(i)
or
exists(Ruby::Lambda g | b = TLambda(g) |
result = g.getBody().(Ruby::DoBlock).getChild(i) or
result = g.getBody().(Ruby::Block).getChild(i)
exists(Ruby::Method g, Ruby::AstNode body | b = TMethod(g) and body = g.getBody() |
result = body.(Ruby::BodyStatement).getChild(i)
or
i = 0 and result = body and not body instanceof Ruby::BodyStatement
)
or
result = any(Ruby::DoBlock g | b = TDoBlock(g)).getChild(i)
exists(Ruby::SingletonMethod g, Ruby::AstNode body |
b = TSingletonMethod(g) and body = g.getBody()
|
result = body.(Ruby::BodyStatement).getChild(i)
or
i = 0 and result = body and not body instanceof Ruby::BodyStatement
)
or
exists(Ruby::Lambda g | b = TLambda(g) |
result = g.getBody().(Ruby::DoBlock).getBody().getChild(i) or
result = g.getBody().(Ruby::Block).getBody().getChild(i)
)
or
result = any(Ruby::DoBlock g | b = TDoBlock(g)).getBody().getChild(i)
or
result = any(Ruby::Program g | b = TToplevel(g)).getChild(i) and
not result instanceof Ruby::BeginBlock
or
result = any(Ruby::Class g | b = TClassDeclaration(g)).getChild(i)
result = any(Ruby::Class g | b = TClassDeclaration(g)).getBody().getChild(i)
or
result = any(Ruby::SingletonClass g | b = TSingletonClass(g)).getChild(i)
result = any(Ruby::SingletonClass g | b = TSingletonClass(g)).getBody().getChild(i)
or
result = any(Ruby::Module g | b = TModuleDeclaration(g)).getChild(i)
result = any(Ruby::Module g | b = TModuleDeclaration(g)).getBody().getChild(i)
or
result = any(Ruby::Begin g | b = TBeginExpr(g)).getChild(i)
}

View File

@@ -15,7 +15,7 @@ class BraceBlockReal extends BraceBlock, TBraceBlockReal {
toGenerated(result) = g.getParameters().getChild(n)
}
final override Stmt getStmt(int i) { toGenerated(result) = g.getChild(i) }
final override Stmt getStmt(int i) { toGenerated(result) = g.getBody().getChild(i) }
}
/**

View File

@@ -137,14 +137,7 @@ private module Cached {
}
cached
Method lookupMethod(Module m, string name) {
// The syntax_suggest library redefines Kernel.require/require_relative.
// Somehow this causes performance issues on ruby/ruby. As a workaround
// we exclude 'require' and 'require_relative'.
// TODO: find the actual cause of the slowdown and fix things properly.
not name = ["require", "require_relative"] and
TMethod(result) = lookupMethodOrConst(m, name)
}
Method lookupMethod(Module m, string name) { TMethod(result) = lookupMethodOrConst(m, name) }
cached
Expr lookupConst(Module m, string name) {

View File

@@ -128,7 +128,7 @@ private AstNode specialParentOfInclSynth(AstNode n) {
n =
[
result.(Namespace).getScopeExpr(), result.(ClassDeclaration).getSuperclassExpr(),
result.(SingletonMethod).getObject()
result.(SingletonMethod).getObject(), result.(SingletonClass).getValue()
]
}

View File

@@ -311,15 +311,15 @@ module Ruby {
/** Gets the name of the primary QL class for this element. */
final override string getAPrimaryQlClass() { result = "Block" }
/** Gets the node corresponding to the field `body`. */
final BlockBody getBody() { ruby_block_body(this, result) }
/** Gets the node corresponding to the field `parameters`. */
final BlockParameters getParameters() { ruby_block_parameters(this, result) }
/** Gets the `i`th child of this node. */
final AstNode getChild(int i) { ruby_block_child(this, i, result) }
/** Gets a field or child node of this node. */
final override AstNode getAFieldOrChild() {
ruby_block_parameters(this, result) or ruby_block_child(this, _, result)
ruby_block_body(this, result) or ruby_block_parameters(this, result)
}
}
@@ -335,6 +335,18 @@ module Ruby {
final override AstNode getAFieldOrChild() { ruby_block_argument_child(this, result) }
}
/** A class representing `block_body` nodes. */
class BlockBody extends @ruby_block_body, AstNode {
/** Gets the name of the primary QL class for this element. */
final override string getAPrimaryQlClass() { result = "BlockBody" }
/** Gets the `i`th child of this node. */
final AstNode getChild(int i) { ruby_block_body_child(this, i, result) }
/** Gets a field or child node of this node. */
final override AstNode getAFieldOrChild() { ruby_block_body_child(this, _, result) }
}
/** A class representing `block_parameter` nodes. */
class BlockParameter extends @ruby_block_parameter, AstNode {
/** Gets the name of the primary QL class for this element. */
@@ -364,6 +376,18 @@ module Ruby {
}
}
/** A class representing `body_statement` nodes. */
class BodyStatement extends @ruby_body_statement, AstNode {
/** Gets the name of the primary QL class for this element. */
final override string getAPrimaryQlClass() { result = "BodyStatement" }
/** Gets the `i`th child of this node. */
final AstNode getChild(int i) { ruby_body_statement_child(this, i, result) }
/** Gets a field or child node of this node. */
final override AstNode getAFieldOrChild() { ruby_body_statement_child(this, _, result) }
}
/** A class representing `break` nodes. */
class Break extends @ruby_break, AstNode {
/** Gets the name of the primary QL class for this element. */
@@ -468,20 +492,20 @@ module Ruby {
/** Gets the name of the primary QL class for this element. */
final override string getAPrimaryQlClass() { result = "Class" }
/** Gets the node corresponding to the field `body`. */
final BodyStatement getBody() { ruby_class_body(this, result) }
/** Gets the node corresponding to the field `name`. */
final AstNode getName() { ruby_class_def(this, result) }
/** Gets the node corresponding to the field `superclass`. */
final Superclass getSuperclass() { ruby_class_superclass(this, result) }
/** Gets the `i`th child of this node. */
final AstNode getChild(int i) { ruby_class_child(this, i, result) }
/** Gets a field or child node of this node. */
final override AstNode getAFieldOrChild() {
ruby_class_body(this, result) or
ruby_class_def(this, result) or
ruby_class_superclass(this, result) or
ruby_class_child(this, _, result)
ruby_class_superclass(this, result)
}
}
@@ -592,15 +616,15 @@ module Ruby {
/** Gets the name of the primary QL class for this element. */
final override string getAPrimaryQlClass() { result = "DoBlock" }
/** Gets the node corresponding to the field `body`. */
final BodyStatement getBody() { ruby_do_block_body(this, result) }
/** Gets the node corresponding to the field `parameters`. */
final BlockParameters getParameters() { ruby_do_block_parameters(this, result) }
/** Gets the `i`th child of this node. */
final AstNode getChild(int i) { ruby_do_block_child(this, i, result) }
/** Gets a field or child node of this node. */
final override AstNode getAFieldOrChild() {
ruby_do_block_parameters(this, result) or ruby_do_block_child(this, _, result)
ruby_do_block_body(this, result) or ruby_do_block_parameters(this, result)
}
}
@@ -1106,20 +1130,20 @@ module Ruby {
/** Gets the name of the primary QL class for this element. */
final override string getAPrimaryQlClass() { result = "Method" }
/** Gets the node corresponding to the field `body`. */
final AstNode getBody() { ruby_method_body(this, result) }
/** Gets the node corresponding to the field `name`. */
final UnderscoreMethodName getName() { ruby_method_def(this, result) }
/** Gets the node corresponding to the field `parameters`. */
final MethodParameters getParameters() { ruby_method_parameters(this, result) }
/** Gets the `i`th child of this node. */
final AstNode getChild(int i) { ruby_method_child(this, i, result) }
/** Gets a field or child node of this node. */
final override AstNode getAFieldOrChild() {
ruby_method_body(this, result) or
ruby_method_def(this, result) or
ruby_method_parameters(this, result) or
ruby_method_child(this, _, result)
ruby_method_parameters(this, result)
}
}
@@ -1140,15 +1164,15 @@ module Ruby {
/** Gets the name of the primary QL class for this element. */
final override string getAPrimaryQlClass() { result = "Module" }
/** Gets the node corresponding to the field `body`. */
final BodyStatement getBody() { ruby_module_body(this, result) }
/** Gets the node corresponding to the field `name`. */
final AstNode getName() { ruby_module_def(this, result) }
/** Gets the `i`th child of this node. */
final AstNode getChild(int i) { ruby_module_child(this, i, result) }
/** Gets a field or child node of this node. */
final override AstNode getAFieldOrChild() {
ruby_module_def(this, result) or ruby_module_child(this, _, result)
ruby_module_body(this, result) or ruby_module_def(this, result)
}
}
@@ -1504,15 +1528,15 @@ module Ruby {
/** Gets the name of the primary QL class for this element. */
final override string getAPrimaryQlClass() { result = "SingletonClass" }
/** Gets the node corresponding to the field `body`. */
final BodyStatement getBody() { ruby_singleton_class_body(this, result) }
/** Gets the node corresponding to the field `value`. */
final UnderscoreArg getValue() { ruby_singleton_class_def(this, result) }
/** Gets the `i`th child of this node. */
final AstNode getChild(int i) { ruby_singleton_class_child(this, i, result) }
/** Gets a field or child node of this node. */
final override AstNode getAFieldOrChild() {
ruby_singleton_class_def(this, result) or ruby_singleton_class_child(this, _, result)
ruby_singleton_class_body(this, result) or ruby_singleton_class_def(this, result)
}
}
@@ -1521,6 +1545,9 @@ module Ruby {
/** Gets the name of the primary QL class for this element. */
final override string getAPrimaryQlClass() { result = "SingletonMethod" }
/** Gets the node corresponding to the field `body`. */
final AstNode getBody() { ruby_singleton_method_body(this, result) }
/** Gets the node corresponding to the field `name`. */
final UnderscoreMethodName getName() { ruby_singleton_method_def(this, result, _) }
@@ -1530,15 +1557,12 @@ module Ruby {
/** Gets the node corresponding to the field `parameters`. */
final MethodParameters getParameters() { ruby_singleton_method_parameters(this, result) }
/** Gets the `i`th child of this node. */
final AstNode getChild(int i) { ruby_singleton_method_child(this, i, result) }
/** Gets a field or child node of this node. */
final override AstNode getAFieldOrChild() {
ruby_singleton_method_body(this, result) or
ruby_singleton_method_def(this, result, _) or
ruby_singleton_method_def(this, _, result) or
ruby_singleton_method_parameters(this, result) or
ruby_singleton_method_child(this, _, result)
ruby_singleton_method_parameters(this, result)
}
}

View File

@@ -200,18 +200,18 @@ private module Cached {
or
i = any(Ruby::Binary x).getRight()
or
i = any(Ruby::Block x).getChild(_)
or
i = any(Ruby::BlockArgument x).getChild()
or
i = any(Ruby::BlockBody x).getChild(_)
or
i = any(Ruby::BodyStatement x).getChild(_)
or
i = any(Ruby::Call x).getReceiver()
or
i = any(Ruby::Case x).getValue()
or
i = any(Ruby::CaseMatch x).getValue()
or
i = any(Ruby::Class x).getChild(_)
or
i = any(Ruby::Conditional x).getCondition()
or
i = any(Ruby::Conditional x).getConsequence()
@@ -220,8 +220,6 @@ private module Cached {
or
i = any(Ruby::Do x).getChild(_)
or
i = any(Ruby::DoBlock x).getChild(_)
or
i = any(Ruby::ElementReference x).getChild(_)
or
i = any(Ruby::ElementReference x).getObject()
@@ -250,9 +248,7 @@ private module Cached {
or
i = any(Ruby::KeywordParameter x).getValue()
or
i = any(Ruby::Method x).getChild(_)
or
i = any(Ruby::Module x).getChild(_)
i = any(Ruby::Method x).getBody()
or
i = any(Ruby::OperatorAssignment x).getRight()
or
@@ -282,9 +278,7 @@ private module Cached {
or
i = any(Ruby::SingletonClass x).getValue()
or
i = any(Ruby::SingletonClass x).getChild(_)
or
i = any(Ruby::SingletonMethod x).getChild(_)
i = any(Ruby::SingletonMethod x).getBody()
or
i = any(Ruby::SingletonMethod x).getObject()
or

View File

@@ -311,13 +311,13 @@ private module Cached {
}
cached
predicate immediatelyControls(ConditionBlock cb, BasicBlock succ, BooleanSuccessor s) {
predicate immediatelyControls(ConditionBlock cb, BasicBlock succ, ConditionalSuccessor s) {
succ = cb.getASuccessor(s) and
forall(BasicBlock pred | pred = succ.getAPredecessor() and pred != cb | succ.dominates(pred))
}
cached
predicate controls(ConditionBlock cb, BasicBlock controlled, BooleanSuccessor s) {
predicate controls(ConditionBlock cb, BasicBlock controlled, ConditionalSuccessor s) {
exists(BasicBlock succ | cb.immediatelyControls(succ, s) | succ.dominates(controlled))
}
}
@@ -406,7 +406,7 @@ class ConditionBlock extends BasicBlock {
* successor of this block, and `succ` can only be reached from
* the callable entry point by going via the `s` edge out of this basic block.
*/
predicate immediatelyControls(BasicBlock succ, BooleanSuccessor s) {
predicate immediatelyControls(BasicBlock succ, ConditionalSuccessor s) {
immediatelyControls(this, succ, s)
}
@@ -415,5 +415,7 @@ class ConditionBlock extends BasicBlock {
* conditional value `s`. That is, `controlled` can only be reached from
* the callable entry point by going via the `s` edge out of this basic block.
*/
predicate controls(BasicBlock controlled, BooleanSuccessor s) { controls(this, controlled, s) }
predicate controls(BasicBlock controlled, ConditionalSuccessor s) {
controls(this, controlled, s)
}
}

View File

@@ -433,7 +433,7 @@ module ExprNodes {
}
private class WhenClauseChildMapping extends NonExprChildMapping, WhenClause {
override predicate relevantChild(AstNode e) { e = this.getBody() }
override predicate relevantChild(AstNode e) { e = [this.getBody(), this.getAPattern()] }
}
/** A control-flow node that wraps a `WhenClause` AST expression. */
@@ -444,6 +444,9 @@ module ExprNodes {
/** Gets the body of this `when`-clause. */
final ExprCfgNode getBody() { e.hasCfgChild(e.getBody(), this, result) }
/** Gets the `i`th pattern this `when`-clause. */
final ExprCfgNode getPattern(int i) { e.hasCfgChild(e.getPattern(i), this, result) }
}
/** A control-flow node that wraps a `CasePattern`. */
@@ -711,6 +714,19 @@ module ExprNodes {
final CfgNode getReceiver() { e.hasCfgChild(e.getReceiver(), this, result) }
}
private class SelfVariableAccessMapping extends ExprChildMapping, SelfVariableAccess {
override predicate relevantChild(AstNode n) { none() }
}
/** A control-flow node that wraps a `SelfVariableAccess` AST expression. */
class SelfVariableAccessCfgNode extends ExprCfgNode {
final override string getAPrimaryQlClass() { result = "SelfVariableAccessCfgNode" }
override SelfVariableAccessMapping e;
override SelfVariableAccess getExpr() { result = ExprCfgNode.super.getExpr() }
}
/** A control-flow node that wraps a `VariableWriteAccess` AST expression. */
class VariableWriteAccessCfgNode extends ExprCfgNode {
override string getAPrimaryQlClass() { result = "VariableWriteAccessCfgNode" }

View File

@@ -44,7 +44,7 @@ class CfgNode extends TCfgNode {
final File getFile() { result = this.getLocation().getFile() }
/** Holds if this control flow node has conditional successors. */
final predicate isCondition() { exists(this.getASuccessor(any(BooleanSuccessor bs))) }
final predicate isCondition() { exists(this.getASuccessor(any(ConditionalSuccessor bs))) }
/** Gets the scope of this node. */
final CfgScope getScope() { result = getNodeCfgScope(this) }

View File

@@ -13,13 +13,9 @@ private import codeql.ruby.Frameworks
* Extend this class to refine existing API models. If you want to model new APIs,
* extend `RemoteFlowSource::Range` instead.
*/
class RemoteFlowSource extends DataFlow::Node {
RemoteFlowSource::Range self;
RemoteFlowSource() { this = self }
class RemoteFlowSource extends DataFlow::Node instanceof RemoteFlowSource::Range {
/** Gets a string that describes the type of this remote flow source. */
string getSourceType() { result = self.getSourceType() }
string getSourceType() { result = super.getSourceType() }
}
/** Provides a class for modeling new sources of remote user input. */

View File

@@ -9,12 +9,11 @@ module Ssa {
private import codeql.Locations
private import codeql.ruby.CFG
private import codeql.ruby.ast.Variable
private import internal.SsaImplCommon as SsaImplCommon
private import internal.SsaImpl as SsaImpl
private import CfgNodes::ExprNodes
/** A static single assignment (SSA) definition. */
class Definition extends SsaImplCommon::Definition {
class Definition extends SsaImpl::Definition {
/**
* Gets the control flow node of this SSA definition, if any. Phi nodes are
* examples of SSA definitions without a control flow node, as they are
@@ -190,7 +189,7 @@ module Ssa {
* puts x
* ```
*/
class WriteDefinition extends Definition, SsaImplCommon::WriteDefinition {
class WriteDefinition extends Definition, SsaImpl::WriteDefinition {
private VariableWriteAccess write;
WriteDefinition() {
@@ -223,7 +222,7 @@ module Ssa {
/**
* An SSA definition that corresponds to the value of `self` upon entry to a method, class or module.
*/
class SelfDefinition extends Definition, SsaImplCommon::WriteDefinition {
class SelfDefinition extends Definition, SsaImpl::WriteDefinition {
private SelfVariable v;
SelfDefinition() {
@@ -254,7 +253,7 @@ module Ssa {
* since the assignment to `x` is conditional, an unitialized definition for
* `x` is inserted at the start of `m`.
*/
class UninitializedDefinition extends Definition, SsaImplCommon::WriteDefinition {
class UninitializedDefinition extends Definition, SsaImpl::WriteDefinition {
UninitializedDefinition() {
exists(BasicBlock bb, int i, Variable v |
this.definesAt(v, bb, i) and
@@ -283,7 +282,7 @@ module Ssa {
*
* an entry definition for `y` is inserted at the start of the `do` block.
*/
class CapturedEntryDefinition extends Definition, SsaImplCommon::WriteDefinition {
class CapturedEntryDefinition extends Definition, SsaImpl::WriteDefinition {
CapturedEntryDefinition() {
exists(BasicBlock bb, int i, Variable v |
this.definesAt(v, bb, i) and
@@ -312,7 +311,7 @@ module Ssa {
*
* a definition for `y` is inserted at the call to `times`.
*/
class CapturedCallDefinition extends Definition, SsaImplCommon::UncertainWriteDefinition {
class CapturedCallDefinition extends Definition, SsaImpl::UncertainWriteDefinition {
CapturedCallDefinition() {
exists(Variable v, BasicBlock bb, int i |
this.definesAt(v, bb, i) and
@@ -343,7 +342,7 @@ module Ssa {
*
* a phi node for `x` is inserted just before the call `puts x`.
*/
class PhiNode extends Definition, SsaImplCommon::PhiNode {
class PhiNode extends Definition, SsaImpl::PhiNode {
/**
* Gets an input of this phi node.
*

View File

@@ -2,10 +2,12 @@ private import ruby
private import codeql.ruby.CFG
private import DataFlowPrivate
private import codeql.ruby.typetracking.TypeTracker
private import codeql.ruby.typetracking.TypeTrackerSpecific as TypeTrackerSpecific
private import codeql.ruby.ast.internal.Module
private import FlowSummaryImpl as FlowSummaryImpl
private import FlowSummaryImplSpecific as FlowSummaryImplSpecific
private import codeql.ruby.dataflow.FlowSummary
private import codeql.ruby.dataflow.SSA
newtype TReturnKind =
TNormalReturnKind() or
@@ -148,12 +150,19 @@ private class NormalCall extends DataFlowCall, TNormalCall {
pragma[nomagic]
private predicate methodCall(
CfgNodes::ExprNodes::CallCfgNode call, DataFlow::Node receiver, string method
) {
method = call.getExpr().(MethodCall).getMethodName() and
receiver.asExpr() = call.getReceiver()
}
pragma[nomagic]
private predicate flowsToMethodCall(
CfgNodes::ExprNodes::CallCfgNode call, DataFlow::LocalSourceNode sourceNode, string method
) {
exists(DataFlow::Node nodeTo |
method = call.getExpr().(MethodCall).getMethodName() and
nodeTo.asExpr() = call.getReceiver() and
sourceNode.flowsTo(nodeTo)
exists(DataFlow::Node receiver |
methodCall(call, receiver, method) and
sourceNode.flowsTo(receiver)
)
}
@@ -177,9 +186,94 @@ private predicate superCall(CfgNodes::ExprNodes::CallCfgNode call, Module superC
pragma[nomagic]
private predicate instanceMethodCall(CfgNodes::ExprNodes::CallCfgNode call, Module tp, string method) {
exists(DataFlow::LocalSourceNode sourceNode |
methodCall(call, sourceNode, method) and
sourceNode = trackInstance(tp)
exists(DataFlow::Node receiver, Module m, boolean exact |
methodCall(call, receiver, method) and
receiver = trackInstance(m, exact)
|
tp = m
or
// When we don't know the exact type, it could be any sub class
exact = false and
tp.getSuperClass+() = m
)
}
/** Holds if `self` belongs to module `m`. */
pragma[nomagic]
private predicate selfInModule(SelfVariable self, Module m) {
exists(Scope scope |
scope = self.getDeclaringScope() and
m = scope.(ModuleBase).getModule() and
not scope instanceof Toplevel
)
}
/** Holds if `self` belongs to method `method` inside module `m`. */
pragma[nomagic]
private predicate selfInMethod(SelfVariable self, MethodBase method, Module m) {
exists(ModuleBase encl |
method = self.getDeclaringScope() and
encl = method.getEnclosingModule() and
if encl instanceof SingletonClass
then m = encl.getEnclosingModule().getModule()
else m = encl.getModule()
)
}
/** Holds if `self` belongs to the top-level. */
pragma[nomagic]
private predicate selfInToplevel(SelfVariable self, Module m) {
self.getDeclaringScope() instanceof Toplevel and
m = TResolved("Object")
}
/**
* Holds if SSA definition `def` belongs to a variable introduced via pattern
* matching on type `m`. For example, in
*
* ```rb
* case object
* in C => c then c.foo
* end
* ```
*
* the SSA definition for `c` is introduced by matching on `C`.
*/
private predicate asModulePattern(SsaDefinitionNode def, Module m) {
exists(AsPattern ap |
m = resolveConstantReadAccess(ap.getPattern()) and
def.getDefinition().(Ssa::WriteDefinition).getWriteAccess() = ap.getVariableAccess()
)
}
/**
* Holds if `read1` and `read2` are adjacent reads of SSA definition `def`,
* and `read2` is checked to have type `m`. For example, in
*
* ```rb
* case object
* when C then object.foo
* end
* ```
*
* the two reads of `object` are adjacent, and the second is checked to have type `C`.
*/
private predicate hasAdjacentTypeCheckedReads(
Ssa::Definition def, CfgNodes::ExprCfgNode read1, CfgNodes::ExprCfgNode read2, Module m
) {
exists(
CfgNodes::ExprCfgNode pattern, ConditionBlock cb, CfgNodes::ExprNodes::CaseExprCfgNode case
|
m = resolveConstantReadAccess(pattern.getExpr()) and
cb.getLastNode() = pattern and
cb.controls(read2.getBasicBlock(),
any(SuccessorTypes::MatchingSuccessor match | match.getValue() = true)) and
def.hasAdjacentReads(read1, read2) and
case.getValue() = read1
|
pattern = case.getBranch(_).(CfgNodes::ExprNodes::WhenClauseCfgNode).getPattern(_)
or
pattern = case.getBranch(_).(CfgNodes::ExprNodes::InClauseCfgNode).getPattern()
)
}
@@ -222,9 +316,45 @@ private module Cached {
else any()
)
or
exists(DataFlow::LocalSourceNode sourceNode |
methodCall(call, sourceNode, method) and
sourceNode = trackSingletonMethod(result, method)
// singleton method defined on an instance, e.g.
// ```rb
// c = C.new
// def c.singleton; end # <- result
// c.singleton # <- call
// ```
exists(DataFlow::Node receiver |
methodCall(call, receiver, method) and
receiver = trackSingletonMethodOnInstance(result, method)
)
or
// singleton method defined on a module
exists(DataFlow::Node sourceNode, Module m |
flowsToMethodCall(call, sourceNode, method) and
singletonMethodOnModule(result, method, m)
|
// ```rb
// def C.singleton; end # <- result
// C.singleton # <- call
// ```
sourceNode = trackModuleAccess(m)
or
// ```rb
// class C
// def self.singleton; end # <- result
// self.singleton # <- call
// end
// ```
selfInModule(sourceNode.(SsaSelfDefinitionNode).getVariable(), m)
or
// ```rb
// class C
// def self.singleton; end # <- result
// def self.other
// self.singleton # <- call
// end
// end
// ```
selfInMethod(sourceNode.(SsaSelfDefinitionNode).getVariable(), _, m.getSuperClass*())
)
)
or
@@ -293,73 +423,166 @@ private module Cached {
import Cached
private DataFlow::LocalSourceNode trackInstance(Module tp, TypeTracker t) {
t.start() and
(
result.asExpr().getExpr() instanceof NilLiteral and tp = TResolved("NilClass")
or
result.asExpr().getExpr().(BooleanLiteral).isFalse() and tp = TResolved("FalseClass")
or
result.asExpr().getExpr().(BooleanLiteral).isTrue() and tp = TResolved("TrueClass")
or
result.asExpr().getExpr() instanceof IntegerLiteral and tp = TResolved("Integer")
or
result.asExpr().getExpr() instanceof FloatLiteral and tp = TResolved("Float")
or
result.asExpr().getExpr() instanceof RationalLiteral and tp = TResolved("Rational")
or
result.asExpr().getExpr() instanceof ComplexLiteral and tp = TResolved("Complex")
or
result.asExpr().getExpr() instanceof StringlikeLiteral and tp = TResolved("String")
or
result.asExpr() instanceof CfgNodes::ExprNodes::ArrayLiteralCfgNode and tp = TResolved("Array")
or
result.asExpr() instanceof CfgNodes::ExprNodes::HashLiteralCfgNode and tp = TResolved("Hash")
or
result.asExpr().getExpr() instanceof MethodBase and tp = TResolved("Symbol")
or
result.asParameter() instanceof BlockParameter and tp = TResolved("Proc")
or
result.asExpr().getExpr() instanceof Lambda and tp = TResolved("Proc")
or
exists(CfgNodes::ExprNodes::CallCfgNode call, DataFlow::Node nodeTo |
call.getExpr().(MethodCall).getMethodName() = "new" and
nodeTo.asExpr() = call.getReceiver() and
trackModule(tp).flowsTo(nodeTo) and
result.asExpr() = call
)
or
// `self` in method
tp = result.(SsaSelfDefinitionNode).getSelfScope().(Method).getEnclosingModule().getModule()
or
// `self` in singleton method
flowsToSingletonMethodObject(trackInstance(tp), result.(SsaSelfDefinitionNode).getSelfScope())
or
// `self` in top-level
result.(SsaSelfDefinitionNode).getSelfScope() instanceof Toplevel and
tp = TResolved("Object")
or
// a module or class
exists(Module m |
result = trackModule(m) and
if m.isClass() then tp = TResolved("Class") else tp = TResolved("Module")
)
)
pragma[nomagic]
private DataFlow::LocalSourceNode trackModuleAccess(Module m, TypeTracker t) {
t.start() and m = resolveConstantReadAccess(result.asExpr().getExpr())
or
exists(TypeTracker t2, StepSummary summary |
result = trackInstanceRec(tp, t2, summary) and t = t2.append(summary)
result = trackModuleAccessRec(m, t2, summary) and t = t2.append(summary)
)
}
pragma[nomagic]
private DataFlow::LocalSourceNode trackInstanceRec(Module tp, TypeTracker t, StepSummary summary) {
StepSummary::step(trackInstance(tp, t), result, summary)
private DataFlow::LocalSourceNode trackModuleAccessRec(Module m, TypeTracker t, StepSummary summary) {
StepSummary::step(trackModuleAccess(m, t), result, summary)
}
private DataFlow::LocalSourceNode trackInstance(Module tp) {
result = trackInstance(tp, TypeTracker::end())
pragma[nomagic]
private DataFlow::LocalSourceNode trackModuleAccess(Module m) {
result = trackModuleAccess(m, TypeTracker::end())
}
pragma[nomagic]
private DataFlow::Node trackInstance(Module tp, boolean exact, TypeTracker t) {
t.start() and
(
result.asExpr().getExpr() instanceof NilLiteral and
tp = TResolved("NilClass") and
exact = true
or
result.asExpr().getExpr().(BooleanLiteral).isFalse() and
tp = TResolved("FalseClass") and
exact = true
or
result.asExpr().getExpr().(BooleanLiteral).isTrue() and
tp = TResolved("TrueClass") and
exact = true
or
result.asExpr().getExpr() instanceof IntegerLiteral and
tp = TResolved("Integer") and
exact = true
or
result.asExpr().getExpr() instanceof FloatLiteral and
tp = TResolved("Float") and
exact = true
or
result.asExpr().getExpr() instanceof RationalLiteral and
tp = TResolved("Rational") and
exact = true
or
result.asExpr().getExpr() instanceof ComplexLiteral and
tp = TResolved("Complex") and
exact = true
or
result.asExpr().getExpr() instanceof StringlikeLiteral and
tp = TResolved("String") and
exact = true
or
result.asExpr() instanceof CfgNodes::ExprNodes::ArrayLiteralCfgNode and
tp = TResolved("Array") and
exact = true
or
result.asExpr() instanceof CfgNodes::ExprNodes::HashLiteralCfgNode and
tp = TResolved("Hash") and
exact = true
or
result.asExpr().getExpr() instanceof MethodBase and
tp = TResolved("Symbol") and
exact = true
or
result.asParameter() instanceof BlockParameter and
tp = TResolved("Proc") and
exact = true
or
result.asExpr().getExpr() instanceof Lambda and
tp = TResolved("Proc") and
exact = true
or
exists(CfgNodes::ExprNodes::CallCfgNode call, DataFlow::LocalSourceNode sourceNode |
flowsToMethodCall(call, sourceNode, "new") and
exact = true and
result.asExpr() = call
|
// `C.new`
sourceNode = trackModuleAccess(tp)
or
// `self.new` inside a module
selfInModule(sourceNode.(SsaSelfDefinitionNode).getVariable(), tp)
or
// `self.new` inside a singleton method
selfInMethod(sourceNode.(SsaSelfDefinitionNode).getVariable(), any(SingletonMethod sm), tp)
)
or
// `self` reference in method or top-level (but not in module or singleton method,
// where instance methods cannot be called; only singleton methods)
result =
any(SsaSelfDefinitionNode self |
exists(MethodBase m |
selfInMethod(self.getVariable(), m, tp) and
not m instanceof SingletonMethod and
if m.getEnclosingModule() instanceof Toplevel then exact = true else exact = false
)
or
selfInToplevel(self.getVariable(), tp) and
exact = true
)
or
exists(Module m |
(if m.isClass() then tp = TResolved("Class") else tp = TResolved("Module")) and
exact = true
|
// needed for e.g. `C.new`
m = resolveConstantReadAccess(result.asExpr().getExpr())
or
// needed for e.g. `self.include`
selfInModule(result.(SsaSelfDefinitionNode).getVariable(), m)
or
// needed for e.g. `self.puts`
selfInMethod(result.(SsaSelfDefinitionNode).getVariable(), any(SingletonMethod sm), m)
)
or
// `in C => c then c.foo`
asModulePattern(result, tp) and
exact = false
or
// `case object when C then object.foo`
hasAdjacentTypeCheckedReads(_, _, result.asExpr(), tp) and
exact = false
)
or
exists(TypeTracker t2, StepSummary summary |
result = trackInstanceRec(tp, t2, exact, summary) and t = t2.append(summary)
)
}
private predicate localFlowStep(DataFlow::Node nodeFrom, DataFlow::Node nodeTo, StepSummary summary) {
localFlowStepTypeTracker(nodeFrom, nodeTo) and
summary.toString() = "level"
}
/**
* We exclude steps into `self` parameters and type checked variables. For those,
* we instead rely on the type of the enclosing module resp. the type being checked
* against, and apply an open-world assumption when determining possible dispatch
* targets.
*/
pragma[nomagic]
private DataFlow::Node trackInstanceRec(Module tp, TypeTracker t, boolean exact, StepSummary summary) {
exists(DataFlow::Node mid | mid = trackInstance(tp, exact, t) |
StepSummary::smallstep(mid, result, summary)
or
localFlowStep(mid, result, summary)
) and
not result instanceof SelfParameterNode and
not hasAdjacentTypeCheckedReads(_, _, result.asExpr(), _)
}
pragma[nomagic]
private DataFlow::Node trackInstance(Module tp, boolean exact) {
result = trackInstance(tp, exact, TypeTracker::end())
}
pragma[nomagic]
private DataFlow::LocalSourceNode trackBlock(Block block, TypeTracker t) {
t.start() and result.asExpr().getExpr() = block
or
@@ -373,84 +596,190 @@ private DataFlow::LocalSourceNode trackBlockRec(Block block, TypeTracker t, Step
StepSummary::step(trackBlock(block, t), result, summary)
}
pragma[nomagic]
private DataFlow::LocalSourceNode trackBlock(Block block) {
result = trackBlock(block, TypeTracker::end())
}
private predicate singletonMethod(MethodBase method, Expr object) {
object = method.(SingletonMethod).getObject()
or
exists(SingletonClass cls |
object = cls.getValue() and method instanceof Method and method = cls.getAMethod()
/** Holds if `m` is a singleton method named `name`, defined on `object. */
private predicate singletonMethod(MethodBase m, string name, Expr object) {
name = m.getName() and
(
object = m.(SingletonMethod).getObject()
or
m = any(SingletonClass cls | object = cls.getValue()).getAMethod().(Method)
)
}
pragma[nomagic]
private predicate flowsToSingletonMethodObject(DataFlow::LocalSourceNode nodeFrom, MethodBase method) {
private predicate flowsToSingletonMethodObject(
DataFlow::LocalSourceNode nodeFrom, MethodBase m, string name
) {
exists(DataFlow::Node nodeTo |
nodeFrom.flowsTo(nodeTo) and
singletonMethod(method, nodeTo.asExpr().getExpr())
singletonMethod(m, name, nodeTo.asExpr().getExpr())
)
}
/**
* Holds if `method` is a singleton method named `name`, defined on module
* `m`:
*
* ```rb
* class C
* def self.m1; end # included
*
* class << self
* def m2; end # included
* end
* end
*
* def C.m3; end # included
*
* c_alias = C
* def c_alias.m4; end # included
*
* c = C.new
* def c.m5; end # not included
*
* class << c
* def m6; end # not included
* end
* ```
*/
pragma[nomagic]
private predicate moduleFlowsToSingletonMethodObject(Module m, MethodBase method) {
flowsToSingletonMethodObject(trackModule(m), method)
}
pragma[nomagic]
private DataFlow::LocalSourceNode trackSingletonMethod0(MethodBase method, TypeTracker t) {
t.start() and
(
flowsToSingletonMethodObject(result, method)
or
exists(Module m | result = trackModule(m) and moduleFlowsToSingletonMethodObject(m, method))
private predicate singletonMethodOnModule(MethodBase method, string name, Module m) {
exists(Expr object |
singletonMethod(method, name, object) and
selfInModule(object.(SelfVariableReadAccess).getVariable(), m)
)
or
exists(TypeTracker t2, StepSummary summary |
result = trackSingletonMethod0Rec(method, t2, summary) and t = t2.append(summary)
)
flowsToSingletonMethodObject(trackModuleAccess(m), method, name)
}
/**
* Holds if `method` is a singleton method named `name`, defined on expression
* `object`, where `object` is not likely to resolve to a module:
*
* ```rb
* class C
* def self.m1; end # not included
*
* class << self
* def m2; end # not included
* end
* end
*
* def C.m3; end # not included
*
* c_alias = C
* def c_alias.m4; end # included (due to negative recursion limitation)
*
* c = C.new
* def c.m5; end # included
*
* class << c
* def m6; end # included
* end
* ```
*/
pragma[nomagic]
private DataFlow::LocalSourceNode trackSingletonMethod0Rec(
MethodBase method, TypeTracker t, StepSummary summary
predicate singletonMethodOnInstance(MethodBase method, string name, Expr object) {
singletonMethod(method, name, object) and
not selfInModule(object.(SelfVariableReadAccess).getVariable(), _) and
// cannot use `trackModuleAccess` because of negative recursion
not exists(resolveConstantReadAccess(object))
}
/**
* Holds if there is reverse flow from `nodeFrom` to `nodeTo` via a parameter.
*
* This is only used for tracking singleton methods, where we want to be able
* to handle cases like
*
* ```rb
* def add_singleton x
* def x.foo; end
* end
*
* y = add_singleton C.new
* y.foo
* ```
*
* and
*
* ```rb
* class C
* def add_singleton_to_self
* def self.foo; end
* end
* end
*
* y = C.new
* y.add_singleton_to_self
* y.foo
* ```
*/
pragma[nomagic]
private predicate paramReturnFlow(
DataFlow::Node nodeFrom, DataFlow::PostUpdateNode nodeTo, StepSummary summary
) {
StepSummary::step(trackSingletonMethod0(method, t), result, summary)
exists(
CfgNodes::ExprNodes::CallCfgNode call, DataFlow::Node arg, DataFlow::ParameterNode p,
Expr nodeFromPreExpr
|
TypeTrackerSpecific::callStep(call, arg, p) and
nodeTo.getPreUpdateNode() = arg and
summary.toString() = "return" and
(
nodeFromPreExpr = nodeFrom.(DataFlow::PostUpdateNode).getPreUpdateNode().asExpr().getExpr()
or
nodeFromPreExpr = nodeFrom.asExpr().getExpr() and
singletonMethodOnInstance(_, _, nodeFromPreExpr)
)
|
nodeFromPreExpr = p.getParameter().(NamedParameter).getVariable().getAnAccess()
or
nodeFromPreExpr = p.(SelfParameterNode).getSelfVariable().getAnAccess()
)
}
pragma[nomagic]
private DataFlow::LocalSourceNode trackSingletonMethod(MethodBase m, string name) {
result = trackSingletonMethod0(m, TypeTracker::end()) and
name = m.getName()
}
private SsaSelfDefinitionNode selfInModule(Module tp) {
tp = result.getSelfScope().(ModuleBase).getModule()
}
private DataFlow::LocalSourceNode trackModule(Module tp, TypeTracker t) {
private DataFlow::Node trackSingletonMethodOnInstance(MethodBase method, string name, TypeTracker t) {
t.start() and
(
// ConstantReadAccess to Module
resolveConstantReadAccess(result.asExpr().getExpr()) = tp
or
// `self` reference to Module
result = selfInModule(tp)
)
singletonMethodOnInstance(method, name, result.asExpr().getExpr())
or
exists(TypeTracker t2, StepSummary summary |
result = trackModuleRec(tp, t2, summary) and t = t2.append(summary)
result = trackSingletonMethodOnInstanceRec(method, name, t2, summary) and
t = t2.append(summary) and
// Stop flow at redefinitions.
//
// Example:
// ```rb
// def x.foo; end
// def x.foo; end
// x.foo # <- we want to resolve this call to the second definition only
// ```
not singletonMethodOnInstance(_, name, result.asExpr().getExpr())
)
}
pragma[nomagic]
private DataFlow::LocalSourceNode trackModuleRec(Module tp, TypeTracker t, StepSummary summary) {
StepSummary::step(trackModule(tp, t), result, summary)
private DataFlow::Node trackSingletonMethodOnInstanceRec(
MethodBase method, string name, TypeTracker t, StepSummary summary
) {
exists(DataFlow::Node mid | mid = trackSingletonMethodOnInstance(method, name, t) |
StepSummary::smallstep(mid, result, summary)
or
paramReturnFlow(mid, result, summary)
or
localFlowStep(mid, result, summary)
)
}
private DataFlow::LocalSourceNode trackModule(Module tp) {
result = trackModule(tp, TypeTracker::end())
pragma[nomagic]
private DataFlow::Node trackSingletonMethodOnInstance(MethodBase method, string name) {
result = trackSingletonMethodOnInstance(method, name, TypeTracker::end())
}
/**
@@ -565,6 +894,12 @@ class ArgumentPosition extends TArgumentPosition {
}
}
pragma[nomagic]
private predicate parameterPositionIsNotSelf(ParameterPosition ppos) { not ppos.isSelf() }
pragma[nomagic]
private predicate argumentPositionIsNotSelf(ArgumentPosition apos) { not apos.isSelf() }
/** Holds if arguments at position `apos` match parameters at position `ppos`. */
pragma[nomagic]
predicate parameterMatch(ParameterPosition ppos, ArgumentPosition apos) {
@@ -582,9 +917,9 @@ predicate parameterMatch(ParameterPosition ppos, ArgumentPosition apos) {
or
ppos.isHashSplat() and apos.isHashSplat()
or
ppos.isAny() and not apos.isSelf()
ppos.isAny() and argumentPositionIsNotSelf(apos)
or
apos.isAny() and not ppos.isSelf()
apos.isAny() and parameterPositionIsNotSelf(ppos)
or
ppos.isAnyNamed() and apos.isKeyword(_)
or

File diff suppressed because it is too large Load Diff

View File

@@ -203,6 +203,12 @@ private class Argument extends CfgNodes::ExprCfgNode {
}
}
/** Holds if `n` is not a constant expression. */
predicate isNonConstantExpr(CfgNodes::ExprCfgNode n) {
not exists(n.getConstantValue()) and
not n.getExpr() instanceof ConstantAccess
}
/** A collection of cached types and predicates to be evaluated in the same stage. */
cached
private module Cached {
@@ -232,8 +238,12 @@ private module Cached {
isParameterNode(_, c, any(ParameterPosition p | p.isKeyword(_)))
} or
TExprPostUpdateNode(CfgNodes::ExprCfgNode n) {
n instanceof Argument or
n = any(CfgNodes::ExprNodes::InstanceVariableAccessCfgNode v).getReceiver()
// filter out nodes that clearly don't need post-update nodes
isNonConstantExpr(n) and
(
n instanceof Argument or
n = any(CfgNodes::ExprNodes::InstanceVariableAccessCfgNode v).getReceiver()
)
} or
TSummaryNode(
FlowSummaryImpl::Public::SummarizedCallable c,
@@ -438,6 +448,9 @@ class SsaDefinitionNode extends NodeImpl, TSsaDefinitionNode {
/** Gets the underlying SSA definition. */
Ssa::Definition getDefinition() { result = def }
/** Gets the underlying variable. */
Variable getVariable() { result = def.getSourceVariable() }
override CfgScope getCfgScope() { result = def.getBasicBlock().getScope() }
override Location getLocationImpl() { result = def.getLocation() }
@@ -539,6 +552,9 @@ private module ParameterNodes {
final MethodBase getMethod() { result = method }
/** Gets the underlying `self` variable. */
final SelfVariable getSelfVariable() { result.getDeclaringScope() = method }
override Parameter getParameter() { none() }
override predicate isParameterOf(DataFlowCallable c, ParameterPosition pos) {
@@ -613,7 +629,7 @@ private module ParameterNodes {
* where direct keyword matching is possible, since we construct a synthesized hash
* splat argument (`SynthHashSplatArgumentNode`) at the call site, which means that
* `taint(1)` will flow into `p1` both via normal keyword matching and via the synthesized
* nodes (and similarly for `p2`). However, this redunancy is OK since
* nodes (and similarly for `p2`). However, this redundancy is OK since
* (a) it means that type-tracking through keyword arguments also works in most cases,
* (b) read/store steps can be avoided when direct keyword matching is possible, and
* hence access path limits are not a concern, and
@@ -1036,7 +1052,8 @@ predicate readStep(Node node1, ContentSet c, Node node2) {
// (instance variable assignment or setter method call).
node2.asExpr() =
any(CfgNodes::ExprNodes::MethodCallCfgNode call |
node1.asExpr() = call.getReceiver() and
node1.asExpr() =
any(CfgNodes::ExprCfgNode e | e = call.getReceiver() and isNonConstantExpr(e)) and
call.getNumberOfArguments() = 0 and
c.isSingleton(any(Content::FieldContent ct |
ct.getName() = "@" + call.getExpr().getMethodName()

View File

@@ -87,6 +87,41 @@ class CallNode extends LocalSourceNode, ExprNode {
/** Gets the block of this call. */
Node getBlock() { result.asExpr() = node.getBlock() }
/**
* Gets the data-flow node corresponding to the named argument of the call
* corresponding to this data-flow node, also including values passed with (pre Ruby
* 2.0) hash arguments.
*
* Such hash arguments are tracked back to their source location within functions, but
* no inter-procedural analysis occurs.
*
* This means all 3 variants below will be handled by this predicate:
*
* ```ruby
* foo(..., some_option: 42)
* foo(..., { some_option: 42 })
* options = { some_option: 42 }
* foo(..., options)
* ```
*/
Node getKeywordArgumentIncludeHashArgument(string name) {
// to reduce number of computed tuples, I have put bindingset on both this and name,
// meaning we only do the local backwards tracking for known calls and known names.
// (not because a performance problem was seen, it just seemed right).
result = this.getKeywordArgument(name)
or
exists(CfgNodes::ExprNodes::PairCfgNode pair |
pair =
this.getArgument(_)
.getALocalSource()
.asExpr()
.(CfgNodes::ExprNodes::HashLiteralCfgNode)
.getAKeyValuePair() and
pair.getKey().getConstantValue().isStringlikeValue(name) and
result.asExpr() = pair.getValue()
)
}
}
/**

View File

@@ -226,15 +226,6 @@ module Public {
none()
}
/**
* Holds if values stored inside `content` are cleared on objects passed as
* arguments at position `pos` to this callable.
*
* TODO: Remove once all languages support `WithoutContent` tokens.
*/
pragma[nomagic]
predicate clearsContent(ParameterPosition pos, ContentSet content) { none() }
/**
* Holds if the summary is auto generated.
*/
@@ -328,23 +319,6 @@ module Private {
SummaryComponentStack::singleton(TArgumentSummaryComponent(_))) and
preservesValue = preservesValue1.booleanAnd(preservesValue2)
)
or
exists(ParameterPosition ppos, ContentSet cs |
c.clearsContent(ppos, cs) and
input = SummaryComponentStack::push(SummaryComponent::withoutContent(cs), output) and
output = SummaryComponentStack::argument(ppos) and
preservesValue = true
)
}
private class MkClearStack extends RequiredSummaryComponentStack {
override predicate required(SummaryComponent head, SummaryComponentStack tail) {
exists(SummarizedCallable sc, ParameterPosition ppos, ContentSet cs |
sc.clearsContent(ppos, cs) and
head = SummaryComponent::withoutContent(cs) and
tail = SummaryComponentStack::argument(ppos)
)
}
}
/**
@@ -945,8 +919,7 @@ module Private {
AccessPath inSpec, AccessPath outSpec, string kind
) {
summaryElement(this, inSpec, outSpec, kind, true) and
not summaryElement(this, _, _, _, false) and
not this.clearsContent(_, _)
not summaryElement(this, _, _, _, false)
}
private predicate relevantSummaryElement(AccessPath inSpec, AccessPath outSpec, string kind) {

View File

@@ -1,19 +1,72 @@
private import SsaImplCommon
private import SsaImplSpecific as SsaImplSpecific
private import codeql.ssa.Ssa as SsaImplCommon
private import codeql.ruby.AST
private import codeql.ruby.CFG
private import codeql.ruby.CFG as CFG
private import codeql.ruby.ast.Variable
private import CfgNodes::ExprNodes
private import CFG::CfgNodes::ExprNodes
private module SsaInput implements SsaImplCommon::InputSig {
private import codeql.ruby.controlflow.BasicBlocks as BasicBlocks
class BasicBlock = BasicBlocks::BasicBlock;
BasicBlock getImmediateBasicBlockDominator(BasicBlock bb) { result = bb.getImmediateDominator() }
BasicBlock getABasicBlockSuccessor(BasicBlock bb) { result = bb.getASuccessor() }
class ExitBasicBlock = BasicBlocks::ExitBasicBlock;
class SourceVariable = LocalVariable;
/**
* Holds if the statement at index `i` of basic block `bb` contains a write to variable `v`.
* `certain` is true if the write definitely occurs.
*/
predicate variableWrite(BasicBlock bb, int i, SourceVariable v, boolean certain) {
(
exists(Scope scope | scope = v.(SelfVariable).getDeclaringScope() |
// We consider the `self` variable to have a single write at the entry to a method block...
scope = bb.(BasicBlocks::EntryBasicBlock).getScope() and
i = 0
or
// ...or a class or module block.
bb.getNode(i).getNode() = scope.(ModuleBase).getAControlFlowEntryNode()
)
or
uninitializedWrite(bb, i, v)
or
capturedEntryWrite(bb, i, v)
or
variableWriteActual(bb, i, v, _)
) and
certain = true
or
capturedCallWrite(_, bb, i, v) and
certain = false
}
predicate variableRead(BasicBlock bb, int i, LocalVariable v, boolean certain) {
variableReadActual(bb, i, v) and
certain = true
or
capturedCallRead(_, bb, i, v) and
certain = false
or
capturedExitRead(bb, i, v) and
certain = false
}
}
import SsaImplCommon::Make<SsaInput>
/** Holds if `v` is uninitialized at index `i` in entry block `bb`. */
predicate uninitializedWrite(EntryBasicBlock bb, int i, LocalVariable v) {
predicate uninitializedWrite(CFG::EntryBasicBlock bb, int i, LocalVariable v) {
v.getDeclaringScope() = bb.getScope() and
i = -1
}
/** Holds if `bb` contains a caputured read of variable `v`. */
pragma[noinline]
private predicate hasCapturedVariableRead(BasicBlock bb, LocalVariable v) {
private predicate hasCapturedVariableRead(CFG::BasicBlock bb, LocalVariable v) {
exists(LocalVariableReadAccess read |
read = bb.getANode().getNode() and
read.isCapturedAccess() and
@@ -23,7 +76,7 @@ private predicate hasCapturedVariableRead(BasicBlock bb, LocalVariable v) {
/** Holds if `bb` contains a caputured write to variable `v`. */
pragma[noinline]
private predicate writesCapturedVariable(BasicBlock bb, LocalVariable v) {
private predicate writesCapturedVariable(CFG::BasicBlock bb, LocalVariable v) {
exists(LocalVariableWriteAccess write |
write = bb.getANode().getNode() and
write.isCapturedAccess() and
@@ -35,7 +88,7 @@ private predicate writesCapturedVariable(BasicBlock bb, LocalVariable v) {
* Holds if a pseudo read of captured variable `v` should be inserted
* at index `i` in exit block `bb`.
*/
private predicate capturedExitRead(AnnotatedExitBasicBlock bb, int i, LocalVariable v) {
private predicate capturedExitRead(CFG::AnnotatedExitBasicBlock bb, int i, LocalVariable v) {
bb.isNormal() and
writesCapturedVariable(bb.getAPredecessor*(), v) and
i = bb.length()
@@ -46,7 +99,7 @@ private predicate capturedExitRead(AnnotatedExitBasicBlock bb, int i, LocalVaria
* or inside a (transitively) nested scope of `scope`.
*/
pragma[noinline]
private predicate hasCapturedRead(Variable v, CfgScope scope) {
private predicate hasCapturedRead(Variable v, CFG::CfgScope scope) {
any(LocalVariableReadAccess read |
read.getVariable() = v and scope = read.getCfgScope().getOuterCfgScope*()
).isCapturedAccess()
@@ -57,13 +110,15 @@ private predicate hasCapturedRead(Variable v, CfgScope scope) {
* outer scope of `scope`.
*/
pragma[noinline]
private predicate variableWriteInOuterScope(BasicBlock bb, LocalVariable v, CfgScope scope) {
SsaImplSpecific::variableWrite(bb, _, v, _) and
private predicate variableWriteInOuterScope(CFG::BasicBlock bb, LocalVariable v, CFG::CfgScope scope) {
SsaInput::variableWrite(bb, _, v, _) and
scope.getOuterCfgScope() = bb.getScope()
}
pragma[noinline]
private predicate hasVariableWriteWithCapturedRead(BasicBlock bb, LocalVariable v, CfgScope scope) {
private predicate hasVariableWriteWithCapturedRead(
CFG::BasicBlock bb, LocalVariable v, CFG::CfgScope scope
) {
hasCapturedRead(v, scope) and
variableWriteInOuterScope(bb, v, scope)
}
@@ -72,10 +127,8 @@ private predicate hasVariableWriteWithCapturedRead(BasicBlock bb, LocalVariable
* Holds if the call `call` at index `i` in basic block `bb` may reach
* a callable that reads captured variable `v`.
*/
private predicate capturedCallRead(
CfgNodes::ExprNodes::CallCfgNode call, BasicBlock bb, int i, LocalVariable v
) {
exists(CfgScope scope |
private predicate capturedCallRead(CallCfgNode call, CFG::BasicBlock bb, int i, LocalVariable v) {
exists(CFG::CfgScope scope |
hasVariableWriteWithCapturedRead(bb.getAPredecessor*(), v, scope) and
call = bb.getNode(i)
|
@@ -88,30 +141,19 @@ private predicate capturedCallRead(
}
/** Holds if `v` is read at index `i` in basic block `bb`. */
private predicate variableReadActual(BasicBlock bb, int i, LocalVariable v) {
private predicate variableReadActual(CFG::BasicBlock bb, int i, LocalVariable v) {
exists(VariableReadAccess read |
read.getVariable() = v and
read = bb.getNode(i).getNode()
)
}
predicate variableRead(BasicBlock bb, int i, LocalVariable v, boolean certain) {
variableReadActual(bb, i, v) and
certain = true
or
capturedCallRead(_, bb, i, v) and
certain = false
or
capturedExitRead(bb, i, v) and
certain = false
}
/**
* Holds if captured variable `v` is written directly inside `scope`,
* or inside a (transitively) nested scope of `scope`.
*/
pragma[noinline]
private predicate hasCapturedWrite(Variable v, CfgScope scope) {
private predicate hasCapturedWrite(Variable v, CFG::CfgScope scope) {
any(LocalVariableWriteAccess write |
write.getVariable() = v and scope = write.getCfgScope().getOuterCfgScope*()
).isCapturedAccess()
@@ -122,13 +164,17 @@ private predicate hasCapturedWrite(Variable v, CfgScope scope) {
* outer scope of `scope`.
*/
pragma[noinline]
private predicate variableReadActualInOuterScope(BasicBlock bb, LocalVariable v, CfgScope scope) {
private predicate variableReadActualInOuterScope(
CFG::BasicBlock bb, LocalVariable v, CFG::CfgScope scope
) {
variableReadActual(bb, _, v) and
bb.getScope() = scope.getOuterCfgScope()
}
pragma[noinline]
private predicate hasVariableReadWithCapturedWrite(BasicBlock bb, LocalVariable v, CfgScope scope) {
private predicate hasVariableReadWithCapturedWrite(
CFG::BasicBlock bb, LocalVariable v, CFG::CfgScope scope
) {
hasCapturedWrite(v, scope) and
variableReadActualInOuterScope(bb, v, scope)
}
@@ -140,7 +186,7 @@ private module Cached {
* `i` in entry block `bb`.
*/
cached
predicate capturedEntryWrite(EntryBasicBlock bb, int i, LocalVariable v) {
predicate capturedEntryWrite(CFG::EntryBasicBlock bb, int i, LocalVariable v) {
hasCapturedVariableRead(bb.getASuccessor*(), v) and
i = -1
}
@@ -150,10 +196,8 @@ private module Cached {
* that writes captured variable `v`.
*/
cached
predicate capturedCallWrite(
CfgNodes::ExprNodes::CallCfgNode call, BasicBlock bb, int i, LocalVariable v
) {
exists(CfgScope scope |
predicate capturedCallWrite(CallCfgNode call, CFG::BasicBlock bb, int i, LocalVariable v) {
exists(CFG::CfgScope scope |
hasVariableReadWithCapturedWrite(bb.getASuccessor*(), v, scope) and
call = bb.getNode(i)
|
@@ -170,7 +214,9 @@ private module Cached {
* AST write access is `write`.
*/
cached
predicate variableWriteActual(BasicBlock bb, int i, LocalVariable v, VariableWriteAccess write) {
predicate variableWriteActual(
CFG::BasicBlock bb, int i, LocalVariable v, VariableWriteAccess write
) {
exists(AstNode n |
write.getVariable() = v and
n = bb.getNode(i).getNode()
@@ -184,7 +230,7 @@ private module Cached {
cached
VariableReadAccessCfgNode getARead(Definition def) {
exists(LocalVariable v, BasicBlock bb, int i |
exists(LocalVariable v, CFG::BasicBlock bb, int i |
ssaDefReachesRead(v, def, bb, i) and
variableReadActual(bb, i, v) and
result = bb.getNode(i)
@@ -193,9 +239,9 @@ private module Cached {
pragma[noinline]
private predicate defReachesCallReadInOuterScope(
Definition def, CfgNodes::ExprNodes::CallCfgNode call, LocalVariable v, CfgScope scope
Definition def, CallCfgNode call, LocalVariable v, CFG::CfgScope scope
) {
exists(BasicBlock bb, int i |
exists(CFG::BasicBlock bb, int i |
ssaDefReachesRead(v, def, bb, i) and
capturedCallRead(call, bb, i, v) and
scope.getOuterCfgScope() = bb.getScope()
@@ -203,8 +249,8 @@ private module Cached {
}
pragma[noinline]
private predicate hasCapturedEntryWrite(Definition entry, LocalVariable v, CfgScope scope) {
exists(BasicBlock bb, int i |
private predicate hasCapturedEntryWrite(Definition entry, LocalVariable v, CFG::CfgScope scope) {
exists(CFG::BasicBlock bb, int i |
capturedEntryWrite(bb, i, v) and
entry.definesAt(v, bb, i) and
bb.getScope().getOuterCfgScope*() = scope
@@ -221,8 +267,8 @@ private module Cached {
* ```
*/
cached
predicate captureFlowIn(CfgNodes::ExprNodes::CallCfgNode call, Definition def, Definition entry) {
exists(LocalVariable v, CfgScope scope |
predicate captureFlowIn(CallCfgNode call, Definition def, Definition entry) {
exists(LocalVariable v, CFG::CfgScope scope |
defReachesCallReadInOuterScope(def, call, v, scope) and
hasCapturedEntryWrite(entry, v, scope)
|
@@ -237,8 +283,10 @@ private module Cached {
private import codeql.ruby.dataflow.SSA
pragma[noinline]
private predicate defReachesExitReadInInnerScope(Definition def, LocalVariable v, CfgScope scope) {
exists(BasicBlock bb, int i |
private predicate defReachesExitReadInInnerScope(
Definition def, LocalVariable v, CFG::CfgScope scope
) {
exists(CFG::BasicBlock bb, int i |
ssaDefReachesRead(v, def, bb, i) and
capturedExitRead(bb, i, v) and
scope = bb.getScope().getOuterCfgScope*()
@@ -247,9 +295,9 @@ private module Cached {
pragma[noinline]
private predicate hasCapturedExitRead(
Definition exit, CfgNodes::ExprNodes::CallCfgNode call, LocalVariable v, CfgScope scope
Definition exit, CallCfgNode call, LocalVariable v, CFG::CfgScope scope
) {
exists(BasicBlock bb, int i |
exists(CFG::BasicBlock bb, int i |
capturedCallWrite(call, bb, i, v) and
exit.definesAt(v, bb, i) and
bb.getScope() = scope.getOuterCfgScope()
@@ -267,8 +315,8 @@ private module Cached {
* ```
*/
cached
predicate captureFlowOut(CfgNodes::ExprNodes::CallCfgNode call, Definition def, Definition exit) {
exists(LocalVariable v, CfgScope scope |
predicate captureFlowOut(CallCfgNode call, Definition def, Definition exit) {
exists(LocalVariable v, CFG::CfgScope scope |
defReachesExitReadInInnerScope(def, v, scope) and
hasCapturedExitRead(exit, call, v, _)
|
@@ -281,7 +329,7 @@ private module Cached {
}
cached
Definition phiHasInputFromBlock(PhiNode phi, BasicBlock bb) {
Definition phiHasInputFromBlock(PhiNode phi, CFG::BasicBlock bb) {
phiHasInputFromBlock(phi, result, bb)
}
@@ -291,7 +339,7 @@ private module Cached {
*/
cached
predicate firstRead(Definition def, VariableReadAccessCfgNode read) {
exists(BasicBlock bb1, int i1, BasicBlock bb2, int i2 |
exists(CFG::BasicBlock bb1, int i1, CFG::BasicBlock bb2, int i2 |
def.definesAt(_, bb1, i1) and
adjacentDefNoUncertainReads(def, bb1, i1, bb2, i2) and
read = bb2.getNode(i2)
@@ -307,7 +355,7 @@ private module Cached {
predicate adjacentReadPair(
Definition def, VariableReadAccessCfgNode read1, VariableReadAccessCfgNode read2
) {
exists(BasicBlock bb1, int i1, BasicBlock bb2, int i2 |
exists(CFG::BasicBlock bb1, int i1, CFG::BasicBlock bb2, int i2 |
read1 = bb1.getNode(i1) and
variableReadActual(bb1, i1, _) and
adjacentDefNoUncertainReads(def, bb1, i1, bb2, i2) and
@@ -322,7 +370,7 @@ private module Cached {
*/
cached
predicate lastRead(Definition def, VariableReadAccessCfgNode read) {
exists(BasicBlock bb, int i |
exists(CFG::BasicBlock bb, int i |
lastRefNoUncertainReads(def, bb, i) and
variableReadActual(bb, i, _) and
read = bb.getNode(i)
@@ -337,7 +385,7 @@ private module Cached {
* The reference is either a read of `def` or `def` itself.
*/
cached
predicate lastRefBeforeRedef(Definition def, BasicBlock bb, int i, Definition next) {
predicate lastRefBeforeRedef(Definition def, CFG::BasicBlock bb, int i, Definition next) {
lastRefRedefNoUncertainReads(def, bb, i, next)
}

View File

@@ -1,795 +0,0 @@
/**
* Provides a language-independent implementation of static single assignment
* (SSA) form.
*/
private import SsaImplSpecific
private BasicBlock getABasicBlockPredecessor(BasicBlock bb) { getABasicBlockSuccessor(result) = bb }
/**
* Liveness analysis (based on source variables) to restrict the size of the
* SSA representation.
*/
private module Liveness {
/**
* A classification of variable references into reads (of a given kind) and
* (certain or uncertain) writes.
*/
private newtype TRefKind =
Read(boolean certain) { certain in [false, true] } or
Write(boolean certain) { certain in [false, true] }
private class RefKind extends TRefKind {
string toString() {
exists(boolean certain | this = Read(certain) and result = "read (" + certain + ")")
or
exists(boolean certain | this = Write(certain) and result = "write (" + certain + ")")
}
int getOrder() {
this = Read(_) and
result = 0
or
this = Write(_) and
result = 1
}
}
/**
* Holds if the `i`th node of basic block `bb` is a reference to `v` of kind `k`.
*/
predicate ref(BasicBlock bb, int i, SourceVariable v, RefKind k) {
exists(boolean certain | variableRead(bb, i, v, certain) | k = Read(certain))
or
exists(boolean certain | variableWrite(bb, i, v, certain) | k = Write(certain))
}
private newtype OrderedRefIndex =
MkOrderedRefIndex(int i, int tag) {
exists(RefKind rk | ref(_, i, _, rk) | tag = rk.getOrder())
}
private OrderedRefIndex refOrd(BasicBlock bb, int i, SourceVariable v, RefKind k, int ord) {
ref(bb, i, v, k) and
result = MkOrderedRefIndex(i, ord) and
ord = k.getOrder()
}
/**
* Gets the (1-based) rank of the reference to `v` at the `i`th node of
* basic block `bb`, which has the given reference kind `k`.
*
* Reads are considered before writes when they happen at the same index.
*/
private int refRank(BasicBlock bb, int i, SourceVariable v, RefKind k) {
refOrd(bb, i, v, k, _) =
rank[result](int j, int ord, OrderedRefIndex res |
res = refOrd(bb, j, v, _, ord)
|
res order by j, ord
)
}
private int maxRefRank(BasicBlock bb, SourceVariable v) {
result = refRank(bb, _, v, _) and
not result + 1 = refRank(bb, _, v, _)
}
predicate lastRefIsRead(BasicBlock bb, SourceVariable v) {
maxRefRank(bb, v) = refRank(bb, _, v, Read(_))
}
/**
* Gets the (1-based) rank of the first reference to `v` inside basic block `bb`
* that is either a read or a certain write.
*/
private int firstReadOrCertainWrite(BasicBlock bb, SourceVariable v) {
result =
min(int r, RefKind k |
r = refRank(bb, _, v, k) and
k != Write(false)
|
r
)
}
/**
* Holds if source variable `v` is live at the beginning of basic block `bb`.
*/
predicate liveAtEntry(BasicBlock bb, SourceVariable v) {
// The first read or certain write to `v` inside `bb` is a read
refRank(bb, _, v, Read(_)) = firstReadOrCertainWrite(bb, v)
or
// There is no certain write to `v` inside `bb`, but `v` is live at entry
// to a successor basic block of `bb`
not exists(firstReadOrCertainWrite(bb, v)) and
liveAtExit(bb, v)
}
/**
* Holds if source variable `v` is live at the end of basic block `bb`.
*/
predicate liveAtExit(BasicBlock bb, SourceVariable v) {
liveAtEntry(getABasicBlockSuccessor(bb), v)
}
/**
* Holds if variable `v` is live in basic block `bb` at index `i`.
* The rank of `i` is `rnk` as defined by `refRank()`.
*/
private predicate liveAtRank(BasicBlock bb, int i, SourceVariable v, int rnk) {
exists(RefKind kind | rnk = refRank(bb, i, v, kind) |
rnk = maxRefRank(bb, v) and
liveAtExit(bb, v)
or
ref(bb, i, v, kind) and
kind = Read(_)
or
exists(RefKind nextKind |
liveAtRank(bb, _, v, rnk + 1) and
rnk + 1 = refRank(bb, _, v, nextKind) and
nextKind != Write(true)
)
)
}
/**
* Holds if variable `v` is live after the (certain or uncertain) write at
* index `i` inside basic block `bb`.
*/
predicate liveAfterWrite(BasicBlock bb, int i, SourceVariable v) {
exists(int rnk | rnk = refRank(bb, i, v, Write(_)) | liveAtRank(bb, i, v, rnk))
}
}
private import Liveness
/**
* Holds if `df` is in the dominance frontier of `bb`.
*
* This is equivalent to:
*
* ```ql
* bb = getImmediateBasicBlockDominator*(getABasicBlockPredecessor(df)) and
* not bb = getImmediateBasicBlockDominator+(df)
* ```
*/
private predicate inDominanceFrontier(BasicBlock bb, BasicBlock df) {
bb = getABasicBlockPredecessor(df) and not bb = getImmediateBasicBlockDominator(df)
or
exists(BasicBlock prev | inDominanceFrontier(prev, df) |
bb = getImmediateBasicBlockDominator(prev) and
not bb = getImmediateBasicBlockDominator(df)
)
}
/**
* Holds if `bb` is in the dominance frontier of a block containing a
* definition of `v`.
*/
pragma[noinline]
private predicate inDefDominanceFrontier(BasicBlock bb, SourceVariable v) {
exists(BasicBlock defbb, Definition def |
def.definesAt(v, defbb, _) and
inDominanceFrontier(defbb, bb)
)
}
cached
newtype TDefinition =
TWriteDef(SourceVariable v, BasicBlock bb, int i) {
variableWrite(bb, i, v, _) and
liveAfterWrite(bb, i, v)
} or
TPhiNode(SourceVariable v, BasicBlock bb) {
inDefDominanceFrontier(bb, v) and
liveAtEntry(bb, v)
}
private module SsaDefReaches {
newtype TSsaRefKind =
SsaActualRead() or
SsaPhiRead() or
SsaDef()
class SsaRead = SsaActualRead or SsaPhiRead;
/**
* A classification of SSA variable references into reads and definitions.
*/
class SsaRefKind extends TSsaRefKind {
string toString() {
this = SsaActualRead() and
result = "SsaActualRead"
or
this = SsaPhiRead() and
result = "SsaPhiRead"
or
this = SsaDef() and
result = "SsaDef"
}
int getOrder() {
this instanceof SsaRead and
result = 0
or
this = SsaDef() and
result = 1
}
}
/**
* Holds if `bb` is in the dominance frontier of a block containing a
* read of `v`.
*/
pragma[nomagic]
private predicate inReadDominanceFrontier(BasicBlock bb, SourceVariable v) {
exists(BasicBlock readbb | inDominanceFrontier(readbb, bb) |
lastRefIsRead(readbb, v)
or
phiRead(readbb, v)
)
}
/**
* Holds if a phi-read node should be inserted for variable `v` at the beginning
* of basic block `bb`.
*
* Phi-read nodes are like normal phi nodes, but they are inserted based on reads
* instead of writes, and only if the dominance-frontier block does not already
* contain a reference (read or write) to `v`. Unlike normal phi nodes, this is
* an internal implementation detail that is not exposed.
*
* The motivation for adding phi-reads is to improve performance of the use-use
* calculation in cases where there is a large number of reads that can reach the
* same join-point, and from there reach a large number of basic blocks. Example:
*
* ```cs
* if (a)
* use(x);
* else if (b)
* use(x);
* else if (c)
* use(x);
* else if (d)
* use(x);
* // many more ifs ...
*
* // phi-read for `x` inserted here
*
* // program not mentioning `x`, with large basic block graph
*
* use(x);
* ```
*
* Without phi-reads, the analysis has to replicate reachability for each of
* the guarded uses of `x`. However, with phi-reads, the analysis will limit
* each conditional use of `x` to reach the basic block containing the phi-read
* node for `x`, and only that basic block will have to compute reachability
* through the remainder of the large program.
*
* Like normal reads, each phi-read node `phi-read` can be reached from exactly
* one SSA definition (without passing through another definition): Assume, for
* the sake of contradiction, that there are two reaching definitions `def1` and
* `def2`. Now, if both `def1` and `def2` dominate `phi-read`, then the nearest
* dominating definition will prevent the other from reaching `phi-read`. So, at
* least one of `def1` and `def2` cannot dominate `phi-read`; assume it is `def1`.
* Then `def1` must go through one of its dominance-frontier blocks in order to
* reach `phi-read`. However, such a block will always start with a (normal) phi
* node, which contradicts reachability.
*
* Also, like normal reads, the unique SSA definition `def` that reaches `phi-read`,
* will dominate `phi-read`. Assuming it doesn't means that the path from `def`
* to `phi-read` goes through a dominance-frontier block, and hence a phi node,
* which contradicts reachability.
*/
pragma[nomagic]
predicate phiRead(BasicBlock bb, SourceVariable v) {
inReadDominanceFrontier(bb, v) and
liveAtEntry(bb, v) and
// only if there are no other references to `v` inside `bb`
not ref(bb, _, v, _) and
not exists(Definition def | def.definesAt(v, bb, _))
}
/**
* Holds if the `i`th node of basic block `bb` is a reference to `v`,
* either a read (when `k` is `SsaRead()`) or an SSA definition (when `k`
* is `SsaDef()`).
*
* Unlike `Liveness::ref`, this includes `phi` nodes.
*/
pragma[nomagic]
predicate ssaRef(BasicBlock bb, int i, SourceVariable v, SsaRefKind k) {
variableRead(bb, i, v, _) and
k = SsaActualRead()
or
phiRead(bb, v) and
i = -1 and
k = SsaPhiRead()
or
any(Definition def).definesAt(v, bb, i) and
k = SsaDef()
}
private newtype OrderedSsaRefIndex =
MkOrderedSsaRefIndex(int i, SsaRefKind k) { ssaRef(_, i, _, k) }
private OrderedSsaRefIndex ssaRefOrd(BasicBlock bb, int i, SourceVariable v, SsaRefKind k, int ord) {
ssaRef(bb, i, v, k) and
result = MkOrderedSsaRefIndex(i, k) and
ord = k.getOrder()
}
/**
* Gets the (1-based) rank of the reference to `v` at the `i`th node of basic
* block `bb`, which has the given reference kind `k`.
*
* For example, if `bb` is a basic block with a phi node for `v` (considered
* to be at index -1), reads `v` at node 2, and defines it at node 5, we have:
*
* ```ql
* ssaRefRank(bb, -1, v, SsaDef()) = 1 // phi node
* ssaRefRank(bb, 2, v, Read()) = 2 // read at node 2
* ssaRefRank(bb, 5, v, SsaDef()) = 3 // definition at node 5
* ```
*
* Reads are considered before writes when they happen at the same index.
*/
int ssaRefRank(BasicBlock bb, int i, SourceVariable v, SsaRefKind k) {
ssaRefOrd(bb, i, v, k, _) =
rank[result](int j, int ord, OrderedSsaRefIndex res |
res = ssaRefOrd(bb, j, v, _, ord)
|
res order by j, ord
)
}
int maxSsaRefRank(BasicBlock bb, SourceVariable v) {
result = ssaRefRank(bb, _, v, _) and
not result + 1 = ssaRefRank(bb, _, v, _)
}
/**
* Holds if the SSA definition `def` reaches rank index `rnk` in its own
* basic block `bb`.
*/
predicate ssaDefReachesRank(BasicBlock bb, Definition def, int rnk, SourceVariable v) {
exists(int i |
rnk = ssaRefRank(bb, i, v, SsaDef()) and
def.definesAt(v, bb, i)
)
or
ssaDefReachesRank(bb, def, rnk - 1, v) and
rnk = ssaRefRank(bb, _, v, any(SsaRead k))
}
/**
* Holds if the SSA definition of `v` at `def` reaches index `i` in the same
* basic block `bb`, without crossing another SSA definition of `v`.
*/
predicate ssaDefReachesReadWithinBlock(SourceVariable v, Definition def, BasicBlock bb, int i) {
exists(int rnk |
ssaDefReachesRank(bb, def, rnk, v) and
rnk = ssaRefRank(bb, i, v, any(SsaRead k))
)
}
/**
* Same as `ssaRefRank()`, but restricted to a particular SSA definition `def`.
*/
int ssaDefRank(Definition def, SourceVariable v, BasicBlock bb, int i, SsaRefKind k) {
v = def.getSourceVariable() and
result = ssaRefRank(bb, i, v, k) and
(
ssaDefReachesRead(_, def, bb, i)
or
def.definesAt(_, bb, i)
)
}
/**
* Holds if the reference to `def` at index `i` in basic block `bb` is the
* last reference to `v` inside `bb`.
*/
pragma[noinline]
predicate lastSsaRef(Definition def, SourceVariable v, BasicBlock bb, int i) {
ssaDefRank(def, v, bb, i, _) = maxSsaRefRank(bb, v)
}
predicate defOccursInBlock(Definition def, BasicBlock bb, SourceVariable v, SsaRefKind k) {
exists(ssaDefRank(def, v, bb, _, k))
}
pragma[noinline]
private predicate ssaDefReachesThroughBlock(Definition def, BasicBlock bb) {
ssaDefReachesEndOfBlock(bb, def, _) and
not defOccursInBlock(_, bb, def.getSourceVariable(), _)
}
/**
* Holds if `def` is accessed in basic block `bb1` (either a read or a write),
* `bb2` is a transitive successor of `bb1`, `def` is live at the end of _some_
* predecessor of `bb2`, and the underlying variable for `def` is neither read
* nor written in any block on the path between `bb1` and `bb2`.
*
* Phi reads are considered as normal reads for this predicate.
*/
pragma[nomagic]
private predicate varBlockReachesInclPhiRead(Definition def, BasicBlock bb1, BasicBlock bb2) {
defOccursInBlock(def, bb1, _, _) and
bb2 = getABasicBlockSuccessor(bb1)
or
exists(BasicBlock mid |
varBlockReachesInclPhiRead(def, bb1, mid) and
ssaDefReachesThroughBlock(def, mid) and
bb2 = getABasicBlockSuccessor(mid)
)
}
pragma[nomagic]
private predicate phiReadStep(Definition def, SourceVariable v, BasicBlock bb1, BasicBlock bb2) {
varBlockReachesInclPhiRead(def, bb1, bb2) and
defOccursInBlock(def, bb2, v, SsaPhiRead())
}
pragma[nomagic]
private predicate varBlockReachesExclPhiRead(Definition def, BasicBlock bb1, BasicBlock bb2) {
varBlockReachesInclPhiRead(pragma[only_bind_into](def), bb1, pragma[only_bind_into](bb2)) and
ssaRef(bb2, _, def.getSourceVariable(), [SsaActualRead().(TSsaRefKind), SsaDef()])
or
exists(BasicBlock mid |
varBlockReachesExclPhiRead(def, mid, bb2) and
phiReadStep(def, _, bb1, mid)
)
}
/**
* Holds if `def` is accessed in basic block `bb1` (either a read or a write),
* the underlying variable `v` of `def` is accessed in basic block `bb2`
* (either a read or a write), `bb2` is a transitive successor of `bb1`, and
* `v` is neither read nor written in any block on the path between `bb1`
* and `bb2`.
*/
pragma[nomagic]
predicate varBlockReaches(Definition def, BasicBlock bb1, BasicBlock bb2) {
varBlockReachesExclPhiRead(def, bb1, bb2) and
not defOccursInBlock(def, bb1, _, SsaPhiRead())
}
pragma[nomagic]
predicate defAdjacentRead(Definition def, BasicBlock bb1, BasicBlock bb2, int i2) {
varBlockReaches(def, bb1, bb2) and
ssaRefRank(bb2, i2, def.getSourceVariable(), SsaActualRead()) = 1
}
/**
* Holds if `def` is accessed in basic block `bb` (either a read or a write),
* `bb1` can reach a transitive successor `bb2` where `def` is no longer live,
* and `v` is neither read nor written in any block on the path between `bb`
* and `bb2`.
*/
pragma[nomagic]
predicate varBlockReachesExit(Definition def, BasicBlock bb) {
exists(BasicBlock bb2 | varBlockReachesInclPhiRead(def, bb, bb2) |
not defOccursInBlock(def, bb2, _, _) and
not ssaDefReachesEndOfBlock(bb2, def, _)
)
or
exists(BasicBlock mid |
varBlockReachesExit(def, mid) and
phiReadStep(def, _, bb, mid)
)
}
}
predicate phiReadExposedForTesting = phiRead/2;
private import SsaDefReaches
pragma[nomagic]
predicate liveThrough(BasicBlock bb, SourceVariable v) {
liveAtExit(bb, v) and
not ssaRef(bb, _, v, SsaDef())
}
/**
* NB: If this predicate is exposed, it should be cached.
*
* Holds if the SSA definition of `v` at `def` reaches the end of basic
* block `bb`, at which point it is still live, without crossing another
* SSA definition of `v`.
*/
pragma[nomagic]
predicate ssaDefReachesEndOfBlock(BasicBlock bb, Definition def, SourceVariable v) {
exists(int last |
last = maxSsaRefRank(pragma[only_bind_into](bb), pragma[only_bind_into](v)) and
ssaDefReachesRank(bb, def, last, v) and
liveAtExit(bb, v)
)
or
// The construction of SSA form ensures that each read of a variable is
// dominated by its definition. An SSA definition therefore reaches a
// control flow node if it is the _closest_ SSA definition that dominates
// the node. If two definitions dominate a node then one must dominate the
// other, so therefore the definition of _closest_ is given by the dominator
// tree. Thus, reaching definitions can be calculated in terms of dominance.
ssaDefReachesEndOfBlock(getImmediateBasicBlockDominator(bb), def, pragma[only_bind_into](v)) and
liveThrough(bb, pragma[only_bind_into](v))
}
/**
* NB: If this predicate is exposed, it should be cached.
*
* Holds if `inp` is an input to the phi node `phi` along the edge originating in `bb`.
*/
pragma[nomagic]
predicate phiHasInputFromBlock(PhiNode phi, Definition inp, BasicBlock bb) {
exists(SourceVariable v, BasicBlock bbDef |
phi.definesAt(v, bbDef, _) and
getABasicBlockPredecessor(bbDef) = bb and
ssaDefReachesEndOfBlock(bb, inp, v)
)
}
/**
* NB: If this predicate is exposed, it should be cached.
*
* Holds if the SSA definition of `v` at `def` reaches a read at index `i` in
* basic block `bb`, without crossing another SSA definition of `v`. The read
* is of kind `rk`.
*/
pragma[nomagic]
predicate ssaDefReachesRead(SourceVariable v, Definition def, BasicBlock bb, int i) {
ssaDefReachesReadWithinBlock(v, def, bb, i)
or
ssaRef(bb, i, v, any(SsaRead k)) and
ssaDefReachesEndOfBlock(getABasicBlockPredecessor(bb), def, v) and
not ssaDefReachesReadWithinBlock(v, _, bb, i)
}
/**
* NB: If this predicate is exposed, it should be cached.
*
* Holds if `def` is accessed at index `i1` in basic block `bb1` (either a read
* or a write), `def` is read at index `i2` in basic block `bb2`, and there is a
* path between them without any read of `def`.
*/
pragma[nomagic]
predicate adjacentDefRead(Definition def, BasicBlock bb1, int i1, BasicBlock bb2, int i2) {
exists(int rnk |
rnk = ssaDefRank(def, _, bb1, i1, _) and
rnk + 1 = ssaDefRank(def, _, bb1, i2, SsaActualRead()) and
variableRead(bb1, i2, _, _) and
bb2 = bb1
)
or
lastSsaRef(def, _, bb1, i1) and
defAdjacentRead(def, bb1, bb2, i2)
}
pragma[noinline]
private predicate adjacentDefRead(
Definition def, BasicBlock bb1, int i1, BasicBlock bb2, int i2, SourceVariable v
) {
adjacentDefRead(def, bb1, i1, bb2, i2) and
v = def.getSourceVariable()
}
private predicate adjacentDefReachesRead(
Definition def, BasicBlock bb1, int i1, BasicBlock bb2, int i2
) {
exists(SourceVariable v | adjacentDefRead(def, bb1, i1, bb2, i2, v) |
ssaRef(bb1, i1, v, SsaDef())
or
variableRead(bb1, i1, v, true)
)
or
exists(BasicBlock bb3, int i3 |
adjacentDefReachesRead(def, bb1, i1, bb3, i3) and
variableRead(bb3, i3, _, false) and
adjacentDefRead(def, bb3, i3, bb2, i2)
)
}
/**
* NB: If this predicate is exposed, it should be cached.
*
* Same as `adjacentDefRead`, but ignores uncertain reads.
*/
pragma[nomagic]
predicate adjacentDefNoUncertainReads(Definition def, BasicBlock bb1, int i1, BasicBlock bb2, int i2) {
adjacentDefReachesRead(def, bb1, i1, bb2, i2) and
variableRead(bb2, i2, _, true)
}
/**
* NB: If this predicate is exposed, it should be cached.
*
* Holds if the node at index `i` in `bb` is a last reference to SSA definition
* `def`. The reference is last because it can reach another write `next`,
* without passing through another read or write.
*/
pragma[nomagic]
predicate lastRefRedef(Definition def, BasicBlock bb, int i, Definition next) {
exists(SourceVariable v |
// Next reference to `v` inside `bb` is a write
exists(int rnk, int j |
rnk = ssaDefRank(def, v, bb, i, _) and
next.definesAt(v, bb, j) and
rnk + 1 = ssaRefRank(bb, j, v, SsaDef())
)
or
// Can reach a write using one or more steps
lastSsaRef(def, v, bb, i) and
exists(BasicBlock bb2 |
varBlockReaches(def, bb, bb2) and
1 = ssaDefRank(next, v, bb2, _, SsaDef())
)
)
}
/**
* NB: If this predicate is exposed, it should be cached.
*
* Holds if `inp` is an immediately preceding definition of uncertain definition
* `def`. Since `def` is uncertain, the value from the preceding definition might
* still be valid.
*/
pragma[nomagic]
predicate uncertainWriteDefinitionInput(UncertainWriteDefinition def, Definition inp) {
lastRefRedef(inp, _, _, def)
}
private predicate adjacentDefReachesUncertainRead(
Definition def, BasicBlock bb1, int i1, BasicBlock bb2, int i2
) {
adjacentDefReachesRead(def, bb1, i1, bb2, i2) and
variableRead(bb2, i2, _, false)
}
/**
* NB: If this predicate is exposed, it should be cached.
*
* Same as `lastRefRedef`, but ignores uncertain reads.
*/
pragma[nomagic]
predicate lastRefRedefNoUncertainReads(Definition def, BasicBlock bb, int i, Definition next) {
lastRefRedef(def, bb, i, next) and
not variableRead(bb, i, def.getSourceVariable(), false)
or
exists(BasicBlock bb0, int i0 |
lastRefRedef(def, bb0, i0, next) and
adjacentDefReachesUncertainRead(def, bb, i, bb0, i0)
)
}
/**
* NB: If this predicate is exposed, it should be cached.
*
* Holds if the node at index `i` in `bb` is a last reference to SSA
* definition `def`.
*
* That is, the node can reach the end of the enclosing callable, or another
* SSA definition for the underlying source variable, without passing through
* another read.
*/
pragma[nomagic]
predicate lastRef(Definition def, BasicBlock bb, int i) {
// Can reach another definition
lastRefRedef(def, bb, i, _)
or
exists(SourceVariable v | lastSsaRef(def, v, bb, i) |
// Can reach exit directly
bb instanceof ExitBasicBlock
or
// Can reach a block using one or more steps, where `def` is no longer live
varBlockReachesExit(def, bb)
)
}
/**
* NB: If this predicate is exposed, it should be cached.
*
* Same as `lastRefRedef`, but ignores uncertain reads.
*/
pragma[nomagic]
predicate lastRefNoUncertainReads(Definition def, BasicBlock bb, int i) {
lastRef(def, bb, i) and
not variableRead(bb, i, def.getSourceVariable(), false)
or
exists(BasicBlock bb0, int i0 |
lastRef(def, bb0, i0) and
adjacentDefReachesUncertainRead(def, bb, i, bb0, i0)
)
}
/** A static single assignment (SSA) definition. */
class Definition extends TDefinition {
/** Gets the source variable underlying this SSA definition. */
SourceVariable getSourceVariable() { this.definesAt(result, _, _) }
/**
* Holds if this SSA definition defines `v` at index `i` in basic block `bb`.
* Phi nodes are considered to be at index `-1`, while normal variable writes
* are at the index of the control flow node they wrap.
*/
final predicate definesAt(SourceVariable v, BasicBlock bb, int i) {
this = TWriteDef(v, bb, i)
or
this = TPhiNode(v, bb) and i = -1
}
/** Gets the basic block to which this SSA definition belongs. */
final BasicBlock getBasicBlock() { this.definesAt(_, result, _) }
/** Gets a textual representation of this SSA definition. */
string toString() { none() }
}
/** An SSA definition that corresponds to a write. */
class WriteDefinition extends Definition, TWriteDef {
private SourceVariable v;
private BasicBlock bb;
private int i;
WriteDefinition() { this = TWriteDef(v, bb, i) }
override string toString() { result = "WriteDef" }
}
/** A phi node. */
class PhiNode extends Definition, TPhiNode {
override string toString() { result = "Phi" }
}
/**
* An SSA definition that represents an uncertain update of the underlying
* source variable.
*/
class UncertainWriteDefinition extends WriteDefinition {
UncertainWriteDefinition() {
exists(SourceVariable v, BasicBlock bb, int i |
this.definesAt(v, bb, i) and
variableWrite(bb, i, v, false)
)
}
}
/** Provides a set of consistency queries. */
module Consistency {
abstract class RelevantDefinition extends Definition {
abstract predicate hasLocationInfo(
string filepath, int startline, int startcolumn, int endline, int endcolumn
);
}
query predicate nonUniqueDef(RelevantDefinition def, SourceVariable v, BasicBlock bb, int i) {
ssaDefReachesRead(v, def, bb, i) and
not exists(unique(Definition def0 | ssaDefReachesRead(v, def0, bb, i)))
}
query predicate readWithoutDef(SourceVariable v, BasicBlock bb, int i) {
variableRead(bb, i, v, _) and
not ssaDefReachesRead(v, _, bb, i)
}
query predicate deadDef(RelevantDefinition def, SourceVariable v) {
v = def.getSourceVariable() and
not ssaDefReachesRead(_, def, _, _) and
not phiHasInputFromBlock(_, def, _) and
not uncertainWriteDefinitionInput(_, def)
}
query predicate notDominatedByDef(RelevantDefinition def, SourceVariable v, BasicBlock bb, int i) {
exists(BasicBlock bbDef, int iDef | def.definesAt(v, bbDef, iDef) |
ssaDefReachesReadWithinBlock(v, def, bb, i) and
(bb != bbDef or i < iDef)
or
ssaDefReachesRead(v, def, bb, i) and
not ssaDefReachesReadWithinBlock(v, def, bb, i) and
not def.definesAt(v, getImmediateBasicBlockDominator*(bb), _)
)
}
}

View File

@@ -1,45 +0,0 @@
/** Provides the Ruby specific parameters for `SsaImplCommon.qll`. */
private import SsaImpl as SsaImpl
private import codeql.ruby.AST
private import codeql.ruby.controlflow.BasicBlocks as BasicBlocks
private import codeql.ruby.controlflow.ControlFlowGraph
class BasicBlock = BasicBlocks::BasicBlock;
BasicBlock getImmediateBasicBlockDominator(BasicBlock bb) { result = bb.getImmediateDominator() }
BasicBlock getABasicBlockSuccessor(BasicBlock bb) { result = bb.getASuccessor() }
class ExitBasicBlock = BasicBlocks::ExitBasicBlock;
class SourceVariable = LocalVariable;
/**
* Holds if the statement at index `i` of basic block `bb` contains a write to variable `v`.
* `certain` is true if the write definitely occurs.
*/
predicate variableWrite(BasicBlock bb, int i, SourceVariable v, boolean certain) {
(
exists(Scope scope | scope = v.(SelfVariable).getDeclaringScope() |
// We consider the `self` variable to have a single write at the entry to a method block...
scope = bb.(BasicBlocks::EntryBasicBlock).getScope() and
i = 0
or
// ...or a class or module block.
bb.getNode(i).getNode() = scope.(ModuleBase).getAControlFlowEntryNode()
)
or
SsaImpl::uninitializedWrite(bb, i, v)
or
SsaImpl::capturedEntryWrite(bb, i, v)
or
SsaImpl::variableWriteActual(bb, i, v, _)
) and
certain = true
or
SsaImpl::capturedCallWrite(_, bb, i, v) and
certain = false
}
predicate variableRead = SsaImpl::variableRead/4;

View File

@@ -172,7 +172,12 @@ abstract class Configuration extends DataFlow::Configuration {
}
override predicate allowImplicitRead(DataFlow::Node node, DataFlow::ContentSet c) {
(this.isSink(node) or this.isAdditionalTaintStep(node, _)) and
(
this.isSink(node) or
this.isSink(node, _) or
this.isAdditionalTaintStep(node, _) or
this.isAdditionalTaintStep(node, _, _, _)
) and
defaultImplicitTaintRead(node, c)
}

View File

@@ -172,7 +172,12 @@ abstract class Configuration extends DataFlow::Configuration {
}
override predicate allowImplicitRead(DataFlow::Node node, DataFlow::ContentSet c) {
(this.isSink(node) or this.isAdditionalTaintStep(node, _)) and
(
this.isSink(node) or
this.isSink(node, _) or
this.isAdditionalTaintStep(node, _) or
this.isAdditionalTaintStep(node, _, _, _)
) and
defaultImplicitTaintRead(node, c)
}

View File

@@ -46,7 +46,7 @@ class ActionControllerControllerClass extends ClassDeclaration {
* A public instance method defined within an `ActionController` controller class.
* This may be the target of a route handler, if such a route is defined.
*/
class ActionControllerActionMethod extends Method, HTTP::Server::RequestHandler::Range {
class ActionControllerActionMethod extends Method, Http::Server::RequestHandler::Range {
private ActionControllerControllerClass controllerClass;
ActionControllerActionMethod() { this = controllerClass.getAMethod() and not this.isPrivate() }
@@ -126,7 +126,7 @@ abstract class ParamsCall extends MethodCall {
* A `RemoteFlowSource::Range` to represent accessing the
* ActionController parameters available via the `params` method.
*/
class ParamsSource extends HTTP::Server::RequestInputAccess::Range {
class ParamsSource extends Http::Server::RequestInputAccess::Range {
ParamsSource() { this.asExpr().getExpr() instanceof ParamsCall }
override string getSourceType() { result = "ActionController::Metal#params" }
@@ -143,7 +143,7 @@ abstract class CookiesCall extends MethodCall {
* A `RemoteFlowSource::Range` to represent accessing the
* ActionController parameters available via the `cookies` method.
*/
class CookiesSource extends HTTP::Server::RequestInputAccess::Range {
class CookiesSource extends Http::Server::RequestInputAccess::Range {
CookiesSource() { this.asExpr().getExpr() instanceof CookiesCall }
override string getSourceType() { result = "ActionController::Metal#cookies" }
@@ -211,7 +211,7 @@ class RedirectToCall extends ActionControllerContextCall {
/**
* A call to the `redirect_to` method, as an `HttpRedirectResponse`.
*/
class ActionControllerRedirectResponse extends HTTP::Server::HttpRedirectResponse::Range {
class ActionControllerRedirectResponse extends Http::Server::HttpRedirectResponse::Range {
RedirectToCall redirectToCall;
ActionControllerRedirectResponse() { this.asExpr().getExpr() = redirectToCall }

View File

@@ -3,6 +3,7 @@
*/
private import codeql.ruby.AST
private import codeql.ruby.ApiGraphs
private import codeql.ruby.Concepts
private import codeql.ruby.controlflow.CfgNodes
private import codeql.ruby.DataFlow
@@ -127,7 +128,7 @@ abstract class RenderCall extends MethodCall {
* A call to `render`, `render_to_body` or `render_to_string`, seen as an
* `HttpResponse`.
*/
private class RenderCallAsHttpResponse extends DataFlow::CallNode, HTTP::Server::HttpResponse::Range {
private class RenderCallAsHttpResponse extends DataFlow::CallNode, Http::Server::HttpResponse::Range {
RenderCallAsHttpResponse() {
this.asExpr().getExpr() instanceof RenderCall or
this.asExpr().getExpr() instanceof RenderToCall
@@ -204,4 +205,15 @@ class LinkToCall extends ActionViewContextCall {
not exists(this.getBlock()) and result = this.getArgument(1)
}
}
/**
* An instantiation of `ActionView::FileSystemResolver`, considered as a `FileSystemAccess`.
*/
class FileSystemResolverAccess extends DataFlow::CallNode, FileSystemAccess::Range {
FileSystemResolverAccess() {
this = API::getTopLevelMember("ActionView").getMember("FileSystemResolver").getAnInstantiation()
}
override DataFlow::Node getAPathArgument() { result = this.getArgument(0) }
}
// TODO: model flow in/out of template files properly,

View File

@@ -0,0 +1,298 @@
/**
* Provides modeling for the `ActiveResource` library.
* Version: 6.0.0.
*/
private import ruby
private import codeql.ruby.Concepts
private import codeql.ruby.controlflow.CfgNodes
private import codeql.ruby.ast.internal.Module
private import codeql.ruby.DataFlow
private import codeql.ruby.ApiGraphs
/**
* Provides modeling for the `ActiveResource` library.
* Version: 6.0.0.
*/
module ActiveResource {
/**
* An ActiveResource model class. This is any (transitive) subclass of ActiveResource.
*/
private API::Node modelApiNode() {
result = API::getTopLevelMember("ActiveResource").getMember("Base").getASubclass+()
}
/**
* An ActiveResource class.
*
* ```rb
* class Person < ActiveResource::Base
* end
* ```
*/
class ModelClass extends ClassDeclaration {
API::Node model;
ModelClass() {
model = modelApiNode() and
this.getSuperclassExpr() = model.getAValueReachableFromSource().asExpr().getExpr()
}
/** Gets the API node for this model */
API::Node getModelApiNode() { result = model }
/** Gets a call to `site=`, which sets the base URL for this model. */
SiteAssignCall getASiteAssignment() { result.getModelClass() = this }
/** Holds if `c` sets a base URL which does not use HTTPS. */
predicate disablesCertificateValidation(SiteAssignCall c) {
c = this.getASiteAssignment() and
c.disablesCertificateValidation()
}
}
/**
* A call to a class method on an ActiveResource model class.
*
* ```rb
* class Person < ActiveResource::Base
* end
*
* Person.find(1)
* ```
*/
class ModelClassMethodCall extends DataFlow::CallNode {
API::Node model;
ModelClassMethodCall() {
model = modelApiNode() and
this = classMethodCall(model, _)
}
/** Gets the model class for this call. */
ModelClass getModelClass() { result.getModelApiNode() = model }
}
/**
* A call to `site=` on an ActiveResource model class.
* This sets the base URL for all HTTP requests made by this class.
*/
private class SiteAssignCall extends DataFlow::CallNode {
API::Node model;
SiteAssignCall() { model = modelApiNode() and this = classMethodCall(model, "site=") }
/**
* Gets a node that contributes to the URLs used for HTTP requests by the parent
* class.
*/
DataFlow::Node getAUrlPart() { result = this.getArgument(0) }
/** Gets the model class for this call. */
ModelClass getModelClass() { result.getModelApiNode() = model }
/** Holds if this site value specifies HTTP rather than HTTPS. */
predicate disablesCertificateValidation() {
this.getAUrlPart()
.asExpr()
.(ExprNodes::AssignExprCfgNode)
.getRhs()
.getConstantValue()
.getString()
.regexpMatch("^http[^s].+")
}
}
/**
* A call to the `find` class method, which returns an ActiveResource model
* object.
*
* ```rb
* alice = Person.find(1)
* ```
*/
class FindCall extends ModelClassMethodCall {
FindCall() { this.getMethodName() = "find" }
}
/**
* A call to the `create(!)` class method, which returns an ActiveResource model
* object.
*
* ```rb
* alice = Person.create(name: "alice")
* ```
*/
class CreateCall extends ModelClassMethodCall {
CreateCall() { this.getMethodName() = ["create", "create!"] }
}
/**
* A call to a method that sends a custom HTTP request outside of the
* ActiveResource conventions. This typically returns an ActiveResource model
* object. It may return a collection, but we don't currently model that.
*
* ```rb
* alice = Person.get(:active)
* ```
*/
class CustomHttpCall extends ModelClassMethodCall {
CustomHttpCall() { this.getMethodName() = ["get", "post", "put", "patch", "delete"] }
}
/**
* An ActiveResource model object.
*/
class ModelInstance extends DataFlow::Node {
ModelClass cls;
ModelInstance() {
exists(API::Node model | model = modelApiNode() |
this = model.getInstance().getAValueReachableFromSource() and
cls.getModelApiNode() = model
)
or
exists(FindCall call | call.flowsTo(this) | cls = call.getModelClass())
or
exists(CreateCall call | call.flowsTo(this) | cls = call.getModelClass())
or
exists(CustomHttpCall call | call.flowsTo(this) | cls = call.getModelClass())
or
exists(CollectionCall call |
call.getMethodName() = ["first", "last"] and
call.flowsTo(this)
|
cls = call.getCollection().getModelClass()
)
}
/** Gets the model class for this instance. */
ModelClass getModelClass() { result = cls }
}
/**
* A call to a method on an ActiveResource model object.
*/
class ModelInstanceMethodCall extends DataFlow::CallNode {
ModelInstance i;
ModelInstanceMethodCall() { this.getReceiver() = i }
/** Gets the model instance for this call. */
ModelInstance getInstance() { result = i }
/** Gets the model class for this call. */
ModelClass getModelClass() { result = i.getModelClass() }
}
/**
* A collection of ActiveResource model objects.
*/
class Collection extends DataFlow::Node {
ModelClassMethodCall classMethodCall;
Collection() {
classMethodCall.flowsTo(this) and
(
classMethodCall.getMethodName() = "all"
or
classMethodCall.getMethodName() = "find" and
classMethodCall.getArgument(0).asExpr().getConstantValue().isStringlikeValue("all")
)
}
/** Gets the model class for this collection. */
ModelClass getModelClass() { result = classMethodCall.getModelClass() }
}
/**
* A method call on a collection.
*/
class CollectionCall extends DataFlow::CallNode {
CollectionCall() { this.getReceiver() instanceof Collection }
/** Gets the collection for this call. */
Collection getCollection() { result = this.getReceiver() }
}
private class ModelClassMethodCallAsHttpRequest extends Http::Client::Request::Range,
ModelClassMethodCall {
ModelClass cls;
ModelClassMethodCallAsHttpRequest() {
this.getModelClass() = cls and
this.getMethodName() = ["all", "build", "create", "create!", "find", "first", "last"]
}
override string getFramework() { result = "ActiveResource" }
override predicate disablesCertificateValidation(
DataFlow::Node disablingNode, DataFlow::Node argumentOrigin
) {
cls.disablesCertificateValidation(disablingNode) and
// TODO: highlight real argument origin
argumentOrigin = disablingNode
}
override DataFlow::Node getAUrlPart() { result = cls.getASiteAssignment().getAUrlPart() }
override DataFlow::Node getResponseBody() { result = this }
}
private class ModelInstanceMethodCallAsHttpRequest extends Http::Client::Request::Range,
ModelInstanceMethodCall {
ModelClass cls;
ModelInstanceMethodCallAsHttpRequest() {
this.getModelClass() = cls and
this.getMethodName() =
[
"exists?", "reload", "save", "save!", "destroy", "delete", "get", "patch", "post", "put",
"update_attribute", "update_attributes"
]
}
override string getFramework() { result = "ActiveResource" }
override predicate disablesCertificateValidation(
DataFlow::Node disablingNode, DataFlow::Node argumentOrigin
) {
cls.disablesCertificateValidation(disablingNode) and
// TODO: highlight real argument origin
argumentOrigin = disablingNode
}
override DataFlow::Node getAUrlPart() { result = cls.getASiteAssignment().getAUrlPart() }
override DataFlow::Node getResponseBody() { result = this }
}
/**
* A call to a class method.
*
* TODO: is this general enough to be useful elsewhere?
*
* Examples:
* ```rb
* class A
* def self.m; end
*
* m # call
* end
*
* A.m # call
* ```
*/
private DataFlow::CallNode classMethodCall(API::Node classNode, string methodName) {
// A.m
result = classNode.getAMethodCall(methodName)
or
// class A
// A.m
// end
result.getReceiver().asExpr() instanceof ExprNodes::SelfVariableAccessCfgNode and
result.asExpr().getExpr().getEnclosingModule().(ClassDeclaration).getSuperclassExpr() =
classNode.getAValueReachableFromSource().asExpr().getExpr() and
result.getMethodName() = methodName
}
}

View File

@@ -164,7 +164,7 @@ private class GraphqlResolvableClass extends ClassDeclaration {
* end
* ```
*/
class GraphqlResolveMethod extends Method, HTTP::Server::RequestHandler::Range {
class GraphqlResolveMethod extends Method, Http::Server::RequestHandler::Range {
private GraphqlResolvableClass resolvableClass;
GraphqlResolveMethod() { this = resolvableClass.getMethod("resolve") }
@@ -208,7 +208,7 @@ class GraphqlResolveMethod extends Method, HTTP::Server::RequestHandler::Range {
* end
* ```
*/
class GraphqlLoadMethod extends Method, HTTP::Server::RequestHandler::Range {
class GraphqlLoadMethod extends Method, Http::Server::RequestHandler::Range {
private GraphqlResolvableClass resolvableClass;
GraphqlLoadMethod() {
@@ -340,7 +340,7 @@ private class GraphqlFieldArgumentDefinitionMethodCall extends GraphqlSchemaObje
* end
* ```
*/
class GraphqlFieldResolutionMethod extends Method, HTTP::Server::RequestHandler::Range {
class GraphqlFieldResolutionMethod extends Method, Http::Server::RequestHandler::Range {
private GraphqlSchemaObjectClass schemaObjectClass;
GraphqlFieldResolutionMethod() {

View File

@@ -97,13 +97,13 @@ class Feature extends TFeature {
abstract string getConstantName();
}
private class FeatureNOENT extends Feature, TNOENT {
private class FeatureNoent extends Feature, TNOENT {
override int getValue() { result = 2 }
override string getConstantName() { result = "NOENT" }
}
private class FeatureNONET extends Feature, TNONET {
private class FeatureNonet extends Feature, TNONET {
override int getValue() { result = 2048 }
override string getConstantName() { result = "NONET" }

View File

@@ -72,6 +72,8 @@ private class Unit = Specific::Unit;
private module API = Specific::API;
private module DataFlow = Specific::DataFlow;
private import Specific::AccessPathSyntax
/** Module containing hooks for providing input data to be interpreted as a model. */
@@ -155,6 +157,38 @@ module ModelInput {
*/
abstract predicate row(string row);
}
/**
* A unit class for adding additional type model rows from CodeQL models.
*/
class TypeModel extends Unit {
/**
* Gets a data-flow node that is a source of the type `package;type`.
*/
DataFlow::Node getASource(string package, string type) { none() }
/**
* Gets a data flow node that is a sink of the type `package;type`,
* usually because it is an argument passed to a parameter of that type.
*/
DataFlow::Node getASink(string package, string type) { none() }
}
/**
* A unit class for adding additional type variable model rows.
*/
class TypeVariableModelCsv extends Unit {
/**
* Holds if `row` specifies a path through a type variable.
*
* A row of form,
* ```
* name;path
* ```
* means `path` can be substituted for a token `TypeVar[name]`.
*/
abstract predicate row(string row);
}
}
private import ModelInput
@@ -182,6 +216,8 @@ private predicate summaryModel(string row) { any(SummaryModelCsv s).row(inverseP
private predicate typeModel(string row) { any(TypeModelCsv s).row(inversePad(row)) }
private predicate typeVariableModel(string row) { any(TypeVariableModelCsv 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 |
@@ -219,7 +255,7 @@ private predicate summaryModel(
)
}
/** Holds if an type model exists for the given parameters. */
/** Holds if a type model exists for the given parameters. */
private predicate typeModel(
string package1, string type1, string package2, string type2, string path
) {
@@ -233,6 +269,15 @@ private predicate typeModel(
)
}
/** Holds if a type variable model exists for the given parameters. */
private predicate typeVariableModel(string name, string path) {
exists(string row |
typeVariableModel(row) and
row.splitAt(";", 0) = name and
row.splitAt(";", 1) = path
)
}
/**
* Gets a package that should be seen as an alias for the given other `package`,
* or the `package` itself.
@@ -253,7 +298,7 @@ private predicate isRelevantPackage(string package) {
sourceModel(package, _, _, _) or
sinkModel(package, _, _, _) or
summaryModel(package, _, _, _, _, _) or
typeModel(package, _, _, _, _)
typeModel(_, _, package, _, _)
) and
(
Specific::isPackageUsed(package)
@@ -290,6 +335,8 @@ private class AccessPathRange extends AccessPath::Range {
summaryModel(package, _, _, this, _, _) or
summaryModel(package, _, _, _, this, _)
)
or
typeVariableModel(_, this)
}
}
@@ -339,6 +386,57 @@ private predicate invocationMatchesCallSiteFilter(Specific::InvokeNode invoke, A
Specific::invocationMatchesExtraCallSiteFilter(invoke, token)
}
private class TypeModelUseEntry extends API::EntryPoint {
private string package;
private string type;
TypeModelUseEntry() {
exists(any(TypeModel tm).getASource(package, type)) and
this = "TypeModelUseEntry;" + package + ";" + type
}
override DataFlow::LocalSourceNode getASource() {
result = any(TypeModel tm).getASource(package, type)
}
API::Node getNodeForType(string package_, string type_) {
package = package_ and type = type_ and result = this.getANode()
}
}
private class TypeModelDefEntry extends API::EntryPoint {
private string package;
private string type;
TypeModelDefEntry() {
exists(any(TypeModel tm).getASink(package, type)) and
this = "TypeModelDefEntry;" + package + ";" + type
}
override DataFlow::Node getASink() { result = any(TypeModel tm).getASink(package, type) }
API::Node getNodeForType(string package_, string type_) {
package = package_ and type = type_ and result = this.getANode()
}
}
/**
* Gets an API node identified by the given `(package,type)` pair.
*/
pragma[nomagic]
private API::Node getNodeFromType(string package, string type) {
exists(string package2, string type2, AccessPath path2 |
typeModel(package, type, package2, type2, path2) and
result = getNodeFromPath(package2, type2, path2)
)
or
result = any(TypeModelUseEntry e).getNodeForType(package, type)
or
result = any(TypeModelDefEntry e).getNodeForType(package, type)
or
result = Specific::getExtraNodeFromType(package, type)
}
/**
* Gets the API node identified by the first `n` tokens of `path` in the given `(package, type, path)` tuple.
*/
@@ -347,12 +445,8 @@ private API::Node getNodeFromPath(string package, string type, AccessPath path,
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())
)
result = getNodeFromType(package, type)
or
// Language-specific cases, such as handling of global variables
result = Specific::getExtraNodeFromPath(package, type, path, n)
)
or
@@ -361,6 +455,72 @@ private API::Node getNodeFromPath(string package, string type, AccessPath path,
// 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))
or
// Apply a subpath
result =
getNodeFromSubPath(getNodeFromPath(package, type, path, n - 1), getSubPathAt(path, n - 1))
or
// Apply a type step
typeStep(getNodeFromPath(package, type, path, n), result)
}
/**
* Gets a subpath for the `TypeVar` token found at the `n`th token of `path`.
*/
pragma[nomagic]
private AccessPath getSubPathAt(AccessPath path, int n) {
exists(string typeVarName |
path.getToken(n).getAnArgument("TypeVar") = typeVarName and
typeVariableModel(typeVarName, result)
)
}
/**
* Gets a node that is found by evaluating the first `n` tokens of `subPath` starting at `base`.
*/
pragma[nomagic]
private API::Node getNodeFromSubPath(API::Node base, AccessPath subPath, int n) {
exists(AccessPath path, int k |
base = [getNodeFromPath(_, _, path, k), getNodeFromSubPath(_, path, k)] and
subPath = getSubPathAt(path, k) and
result = base and
n = 0
)
or
exists(string package, string type, AccessPath basePath |
typeStepModel(package, type, basePath, subPath) and
base = getNodeFromPath(package, type, basePath) and
result = base and
n = 0
)
or
result = getSuccessorFromNode(getNodeFromSubPath(base, subPath, n - 1), subPath.getToken(n - 1))
or
result =
getSuccessorFromInvoke(getInvocationFromSubPath(base, subPath, n - 1), subPath.getToken(n - 1))
or
result =
getNodeFromSubPath(getNodeFromSubPath(base, subPath, n - 1), getSubPathAt(subPath, n - 1))
or
typeStep(getNodeFromSubPath(base, subPath, n), result)
}
/**
* Gets a call site that is found by evaluating the first `n` tokens of `subPath` starting at `base`.
*/
private Specific::InvokeNode getInvocationFromSubPath(API::Node base, AccessPath subPath, int n) {
result = Specific::getAnInvocationOf(getNodeFromSubPath(base, subPath, n))
or
result = getInvocationFromSubPath(base, subPath, n - 1) and
invocationMatchesCallSiteFilter(result, subPath.getToken(n - 1))
}
/**
* Gets a node that is found by evaluating `subPath` starting at `base`.
*/
pragma[nomagic]
private API::Node getNodeFromSubPath(API::Node base, AccessPath subPath) {
result = getNodeFromSubPath(base, subPath, subPath.getNumToken())
}
/** Gets the node identified by the given `(package, type, path)` tuple. */
@@ -368,6 +528,20 @@ API::Node getNodeFromPath(string package, string type, AccessPath path) {
result = getNodeFromPath(package, type, path, path.getNumToken())
}
pragma[nomagic]
private predicate typeStepModel(string package, string type, AccessPath basePath, AccessPath output) {
summaryModel(package, type, basePath, "", output, "type")
}
pragma[nomagic]
private predicate typeStep(API::Node pred, API::Node succ) {
exists(string package, string type, AccessPath basePath, AccessPath output |
typeStepModel(package, type, basePath, output) and
pred = getNodeFromPath(package, type, basePath) and
succ = getNodeFromSubPath(pred, output)
)
}
/**
* Gets an invocation identified by the given `(package, type, path)` tuple.
*
@@ -390,7 +564,7 @@ Specific::InvokeNode getInvocationFromPath(string package, string type, AccessPa
*/
bindingset[name]
predicate isValidTokenNameInIdentifyingAccessPath(string name) {
name = ["Argument", "Parameter", "ReturnValue", "WithArity"]
name = ["Argument", "Parameter", "ReturnValue", "WithArity", "TypeVar"]
or
Specific::isExtraValidTokenNameInIdentifyingAccessPath(name)
}
@@ -418,6 +592,9 @@ predicate isValidTokenArgumentInIdentifyingAccessPath(string name, string argume
name = "WithArity" and
argument.regexpMatch("\\d+(\\.\\.(\\d+)?)?")
or
name = "TypeVar" and
exists(argument)
or
Specific::isExtraValidTokenArgumentInIdentifyingAccessPath(name, argument)
}
@@ -469,12 +646,7 @@ module ModelOutput {
* 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)
)
}
API::Node getATypeNode(string package, string type) { result = getNodeFromType(package, type) }
/**
* Gets an error message relating to an invalid CSV row in a model.
@@ -489,6 +661,8 @@ module ModelOutput {
any(SummaryModelCsv csv).row(row) and kind = "summary" and expectedArity = 6
or
any(TypeModelCsv csv).row(row) and kind = "type" and expectedArity = 5
or
any(TypeVariableModelCsv csv).row(row) and kind = "type-variable" and expectedArity = 2
|
actualArity = count(row.indexOf(";")) + 1 and
actualArity != expectedArity and
@@ -499,7 +673,7 @@ module ModelOutput {
or
// Check names and arguments of access path tokens
exists(AccessPath path, AccessPathToken token |
isRelevantFullPath(_, _, path) and
(isRelevantFullPath(_, _, path) or typeVariableModel(_, path)) and
token = path.getToken(_)
|
not isValidTokenNameInIdentifyingAccessPath(token.getName()) and

View File

@@ -20,7 +20,6 @@
*/
private import ruby
private import codeql.ruby.DataFlow
private import codeql.ruby.dataflow.internal.DataFlowPrivate as DataFlowPrivate
private import ApiGraphModels
@@ -29,6 +28,7 @@ class Unit = DataFlowPrivate::Unit;
// Re-export libraries needed by ApiGraphModels.qll
import codeql.ruby.ApiGraphs
import codeql.ruby.dataflow.internal.AccessPathSyntax as AccessPathSyntax
import codeql.ruby.DataFlow::DataFlow as DataFlow
private import AccessPathSyntax
private import codeql.ruby.dataflow.internal.FlowSummaryImplSpecific as FlowSummaryImplSpecific
private import codeql.ruby.dataflow.internal.DataFlowDispatch as DataFlowDispatch
@@ -55,12 +55,6 @@ predicate isPackageUsed(string package) {
/** 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
@@ -71,6 +65,13 @@ API::Node getExtraNodeFromPath(string package, string type, AccessPath path, int
)
}
/** Gets a Ruby-specific interpretation of the `(package, type)` tuple. */
API::Node getExtraNodeFromType(string package, string type) {
isRelevantFullPath(package, type, _) and // Allow any package name, see `isPackageUsed`.
type = "" and
result = API::root()
}
/**
* Holds if `path` occurs in a CSV row with type `any`, meaning it can start
* matching anywhere, and the path begins with `Method[methodName]`.

View File

@@ -7,6 +7,7 @@ private import codeql.ruby.CFG
private import codeql.ruby.Concepts
private import codeql.ruby.ApiGraphs
private import codeql.ruby.DataFlow
private import codeql.ruby.dataflow.internal.DataFlowImplForHttpClientLibraries as DataFlowImplForHttpClientLibraries
/**
* A call that makes an HTTP request using `Excon`.
@@ -23,14 +24,13 @@ private import codeql.ruby.DataFlow
* TODO: pipelining, streaming responses
* https://github.com/excon/excon/blob/master/README.md
*/
class ExconHttpRequest extends HTTP::Client::Request::Range {
DataFlow::CallNode requestUse;
class ExconHttpRequest extends Http::Client::Request::Range, DataFlow::CallNode {
API::Node requestNode;
API::Node connectionNode;
DataFlow::Node connectionUse;
ExconHttpRequest() {
requestUse = requestNode.asSource() and
this = requestNode.asSource() and
connectionUse = connectionNode.asSource() and
connectionNode =
[
@@ -46,8 +46,7 @@ class ExconHttpRequest extends HTTP::Client::Request::Range {
// Excon#request exists but Excon.request doesn't.
// This shouldn't be a problem - in real code the latter would raise NoMethodError anyway.
"get", "head", "delete", "options", "post", "put", "patch", "trace", "request"
]) and
this = requestUse.asExpr().getExpr()
])
}
override DataFlow::Node getResponseBody() { result = requestNode.getAMethodCall("body") }
@@ -56,96 +55,76 @@ class ExconHttpRequest extends HTTP::Client::Request::Range {
// For one-off requests, the URL is in the first argument of the request method call.
// For connection re-use, the URL is split between the first argument of the `new` call
// and the `path` keyword argument of the request method call.
result = requestUse.getArgument(0) and not result.asExpr().getExpr() instanceof Pair
result = this.getArgument(0) and not result.asExpr().getExpr() instanceof Pair
or
result = requestUse.getKeywordArgument("path")
result = this.getKeywordArgument("path")
or
result = connectionUse.(DataFlow::CallNode).getArgument(0)
}
override predicate disablesCertificateValidation(DataFlow::Node disablingNode) {
// Check for `ssl_verify_peer: false` in the options hash.
exists(DataFlow::Node arg, int i |
i > 0 and
arg = connectionNode.getAValueReachableFromSource().(DataFlow::CallNode).getArgument(i)
|
argSetsVerifyPeer(arg, false, disablingNode)
/** Gets the value that controls certificate validation, if any. */
DataFlow::Node getCertificateValidationControllingValue() {
exists(DataFlow::CallNode newCall | newCall = connectionNode.getAValueReachableFromSource() |
// Check for `ssl_verify_peer: false`
result = newCall.getKeywordArgumentIncludeHashArgument("ssl_verify_peer")
)
}
override predicate disablesCertificateValidation(
DataFlow::Node disablingNode, DataFlow::Node argumentOrigin
) {
any(ExconDisablesCertificateValidationConfiguration config)
.hasFlow(argumentOrigin, disablingNode) and
disablingNode = this.getCertificateValidationControllingValue()
or
// Or we see a call to `Excon.defaults[:ssl_verify_peer] = false` before the
// request, and no `ssl_verify_peer: true` in the explicit options hash for
// the request call.
exists(DataFlow::CallNode disableCall |
setsDefaultVerification(disableCall, false) and
disableCall.asExpr().getASuccessor+() = requestUse.asExpr() and
disablingNode = disableCall and
not exists(DataFlow::Node arg, int i |
i > 0 and
arg = connectionNode.getAValueReachableFromSource().(DataFlow::CallNode).getArgument(i)
|
argSetsVerifyPeer(arg, true, _)
)
// We set `Excon.defaults[:ssl_verify_peer]` or `Excon.ssl_verify_peer` = false`
// before the request, and no `ssl_verify_peer: true` in the explicit options hash
// for the request call.
exists(DataFlow::CallNode disableCall, BooleanLiteral value |
// Excon.defaults[:ssl_verify_peer]
disableCall = API::getTopLevelMember("Excon").getReturn("defaults").getAMethodCall("[]=") and
disableCall
.getArgument(0)
.getALocalSource()
.asExpr()
.getConstantValue()
.isStringlikeValue("ssl_verify_peer") and
disablingNode = disableCall.getArgument(1) and
argumentOrigin = disablingNode.getALocalSource() and
value = argumentOrigin.asExpr().getExpr()
or
// Excon.ssl_verify_peer
disableCall = API::getTopLevelMember("Excon").getAMethodCall("ssl_verify_peer=") and
disablingNode = disableCall.getArgument(0) and
argumentOrigin = disablingNode.getALocalSource() and
value = argumentOrigin.asExpr().getExpr()
|
value.getValue() = false and
disableCall.asExpr().getASuccessor+() = this.asExpr() and
// no `ssl_verify_peer: true` in the request call.
not this.getCertificateValidationControllingValue()
.getALocalSource()
.asExpr()
.getExpr()
.(BooleanLiteral)
.getValue() = true
)
}
override string getFramework() { result = "Excon" }
}
/**
* Holds if `arg` represents an options hash that contains the key
* `:ssl_verify_peer` with `value`, where `kvNode` is the data-flow node for
* this key-value pair.
*/
predicate argSetsVerifyPeer(DataFlow::Node arg, boolean value, DataFlow::Node kvNode) {
// Either passed as an individual key:value argument, e.g.:
// Excon.get(..., ssl_verify_peer: false)
isSslVerifyPeerPair(arg.asExpr(), value) and
kvNode = arg
or
// Or as a single hash argument, e.g.:
// Excon.get(..., { ssl_verify_peer: false, ... })
exists(DataFlow::LocalSourceNode optionsNode, CfgNodes::ExprNodes::PairCfgNode p |
p = optionsNode.asExpr().(CfgNodes::ExprNodes::HashLiteralCfgNode).getAKeyValuePair() and
isSslVerifyPeerPair(p, value) and
optionsNode.flowsTo(arg) and
kvNode.asExpr() = p
)
}
/** A configuration to track values that can disable certificate validation for Excon. */
private class ExconDisablesCertificateValidationConfiguration extends DataFlowImplForHttpClientLibraries::Configuration {
ExconDisablesCertificateValidationConfiguration() {
this = "ExconDisablesCertificateValidationConfiguration"
}
/**
* Holds if `callNode` sets `Excon.defaults[:ssl_verify_peer]` or
* `Excon.ssl_verify_peer` to `value`.
*/
private predicate setsDefaultVerification(DataFlow::CallNode callNode, boolean value) {
callNode = API::getTopLevelMember("Excon").getReturn("defaults").getAMethodCall("[]=") and
isSslVerifyPeerLiteral(callNode.getArgument(0)) and
hasBooleanValue(callNode.getArgument(1), value)
or
callNode = API::getTopLevelMember("Excon").getAMethodCall("ssl_verify_peer=") and
hasBooleanValue(callNode.getArgument(0), value)
}
override predicate isSource(DataFlow::Node source) {
source.asExpr().getExpr().(BooleanLiteral).isFalse()
}
private predicate isSslVerifyPeerLiteral(DataFlow::Node node) {
exists(DataFlow::LocalSourceNode literal |
literal.asExpr().getExpr().getConstantValue().isStringlikeValue("ssl_verify_peer") and
literal.flowsTo(node)
)
}
/** Holds if `node` can contain `value`. */
private predicate hasBooleanValue(DataFlow::Node node, boolean value) {
exists(DataFlow::LocalSourceNode literal |
literal.asExpr().getExpr().(BooleanLiteral).getValue() = value and
literal.flowsTo(node)
)
}
/** Holds if `p` is the pair `ssl_verify_peer: <value>`. */
private predicate isSslVerifyPeerPair(CfgNodes::ExprNodes::PairCfgNode p, boolean value) {
exists(DataFlow::Node key, DataFlow::Node valueNode |
key.asExpr() = p.getKey() and
valueNode.asExpr() = p.getValue() and
isSslVerifyPeerLiteral(key) and
hasBooleanValue(valueNode, value)
)
override predicate isSink(DataFlow::Node sink) {
sink = any(ExconHttpRequest req).getCertificateValidationControllingValue()
}
}

View File

@@ -7,6 +7,7 @@ private import codeql.ruby.CFG
private import codeql.ruby.Concepts
private import codeql.ruby.ApiGraphs
private import codeql.ruby.DataFlow
private import codeql.ruby.dataflow.internal.DataFlowImplForHttpClientLibraries as DataFlowImplForHttpClientLibraries
/**
* A call that makes an HTTP request using `Faraday`.
@@ -22,11 +23,10 @@ private import codeql.ruby.DataFlow
* connection.get("/").body
* ```
*/
class FaradayHttpRequest extends HTTP::Client::Request::Range {
class FaradayHttpRequest extends Http::Client::Request::Range, DataFlow::CallNode {
API::Node requestNode;
API::Node connectionNode;
DataFlow::Node connectionUse;
DataFlow::CallNode requestUse;
FaradayHttpRequest() {
connectionNode =
@@ -38,125 +38,67 @@ class FaradayHttpRequest extends HTTP::Client::Request::Range {
] and
requestNode =
connectionNode.getReturn(["get", "head", "delete", "post", "put", "patch", "trace"]) and
requestUse = requestNode.asSource() and
connectionUse = connectionNode.asSource() and
this = requestUse.asExpr().getExpr()
this = requestNode.asSource() and
connectionUse = connectionNode.asSource()
}
override DataFlow::Node getResponseBody() { result = requestNode.getAMethodCall("body") }
override DataFlow::Node getAUrlPart() {
result = requestUse.getArgument(0) or
result = this.getArgument(0) or
result = connectionUse.(DataFlow::CallNode).getArgument(0) or
result = connectionUse.(DataFlow::CallNode).getKeywordArgument("url")
}
override predicate disablesCertificateValidation(DataFlow::Node disablingNode) {
/** Gets the value that controls certificate validation, if any, with argument name `name`. */
DataFlow::Node getCertificateValidationControllingValue(string argName) {
// `Faraday::new` takes an options hash as its second argument, and we're
// looking for
// `{ ssl: { verify: false } }`
// or
// `{ ssl: { verify_mode: OpenSSL::SSL::VERIFY_NONE } }`
exists(DataFlow::Node arg, int i |
i > 0 and
arg = connectionNode.getAValueReachableFromSource().(DataFlow::CallNode).getArgument(i)
argName in ["verify", "verify_mode"] and
exists(DataFlow::Node sslValue, DataFlow::CallNode newCall |
newCall = connectionNode.getAValueReachableFromSource() and
sslValue = newCall.getKeywordArgumentIncludeHashArgument("ssl")
|
// Either passed as an individual key:value argument, e.g.:
// Faraday.new(..., ssl: {...})
isSslOptionsPairDisablingValidation(arg.asExpr()) and
disablingNode = arg
or
// Or as a single hash argument, e.g.:
// Faraday.new(..., { ssl: {...} })
exists(DataFlow::LocalSourceNode optionsNode, CfgNodes::ExprNodes::PairCfgNode p |
p = optionsNode.asExpr().(CfgNodes::ExprNodes::HashLiteralCfgNode).getAKeyValuePair() and
isSslOptionsPairDisablingValidation(p) and
optionsNode.flowsTo(arg) and
disablingNode.asExpr() = p
exists(CfgNodes::ExprNodes::PairCfgNode p, DataFlow::Node key |
p = sslValue.asExpr().(CfgNodes::ExprNodes::HashLiteralCfgNode).getAKeyValuePair() and
key.asExpr() = p.getKey() and
key.getALocalSource().asExpr().getConstantValue().isStringlikeValue(argName) and
result.asExpr() = p.getValue()
)
)
}
override predicate disablesCertificateValidation(
DataFlow::Node disablingNode, DataFlow::Node argumentOrigin
) {
any(FaradayDisablesCertificateValidationConfiguration config)
.hasFlow(argumentOrigin, disablingNode) and
disablingNode = this.getCertificateValidationControllingValue(_)
}
override string getFramework() { result = "Faraday" }
}
/**
* Holds if the pair `p` contains the key `:ssl` for which the value is a hash
* containing either `verify: false` or
* `verify_mode: OpenSSL::SSL::VERIFY_NONE`.
*/
private predicate isSslOptionsPairDisablingValidation(CfgNodes::ExprNodes::PairCfgNode p) {
exists(DataFlow::Node key, DataFlow::Node value |
key.asExpr() = p.getKey() and
value.asExpr() = p.getValue() and
isSymbolLiteral(key, "ssl") and
(isHashWithVerifyFalse(value) or isHashWithVerifyModeNone(value))
)
}
/** A configuration to track values that can disable certificate validation for Faraday. */
private class FaradayDisablesCertificateValidationConfiguration extends DataFlowImplForHttpClientLibraries::Configuration {
FaradayDisablesCertificateValidationConfiguration() {
this = "FaradayDisablesCertificateValidationConfiguration"
}
/** Holds if `node` represents the symbol literal with the given `valueText`. */
private predicate isSymbolLiteral(DataFlow::Node node, string valueText) {
exists(DataFlow::LocalSourceNode literal |
literal.asExpr().getExpr().getConstantValue().isStringlikeValue(valueText) and
literal.flowsTo(node)
)
}
override predicate isSource(
DataFlow::Node source, DataFlowImplForHttpClientLibraries::FlowState state
) {
source.asExpr().getExpr().(BooleanLiteral).isFalse() and
state = "verify"
or
source = API::getTopLevelMember("OpenSSL").getMember("SSL").getMember("VERIFY_NONE").asSource() and
state = "verify_mode"
}
/**
* Holds if `node` represents a hash containing the key-value pair
* `verify: false`.
*/
private predicate isHashWithVerifyFalse(DataFlow::Node node) {
exists(DataFlow::LocalSourceNode hash |
isVerifyFalsePair(hash.asExpr().(CfgNodes::ExprNodes::HashLiteralCfgNode).getAKeyValuePair()) and
hash.flowsTo(node)
)
}
/**
* Holds if `node` represents a hash containing the key-value pair
* `verify_mode: OpenSSL::SSL::VERIFY_NONE`.
*/
private predicate isHashWithVerifyModeNone(DataFlow::Node node) {
exists(DataFlow::LocalSourceNode hash |
isVerifyModeNonePair(hash.asExpr().(CfgNodes::ExprNodes::HashLiteralCfgNode).getAKeyValuePair()) and
hash.flowsTo(node)
)
}
/**
* Holds if the pair `p` has the key `:verify_mode` and the value
* `OpenSSL::SSL::VERIFY_NONE`.
*/
private predicate isVerifyModeNonePair(CfgNodes::ExprNodes::PairCfgNode p) {
exists(DataFlow::Node key, DataFlow::Node value |
key.asExpr() = p.getKey() and
value.asExpr() = p.getValue() and
isSymbolLiteral(key, "verify_mode") and
value =
API::getTopLevelMember("OpenSSL")
.getMember("SSL")
.getMember("VERIFY_NONE")
.getAValueReachableFromSource()
)
}
/**
* Holds if the pair `p` has the key `:verify` and the value `false`.
*/
private predicate isVerifyFalsePair(CfgNodes::ExprNodes::PairCfgNode p) {
exists(DataFlow::Node key, DataFlow::Node value |
key.asExpr() = p.getKey() and
value.asExpr() = p.getValue() and
isSymbolLiteral(key, "verify") and
isFalse(value)
)
}
/** Holds if `node` can contain the Boolean value `false`. */
private predicate isFalse(DataFlow::Node node) {
exists(DataFlow::LocalSourceNode literal |
literal.asExpr().getExpr().(BooleanLiteral).isFalse() and
literal.flowsTo(node)
)
override predicate isSink(DataFlow::Node sink, DataFlowImplForHttpClientLibraries::FlowState state) {
sink = any(FaradayHttpRequest req).getCertificateValidationControllingValue(state)
}
}

View File

@@ -6,6 +6,7 @@ private import ruby
private import codeql.ruby.Concepts
private import codeql.ruby.ApiGraphs
private import codeql.ruby.DataFlow
private import codeql.ruby.dataflow.internal.DataFlowImplForHttpClientLibraries as DataFlowImplForHttpClientLibraries
/**
* A call that makes an HTTP request using `HTTPClient`.
@@ -14,10 +15,9 @@ private import codeql.ruby.DataFlow
* HTTPClient.get_content("http://example.com")
* ```
*/
class HttpClientRequest extends HTTP::Client::Request::Range {
class HttpClientRequest extends Http::Client::Request::Range, DataFlow::CallNode {
API::Node requestNode;
API::Node connectionNode;
DataFlow::CallNode requestUse;
string method;
HttpClientRequest() {
@@ -29,36 +29,60 @@ class HttpClientRequest extends HTTP::Client::Request::Range {
API::getTopLevelMember("HTTPClient").getInstance()
] and
requestNode = connectionNode.getReturn(method) and
requestUse = requestNode.asSource() and
this = requestNode.asSource() and
method in [
"get", "head", "delete", "options", "post", "put", "trace", "get_content", "post_content"
] and
this = requestUse.asExpr().getExpr()
]
}
override DataFlow::Node getAUrlPart() { result = requestUse.getArgument(0) }
override DataFlow::Node getAUrlPart() { result = this.getArgument(0) }
override DataFlow::Node getResponseBody() {
// The `get_content` and `post_content` methods return the response body as
// a string. The other methods return a `HTTPClient::Message` object which
// has various methods that return the response body.
method in ["get_content", "post_content"] and result = requestUse
method in ["get_content", "post_content"] and result = this
or
not method in ["get_content", "put_content"] and
result = requestNode.getAMethodCall(["body", "http_body", "content", "dump"])
}
override predicate disablesCertificateValidation(DataFlow::Node disablingNode) {
/** Gets the value that controls certificate validation, if any. */
DataFlow::Node getCertificateValidationControllingValue() {
// Look for calls to set
// `c.ssl_config.verify_mode = OpenSSL::SSL::VERIFY_NONE`
// on an HTTPClient connection object `c`.
disablingNode = connectionNode.getReturn("ssl_config").getReturn("verify_mode=").asSource() and
disablingNode.(DataFlow::CallNode).getArgument(0) =
API::getTopLevelMember("OpenSSL")
.getMember("SSL")
.getMember("VERIFY_NONE")
.getAValueReachableFromSource()
result =
connectionNode
.getReturn("ssl_config")
.getReturn("verify_mode=")
.asSource()
.(DataFlow::CallNode)
.getArgument(0)
}
override predicate disablesCertificateValidation(
DataFlow::Node disablingNode, DataFlow::Node argumentOrigin
) {
any(HttpClientDisablesCertificateValidationConfiguration config)
.hasFlow(argumentOrigin, disablingNode) and
disablingNode = this.getCertificateValidationControllingValue()
}
override string getFramework() { result = "HTTPClient" }
}
/** A configuration to track values that can disable certificate validation for HttpClient. */
private class HttpClientDisablesCertificateValidationConfiguration extends DataFlowImplForHttpClientLibraries::Configuration {
HttpClientDisablesCertificateValidationConfiguration() {
this = "HttpClientDisablesCertificateValidationConfiguration"
}
override predicate isSource(DataFlow::Node source) {
source = API::getTopLevelMember("OpenSSL").getMember("SSL").getMember("VERIFY_NONE").asSource()
}
override predicate isSink(DataFlow::Node sink) {
sink = any(HttpClientRequest req).getCertificateValidationControllingValue()
}
}

View File

@@ -7,6 +7,7 @@ private import codeql.ruby.CFG
private import codeql.ruby.Concepts
private import codeql.ruby.ApiGraphs
private import codeql.ruby.DataFlow
private import codeql.ruby.dataflow.internal.DataFlowImplForHttpClientLibraries as DataFlowImplForHttpClientLibraries
/**
* A call that makes an HTTP request using `HTTParty`.
@@ -23,19 +24,17 @@ private import codeql.ruby.DataFlow
* MyClass.new("http://example.com")
* ```
*/
class HttpartyRequest extends HTTP::Client::Request::Range {
class HttpartyRequest extends Http::Client::Request::Range, DataFlow::CallNode {
API::Node requestNode;
DataFlow::CallNode requestUse;
HttpartyRequest() {
requestUse = requestNode.asSource() and
this = requestNode.asSource() and
requestNode =
API::getTopLevelMember("HTTParty")
.getReturn(["get", "head", "delete", "options", "post", "put", "patch"]) and
this = requestUse.asExpr().getExpr()
.getReturn(["get", "head", "delete", "options", "post", "put", "patch"])
}
override DataFlow::Node getAUrlPart() { result = requestUse.getArgument(0) }
override DataFlow::Node getAUrlPart() { result = this.getArgument(0) }
override DataFlow::Node getResponseBody() {
// If HTTParty can recognise the response type, it will parse and return it
@@ -46,60 +45,36 @@ class HttpartyRequest extends HTTP::Client::Request::Range {
or
// Otherwise, treat the response as the response body.
not exists(requestNode.getAMethodCall("body")) and
result = requestUse
result = this
}
override predicate disablesCertificateValidation(DataFlow::Node disablingNode) {
// The various request methods take an options hash as their second
// argument, and we're looking for `{ verify: false }` or
// `{ verify_peer: false }`.
exists(DataFlow::Node arg, int i |
i > 0 and
arg.asExpr() = requestUse.asExpr().(CfgNodes::ExprNodes::MethodCallCfgNode).getArgument(i)
|
// Either passed as an individual key:value argument, e.g.:
// HTTParty.get(..., verify: false)
isVerifyFalsePair(arg.asExpr()) and
disablingNode = arg
or
// Or as a single hash argument, e.g.:
// HTTParty.get(..., { verify: false, ... })
exists(DataFlow::LocalSourceNode optionsNode, CfgNodes::ExprNodes::PairCfgNode p |
p = optionsNode.asExpr().(CfgNodes::ExprNodes::HashLiteralCfgNode).getAKeyValuePair() and
isVerifyFalsePair(p) and
optionsNode.flowsTo(arg) and
disablingNode.asExpr() = p
)
)
/** Gets the value that controls certificate validation, if any. */
DataFlow::Node getCertificateValidationControllingValue() {
result = this.getKeywordArgumentIncludeHashArgument(["verify", "verify_peer"])
}
override predicate disablesCertificateValidation(
DataFlow::Node disablingNode, DataFlow::Node argumentOrigin
) {
any(HttpartyDisablesCertificateValidationConfiguration config)
.hasFlow(argumentOrigin, disablingNode) and
disablingNode = this.getCertificateValidationControllingValue()
}
override string getFramework() { result = "HTTParty" }
}
/** Holds if `node` represents the symbol literal `verify` or `verify_peer`. */
private predicate isVerifyLiteral(DataFlow::Node node) {
exists(DataFlow::LocalSourceNode literal |
literal.asExpr().getExpr().getConstantValue().isStringlikeValue(["verify", "verify_peer"]) and
literal.flowsTo(node)
)
}
/** A configuration to track values that can disable certificate validation for Httparty. */
private class HttpartyDisablesCertificateValidationConfiguration extends DataFlowImplForHttpClientLibraries::Configuration {
HttpartyDisablesCertificateValidationConfiguration() {
this = "HttpartyDisablesCertificateValidationConfiguration"
}
/** Holds if `node` can contain the Boolean value `false`. */
private predicate isFalse(DataFlow::Node node) {
exists(DataFlow::LocalSourceNode literal |
literal.asExpr().getExpr().(BooleanLiteral).isFalse() and
literal.flowsTo(node)
)
}
override predicate isSource(DataFlow::Node source) {
source.asExpr().getExpr().(BooleanLiteral).isFalse()
}
/**
* Holds if `p` is the pair `verify: false` or `verify_peer: false`.
*/
private predicate isVerifyFalsePair(CfgNodes::ExprNodes::PairCfgNode p) {
exists(DataFlow::Node key, DataFlow::Node value |
key.asExpr() = p.getKey() and
value.asExpr() = p.getValue() and
isVerifyLiteral(key) and
isFalse(value)
)
override predicate isSink(DataFlow::Node sink) {
sink = any(HttpartyRequest req).getCertificateValidationControllingValue()
}
}

View File

@@ -8,6 +8,7 @@ private import codeql.ruby.dataflow.RemoteFlowSources
private import codeql.ruby.ApiGraphs
private import codeql.ruby.dataflow.internal.DataFlowPublic
private import codeql.ruby.DataFlow
private import codeql.ruby.dataflow.internal.DataFlowImplForHttpClientLibraries as DataFlowImplForHttpClientLibraries
/**
* A `Net::HTTP` call which initiates an HTTP request.
@@ -18,7 +19,7 @@ private import codeql.ruby.DataFlow
* response = req.get("/")
* ```
*/
class NetHttpRequest extends HTTP::Client::Request::Range {
class NetHttpRequest extends Http::Client::Request::Range, DataFlow::CallNode {
private DataFlow::CallNode request;
private DataFlow::Node responseBody;
private API::Node requestNode;
@@ -26,7 +27,7 @@ class NetHttpRequest extends HTTP::Client::Request::Range {
NetHttpRequest() {
exists(string method |
request = requestNode.asSource() and
this = request.asExpr().getExpr()
this = request
|
// Net::HTTP.get(...)
method = "get" and
@@ -65,23 +66,42 @@ class NetHttpRequest extends HTTP::Client::Request::Range {
override DataFlow::Node getResponseBody() { result = responseBody }
override predicate disablesCertificateValidation(DataFlow::Node disablingNode) {
/** Gets the value that controls certificate validation, if any. */
DataFlow::Node getCertificateValidationControllingValue() {
// A Net::HTTP request bypasses certificate validation if we see a setter
// call like this:
// foo.verify_mode = OpenSSL::SSL::VERIFY_NONE
// and then the receiver of that call flows to the receiver in the request:
// foo.request(...)
exists(DataFlow::CallNode setter |
disablingNode =
API::getTopLevelMember("OpenSSL")
.getMember("SSL")
.getMember("VERIFY_NONE")
.getAValueReachableFromSource() and
setter.asExpr().getExpr().(SetterMethodCall).getMethodName() = "verify_mode=" and
disablingNode = setter.getArgument(0) and
result = setter.getArgument(0) and
localFlow(setter.getReceiver(), request.getReceiver())
)
}
override predicate disablesCertificateValidation(
DataFlow::Node disablingNode, DataFlow::Node argumentOrigin
) {
any(NetHttpDisablesCertificateValidationConfiguration config)
.hasFlow(argumentOrigin, disablingNode) and
disablingNode = this.getCertificateValidationControllingValue()
}
override string getFramework() { result = "Net::HTTP" }
}
/** A configuration to track values that can disable certificate validation for NetHttp. */
private class NetHttpDisablesCertificateValidationConfiguration extends DataFlowImplForHttpClientLibraries::Configuration {
NetHttpDisablesCertificateValidationConfiguration() {
this = "NetHttpDisablesCertificateValidationConfiguration"
}
override predicate isSource(DataFlow::Node source) {
source = API::getTopLevelMember("OpenSSL").getMember("SSL").getMember("VERIFY_NONE").asSource()
}
override predicate isSink(DataFlow::Node sink) {
sink = any(NetHttpRequest req).getCertificateValidationControllingValue()
}
}

View File

@@ -8,6 +8,7 @@ private import codeql.ruby.Concepts
private import codeql.ruby.ApiGraphs
private import codeql.ruby.DataFlow
private import codeql.ruby.frameworks.Core
private import codeql.ruby.dataflow.internal.DataFlowImplForHttpClientLibraries as DataFlowImplForHttpClientLibraries
/**
* A call that makes an HTTP request using `OpenURI` via `URI.open` or
@@ -18,9 +19,8 @@ private import codeql.ruby.frameworks.Core
* URI.parse("http://example.com").open.read
* ```
*/
class OpenUriRequest extends HTTP::Client::Request::Range {
class OpenUriRequest extends Http::Client::Request::Range, DataFlow::CallNode {
API::Node requestNode;
DataFlow::CallNode requestUse;
OpenUriRequest() {
requestNode =
@@ -28,21 +28,26 @@ class OpenUriRequest extends HTTP::Client::Request::Range {
[API::getTopLevelMember("URI"), API::getTopLevelMember("URI").getReturn("parse")]
.getReturn("open"), API::getTopLevelMember("OpenURI").getReturn("open_uri")
] and
requestUse = requestNode.asSource() and
this = requestUse.asExpr().getExpr()
this = requestNode.asSource()
}
override DataFlow::Node getAUrlPart() { result = requestUse.getArgument(0) }
override DataFlow::Node getAUrlPart() { result = this.getArgument(0) }
override DataFlow::Node getResponseBody() {
result = requestNode.getAMethodCall(["read", "readlines"])
}
override predicate disablesCertificateValidation(DataFlow::Node disablingNode) {
exists(DataFlow::Node arg |
arg.asExpr() = requestUse.asExpr().(CfgNodes::ExprNodes::MethodCallCfgNode).getAnArgument() and
argumentDisablesValidation(arg, disablingNode)
)
/** Gets the value that controls certificate validation, if any. */
DataFlow::Node getCertificateValidationControllingValue() {
result = this.getKeywordArgumentIncludeHashArgument("ssl_verify_mode")
}
override predicate disablesCertificateValidation(
DataFlow::Node disablingNode, DataFlow::Node argumentOrigin
) {
any(OpenUriDisablesCertificateValidationConfiguration config)
.hasFlow(argumentOrigin, disablingNode) and
disablingNode = this.getCertificateValidationControllingValue()
}
override string getFramework() { result = "OpenURI" }
@@ -56,72 +61,60 @@ class OpenUriRequest extends HTTP::Client::Request::Range {
* Kernel.open("http://example.com").read
* ```
*/
class OpenUriKernelOpenRequest extends HTTP::Client::Request::Range {
DataFlow::CallNode requestUse;
class OpenUriKernelOpenRequest extends Http::Client::Request::Range, DataFlow::CallNode {
OpenUriKernelOpenRequest() {
requestUse instanceof KernelMethodCall and
this.getMethodName() = "open" and
this = requestUse.asExpr().getExpr()
this instanceof KernelMethodCall and
this.getMethodName() = "open"
}
override DataFlow::Node getAUrlPart() { result = requestUse.getArgument(0) }
override DataFlow::Node getAUrlPart() { result = this.getArgument(0) }
override DataFlow::CallNode getResponseBody() {
result.asExpr().getExpr().(MethodCall).getMethodName() in ["read", "readlines"] and
requestUse.(DataFlow::LocalSourceNode).flowsTo(result.getReceiver())
this.(DataFlow::LocalSourceNode).flowsTo(result.getReceiver())
}
override predicate disablesCertificateValidation(DataFlow::Node disablingNode) {
exists(DataFlow::Node arg, int i |
i > 0 and
arg.asExpr() = requestUse.asExpr().(CfgNodes::ExprNodes::MethodCallCfgNode).getArgument(i) and
argumentDisablesValidation(arg, disablingNode)
/** Gets the value that controls certificate validation, if any. */
DataFlow::Node getCertificateValidationControllingValue() {
result = this.getKeywordArgument("ssl_verify_mode")
or
// using a hashliteral
exists(
DataFlow::LocalSourceNode optionsNode, CfgNodes::ExprNodes::PairCfgNode p, DataFlow::Node key
|
// can't flow to argument 0, since that's the URL
optionsNode.flowsTo(this.getArgument(any(int i | i > 0))) and
p = optionsNode.asExpr().(CfgNodes::ExprNodes::HashLiteralCfgNode).getAKeyValuePair() and
key.asExpr() = p.getKey() and
key.getALocalSource().asExpr().getConstantValue().isStringlikeValue("ssl_verify_mode") and
result.asExpr() = p.getValue()
)
}
override predicate disablesCertificateValidation(
DataFlow::Node disablingNode, DataFlow::Node argumentOrigin
) {
any(OpenUriDisablesCertificateValidationConfiguration config)
.hasFlow(argumentOrigin, disablingNode) and
disablingNode = this.getCertificateValidationControllingValue()
}
override string getFramework() { result = "OpenURI" }
}
/**
* Holds if the argument `arg` is an options hash that disables certificate
* validation, and `disablingNode` is the specific node representing the
* `ssl_verify_mode: OpenSSL::SSL_VERIFY_NONE` pair.
*/
private predicate argumentDisablesValidation(DataFlow::Node arg, DataFlow::Node disablingNode) {
// Either passed as an individual key:value argument, e.g.:
// URI.open(..., ssl_verify_mode: OpenSSL::SSL::VERIFY_NONE)
isSslVerifyModeNonePair(arg.asExpr()) and
disablingNode = arg
or
// Or as a single hash argument, e.g.:
// URI.open(..., { ssl_verify_mode: OpenSSL::SSL::VERIFY_NONE, ... })
exists(DataFlow::LocalSourceNode optionsNode, CfgNodes::ExprNodes::PairCfgNode p |
p = optionsNode.asExpr().(CfgNodes::ExprNodes::HashLiteralCfgNode).getAKeyValuePair() and
isSslVerifyModeNonePair(p) and
optionsNode.flowsTo(arg) and
disablingNode.asExpr() = p
)
}
/** A configuration to track values that can disable certificate validation for OpenURI. */
private class OpenUriDisablesCertificateValidationConfiguration extends DataFlowImplForHttpClientLibraries::Configuration {
OpenUriDisablesCertificateValidationConfiguration() {
this = "OpenUriDisablesCertificateValidationConfiguration"
}
/** Holds if `p` is the pair `ssl_verify_mode: OpenSSL::SSL::VERIFY_NONE`. */
private predicate isSslVerifyModeNonePair(CfgNodes::ExprNodes::PairCfgNode p) {
exists(DataFlow::Node key, DataFlow::Node value |
key.asExpr() = p.getKey() and
value.asExpr() = p.getValue() and
isSslVerifyModeLiteral(key) and
value =
API::getTopLevelMember("OpenSSL")
.getMember("SSL")
.getMember("VERIFY_NONE")
.getAValueReachableFromSource()
)
}
override predicate isSource(DataFlow::Node source) {
source = API::getTopLevelMember("OpenSSL").getMember("SSL").getMember("VERIFY_NONE").asSource()
}
/** Holds if `node` can represent the symbol literal `:ssl_verify_mode`. */
private predicate isSslVerifyModeLiteral(DataFlow::Node node) {
exists(DataFlow::LocalSourceNode literal |
literal.asExpr().getExpr().getConstantValue().isStringlikeValue("ssl_verify_mode") and
literal.flowsTo(node)
)
override predicate isSink(DataFlow::Node sink) {
sink = any(OpenUriRequest req).getCertificateValidationControllingValue()
or
sink = any(OpenUriKernelOpenRequest req).getCertificateValidationControllingValue()
}
}

View File

@@ -7,6 +7,7 @@ private import codeql.ruby.CFG
private import codeql.ruby.Concepts
private import codeql.ruby.ApiGraphs
private import codeql.ruby.DataFlow
private import codeql.ruby.dataflow.internal.DataFlowImplForHttpClientLibraries as DataFlowImplForHttpClientLibraries
/**
* A call that makes an HTTP request using `RestClient`.
@@ -16,14 +17,12 @@ private import codeql.ruby.DataFlow
* RestClient::Request.execute(url: "http://example.com").body
* ```
*/
class RestClientHttpRequest extends HTTP::Client::Request::Range {
DataFlow::CallNode requestUse;
class RestClientHttpRequest extends Http::Client::Request::Range, DataFlow::CallNode {
API::Node requestNode;
API::Node connectionNode;
RestClientHttpRequest() {
requestUse = requestNode.asSource() and
this = requestUse.asExpr().getExpr() and
this = requestNode.asSource() and
(
connectionNode =
[
@@ -39,59 +38,44 @@ class RestClientHttpRequest extends HTTP::Client::Request::Range {
}
override DataFlow::Node getAUrlPart() {
result = requestUse.getKeywordArgument("url")
result = this.getKeywordArgument("url")
or
result = requestUse.getArgument(0) and
result = this.getArgument(0) and
// this rules out the alternative above
not result.asExpr().getExpr() instanceof Pair
}
override DataFlow::Node getResponseBody() { result = requestNode.getAMethodCall("body") }
override predicate disablesCertificateValidation(DataFlow::Node disablingNode) {
// `RestClient::Resource::new` takes an options hash argument, and we're
// looking for `{ verify_ssl: OpenSSL::SSL::VERIFY_NONE }`.
exists(DataFlow::Node arg, int i |
i > 0 and
arg = connectionNode.getAValueReachableFromSource().(DataFlow::CallNode).getArgument(i)
|
// Either passed as an individual key:value argument, e.g.:
// RestClient::Resource.new(..., verify_ssl: OpenSSL::SSL::VERIFY_NONE)
isVerifySslNonePair(arg.asExpr()) and
disablingNode = arg
or
// Or as a single hash argument, e.g.:
// RestClient::Resource.new(..., { verify_ssl: OpenSSL::SSL::VERIFY_NONE })
exists(DataFlow::LocalSourceNode optionsNode, CfgNodes::ExprNodes::PairCfgNode p |
p = optionsNode.asExpr().(CfgNodes::ExprNodes::HashLiteralCfgNode).getAKeyValuePair() and
isVerifySslNonePair(p) and
optionsNode.flowsTo(arg) and
disablingNode.asExpr() = p
)
/** Gets the value that controls certificate validation, if any. */
DataFlow::Node getCertificateValidationControllingValue() {
exists(DataFlow::CallNode newCall | newCall = connectionNode.getAValueReachableFromSource() |
result = newCall.getKeywordArgumentIncludeHashArgument("verify_ssl")
)
}
override predicate disablesCertificateValidation(
DataFlow::Node disablingNode, DataFlow::Node argumentOrigin
) {
any(RestClientDisablesCertificateValidationConfiguration config)
.hasFlow(argumentOrigin, disablingNode) and
disablingNode = this.getCertificateValidationControllingValue()
}
override string getFramework() { result = "RestClient" }
}
/** Holds if `p` is the pair `verify_ssl: OpenSSL::SSL::VERIFY_NONE`. */
private predicate isVerifySslNonePair(CfgNodes::ExprNodes::PairCfgNode p) {
exists(DataFlow::Node key, DataFlow::Node value |
key.asExpr() = p.getKey() and
value.asExpr() = p.getValue() and
isSslVerifyModeLiteral(key) and
value =
API::getTopLevelMember("OpenSSL")
.getMember("SSL")
.getMember("VERIFY_NONE")
.getAValueReachableFromSource()
)
}
/** A configuration to track values that can disable certificate validation for RestClient. */
private class RestClientDisablesCertificateValidationConfiguration extends DataFlowImplForHttpClientLibraries::Configuration {
RestClientDisablesCertificateValidationConfiguration() {
this = "RestClientDisablesCertificateValidationConfiguration"
}
/** Holds if `node` can represent the symbol literal `:verify_ssl`. */
private predicate isSslVerifyModeLiteral(DataFlow::Node node) {
exists(DataFlow::LocalSourceNode literal |
literal.asExpr().getExpr().getConstantValue().isStringlikeValue("verify_ssl") and
literal.flowsTo(node)
)
override predicate isSource(DataFlow::Node source) {
source = API::getTopLevelMember("OpenSSL").getMember("SSL").getMember("VERIFY_NONE").asSource()
}
override predicate isSink(DataFlow::Node sink) {
sink = any(RestClientHttpRequest req).getCertificateValidationControllingValue()
}
}

View File

@@ -7,6 +7,7 @@ private import codeql.ruby.CFG
private import codeql.ruby.Concepts
private import codeql.ruby.ApiGraphs
private import codeql.ruby.DataFlow
private import codeql.ruby.dataflow.internal.DataFlowImplForHttpClientLibraries as DataFlowImplForHttpClientLibraries
/**
* A call that makes an HTTP request using `Typhoeus`.
@@ -14,68 +15,47 @@ private import codeql.ruby.DataFlow
* Typhoeus.get("http://example.com").body
* ```
*/
class TyphoeusHttpRequest extends HTTP::Client::Request::Range {
DataFlow::CallNode requestUse;
class TyphoeusHttpRequest extends Http::Client::Request::Range, DataFlow::CallNode {
API::Node requestNode;
TyphoeusHttpRequest() {
requestUse = requestNode.asSource() and
this = requestNode.asSource() and
requestNode =
API::getTopLevelMember("Typhoeus")
.getReturn(["get", "head", "delete", "options", "post", "put", "patch"]) and
this = requestUse.asExpr().getExpr()
.getReturn(["get", "head", "delete", "options", "post", "put", "patch"])
}
override DataFlow::Node getAUrlPart() { result = requestUse.getArgument(0) }
override DataFlow::Node getAUrlPart() { result = this.getArgument(0) }
override DataFlow::Node getResponseBody() { result = requestNode.getAMethodCall("body") }
override predicate disablesCertificateValidation(DataFlow::Node disablingNode) {
// Check for `ssl_verifypeer: false` in the options hash.
exists(DataFlow::Node arg, int i |
i > 0 and arg.asExpr().getExpr() = requestUse.asExpr().getExpr().(MethodCall).getArgument(i)
|
// Either passed as an individual key:value argument, e.g.:
// Typhoeus.get(..., ssl_verifypeer: false)
isSslVerifyPeerFalsePair(arg.asExpr()) and
disablingNode = arg
or
// Or as a single hash argument, e.g.:
// Typhoeus.get(..., { ssl_verifypeer: false, ... })
exists(DataFlow::LocalSourceNode optionsNode, CfgNodes::ExprNodes::PairCfgNode p |
p = optionsNode.asExpr().(CfgNodes::ExprNodes::HashLiteralCfgNode).getAKeyValuePair() and
isSslVerifyPeerFalsePair(p) and
optionsNode.flowsTo(arg) and
disablingNode.asExpr() = p
)
)
/** Gets the value that controls certificate validation, if any. */
DataFlow::Node getCertificateValidationControllingValue() {
result = this.getKeywordArgumentIncludeHashArgument("ssl_verifypeer")
}
override predicate disablesCertificateValidation(
DataFlow::Node disablingNode, DataFlow::Node argumentOrigin
) {
any(TyphoeusDisablesCertificateValidationConfiguration config)
.hasFlow(argumentOrigin, disablingNode) and
disablingNode = this.getCertificateValidationControllingValue()
}
override string getFramework() { result = "Typhoeus" }
}
/** Holds if `p` is the pair `ssl_verifypeer: false`. */
private predicate isSslVerifyPeerFalsePair(CfgNodes::ExprNodes::PairCfgNode p) {
exists(DataFlow::Node key, DataFlow::Node value |
key.asExpr() = p.getKey() and
value.asExpr() = p.getValue() and
isSslVerifyPeerLiteral(key) and
isFalse(value)
)
}
/** A configuration to track values that can disable certificate validation for Typhoeus. */
private class TyphoeusDisablesCertificateValidationConfiguration extends DataFlowImplForHttpClientLibraries::Configuration {
TyphoeusDisablesCertificateValidationConfiguration() {
this = "TyphoeusDisablesCertificateValidationConfiguration"
}
/** Holds if `node` represents the symbol literal `verify` or `verify_peer`. */
private predicate isSslVerifyPeerLiteral(DataFlow::Node node) {
exists(DataFlow::LocalSourceNode literal |
literal.asExpr().getExpr().getConstantValue().isStringlikeValue("ssl_verifypeer") and
literal.flowsTo(node)
)
}
override predicate isSource(DataFlow::Node source) {
source.asExpr().getExpr().(BooleanLiteral).isFalse()
}
/** Holds if `node` can contain the Boolean value `false`. */
private predicate isFalse(DataFlow::Node node) {
exists(DataFlow::LocalSourceNode literal |
literal.asExpr().getExpr().(BooleanLiteral).isFalse() and
literal.flowsTo(node)
)
override predicate isSink(DataFlow::Node sink) {
sink = any(TyphoeusHttpRequest req).getCertificateValidationControllingValue()
}
}

View File

@@ -87,3 +87,70 @@ module Cryptography {
predicate isWeak() { this = "ECB" }
}
}
/** Provides classes for modeling HTTP-related APIs. */
module Http {
/** Provides classes for modeling HTTP clients. */
module Client {
/**
* A data-flow node that makes an outgoing HTTP request.
*
* Extend this class to refine existing API models. If you want to model new APIs,
* extend `Http::Client::Request::Range` instead.
*/
class Request extends DataFlow::Node instanceof Request::Range {
/**
* Gets a data-flow node that contributes to the URL of the request.
* Depending on the framework, a request may have multiple nodes which contribute to the URL.
*/
DataFlow::Node getAUrlPart() { result = super.getAUrlPart() }
/** Gets a string that identifies the framework used for this request. */
string getFramework() { result = super.getFramework() }
/**
* Holds if this request is made using a mode that disables SSL/TLS
* certificate validation, where `disablingNode` represents the point at
* which the validation was disabled, and `argumentOrigin` represents the origin
* of the argument that disabled the validation (which could be the same node as
* `disablingNode`).
*/
predicate disablesCertificateValidation(
DataFlow::Node disablingNode, DataFlow::Node argumentOrigin
) {
super.disablesCertificateValidation(disablingNode, argumentOrigin)
}
}
/** Provides a class for modeling new HTTP requests. */
module Request {
/**
* A data-flow node that makes an outgoing HTTP request.
*
* Extend this class to model new APIs. If you want to refine existing API models,
* extend `Http::Client::Request` instead.
*/
abstract class Range extends DataFlow::Node {
/**
* Gets a data-flow node that contributes to the URL of the request.
* Depending on the framework, a request may have multiple nodes which contribute to the URL.
*/
abstract DataFlow::Node getAUrlPart();
/** Gets a string that identifies the framework used for this request. */
abstract string getFramework();
/**
* Holds if this request is made using a mode that disables SSL/TLS
* certificate validation, where `disablingNode` represents the point at
* which the validation was disabled, and `argumentOrigin` represents the origin
* of the argument that disabled the validation (which could be the same node as
* `disablingNode`).
*/
abstract predicate disablesCertificateValidation(
DataFlow::Node disablingNode, DataFlow::Node argumentOrigin
);
}
}
}
}

View File

@@ -0,0 +1,111 @@
/**
* Provides default sources, sinks and sanitizers for reasoning about hard-coded
* data being interpreted as code, as well as extension points for adding your
* own.
*/
private import codeql.ruby.DataFlow
private import codeql.ruby.security.CodeInjectionCustomizations
private import codeql.ruby.AST as AST
private import codeql.ruby.controlflow.CfgNodes
/**
* Provides default sources, sinks and sanitizers for reasoning about hard-coded
* data being interpreted as code, as well as extension points for adding your
* own.
*/
module HardcodedDataInterpretedAsCode {
/**
* Flow states used to distinguish value-preserving flow from taint flow.
*/
module FlowState {
/** Flow state used to track value-preserving flow. */
DataFlow::FlowState data() { result = "data" }
/** Flow state used to tainted data (non-value preserving flow). */
DataFlow::FlowState taint() { result = "taint" }
}
/**
* A data flow source for hard-coded data.
*/
abstract class Source extends DataFlow::Node {
/** Gets a flow label for which this is a source. */
DataFlow::FlowState getLabel() { result = FlowState::data() }
}
/**
* A data flow sink for code injection.
*/
abstract class Sink extends DataFlow::Node {
/** Gets a description of what kind of sink this is. */
abstract string getKind();
/** Gets a flow label for which this is a sink. */
DataFlow::FlowState getLabel() {
// We want to ignore value-flow and only consider taint-flow, since the
// source is just a hex string, and evaluating that directly will just
// cause a syntax error.
result = FlowState::taint()
}
}
/** A sanitizer for hard-coded data. */
abstract class Sanitizer extends DataFlow::Node { }
/**
* A constant string consisting of eight or more hexadecimal characters (including at
* least one digit), viewed as a source of hard-coded data that should not be
* interpreted as code.
*/
private class HexStringSource extends Source {
HexStringSource() {
exists(string val |
this.asExpr().(ExprNodes::StringLiteralCfgNode).getConstantValue().isString(val)
|
val.regexpMatch("[0-9a-fA-F]{8,}") and
val.regexpMatch(".*[0-9].*")
)
}
}
/**
* A string literal whose raw text is made up entirely of `\x` escape
* sequences, viewed as a source of hard-coded data that should not be
* interpreted as code.
*/
private class HexEscapedStringSource extends Source {
HexEscapedStringSource() {
forex(StringComponentCfgNode c |
c = this.asExpr().(ExprNodes::StringlikeLiteralCfgNode).getAComponent()
|
c.getNode().(AST::StringEscapeSequenceComponent).getRawText().matches("\\x%")
)
}
}
/**
* A code injection sink; hard-coded data should not flow here.
*/
private class DefaultCodeInjectionSink extends Sink {
DefaultCodeInjectionSink() { this instanceof CodeInjection::Sink }
override string getKind() { result = "code" }
}
/**
* An argument to `require` path; hard-coded data should not flow here.
*/
private class RequireArgumentSink extends Sink {
RequireArgumentSink() {
exists(DataFlow::CallNode require |
require.getReceiver().getExprNode().getExpr() instanceof AST::SelfVariableAccess and
require.getMethodName() = "require"
|
this = require.getArgument(0)
)
}
override string getKind() { result = "an import path" }
}
}

View File

@@ -0,0 +1,48 @@
/**
* Provides a taint-tracking configuration for reasoning about hard-coded data
* being interpreted as code.
*
* Note, for performance reasons: only import this file if
* `HardcodedDataInterpretedAsCode::Configuration` is needed, otherwise
* `HardcodedDataInterpretedAsCodeCustomizations` should be imported instead.
*/
private import codeql.ruby.DataFlow
private import codeql.ruby.TaintTracking
private import codeql.ruby.dataflow.internal.TaintTrackingPrivate
import HardcodedDataInterpretedAsCodeCustomizations::HardcodedDataInterpretedAsCode
/**
* A taint-tracking configuration for reasoning about hard-coded data
* being interpreted as code.
*
* We extend `DataFlow::Configuration` rather than
* `TaintTracking::Configuration`, so that we can set the flow state to
* `"taint"` on a taint step.
*/
class Configuration extends DataFlow::Configuration {
Configuration() { this = "HardcodedDataInterpretedAsCode" }
override predicate isSource(DataFlow::Node source, DataFlow::FlowState label) {
source.(Source).getLabel() = label
}
override predicate isSink(DataFlow::Node sink, DataFlow::FlowState label) {
sink.(Sink).getLabel() = label
}
override predicate isBarrier(DataFlow::Node node) {
super.isBarrier(node) or
node instanceof Sanitizer
}
override predicate isAdditionalFlowStep(
DataFlow::Node nodeFrom, DataFlow::FlowState stateFrom, DataFlow::Node nodeTo,
DataFlow::FlowState stateTo
) {
defaultAdditionalTaintStep(nodeFrom, nodeTo) and
// This is a taint step, so the flow state becomes `taint`.
stateFrom = [FlowState::data(), FlowState::taint()] and
stateTo = FlowState::taint()
}
}

View File

@@ -12,10 +12,10 @@ private import HttpToFileAccessCustomizations::HttpToFileAccess
/**
* An access to a user-controlled HTTP request input, considered as a flow source for writing user-controlled data to files
*/
private class RequestInputAccessAsSource extends Source instanceof HTTP::Server::RequestInputAccess {
private class RequestInputAccessAsSource extends Source instanceof Http::Server::RequestInputAccess {
}
/** A response from an outgoing HTTP request, considered as a flow source for writing user-controlled data to files. */
private class HttpResponseAsSource extends Source {
HttpResponseAsSource() { this = any(HTTP::Client::Request r).getResponseBody() }
HttpResponseAsSource() { this = any(Http::Client::Request r).getResponseBody() }
}

View File

@@ -135,7 +135,7 @@ module InsecureDownload {
* In other words, if the URL is HTTP and the extension is in `unsafeExtension()`.
*/
private class HttpResponseAsSink extends Sink {
private HTTP::Client::Request req;
private Http::Client::Request req;
HttpResponseAsSink() {
this = req.getAUrlPart() and
@@ -143,7 +143,7 @@ module InsecureDownload {
hasUnsafeExtension(req.getAUrlPart().asExpr().getConstantValue().getString())
}
override DataFlow::Node getDownloadCall() { result.asExpr().getExpr() = req }
override DataFlow::Node getDownloadCall() { result = req }
override DataFlow::FlowState getALabel() {
result instanceof Label::SensitiveInsecure
@@ -155,7 +155,7 @@ module InsecureDownload {
/**
* Gets a node for the response from `request`, type-tracked using `t`.
*/
DataFlow::LocalSourceNode clientRequestResponse(TypeTracker t, HTTP::Client::Request request) {
DataFlow::LocalSourceNode clientRequestResponse(TypeTracker t, Http::Client::Request request) {
t.start() and
result = request.getResponseBody()
or
@@ -166,7 +166,7 @@ module InsecureDownload {
* A url that is downloaded through an insecure connection, where the result ends up being saved to a sensitive location.
*/
class FileWriteSink extends Sink {
HTTP::Client::Request request;
Http::Client::Request request;
FileWriteSink() {
// For example, in:
@@ -193,6 +193,6 @@ module InsecureDownload {
override DataFlow::FlowState getALabel() { result instanceof Label::Insecure }
override DataFlow::Node getDownloadCall() { result.asExpr().getExpr() = request }
override DataFlow::Node getDownloadCall() { result = request }
}
}

View File

@@ -96,7 +96,10 @@ class OverlyWideRange extends RegExpCharacterRange {
toCodePoint("A") <= high
or
// a non-alphanumeric char as part of the range boundaries
exists(int bound | bound = [low, high] | not isAlphanumeric(bound.toUnicode()))
exists(int bound | bound = [low, high] | not isAlphanumeric(bound.toUnicode())) and
// while still being ascii
low < 128 and
high < 128
) and
// allowlist for known ranges
not this = allowedWideRanges()
@@ -173,7 +176,7 @@ module RangePrinter {
}
/** Gets the number of parts we should print for a given `range`. */
private int parts(OverlyWideRange range) { result = 1 + strictcount(cutoff(range, _)) }
private int parts(OverlyWideRange range) { result = 1 + count(cutoff(range, _)) }
/** Holds if the given part of a range should span from `low` to `high`. */
private predicate part(OverlyWideRange range, int part, string low, string high) {

View File

@@ -45,7 +45,7 @@ module ServerSideRequestForgery {
/** The URL of an HTTP request, considered as a sink. */
class HttpRequestAsSink extends Sink {
HttpRequestAsSink() { exists(HTTP::Client::Request req | req.getAUrlPart() = this) }
HttpRequestAsSink() { exists(Http::Client::Request req | req.getAUrlPart() = this) }
}
/** A string interpolation with a fixed prefix, considered as a flow sanitizer. */

View File

@@ -57,7 +57,7 @@ module UrlRedirect {
*/
class RedirectLocationAsSink extends Sink {
RedirectLocationAsSink() {
exists(HTTP::Server::HttpRedirectResponse e, MethodBase method |
exists(Http::Server::HttpRedirectResponse e, MethodBase method |
this = e.getRedirectLocation() and
// We only want handlers for GET requests.
// Handlers for other HTTP methods are not as vulnerable to URL

View File

@@ -115,6 +115,7 @@ private newtype TStatePair =
private int rankState(State state) {
state =
rank[result](State s, Location l |
stateInsideBacktracking(s) and
l = s.getRepr().getLocation()
|
s order by l.getStartLine(), l.getStartColumn(), s.toString()

View File

@@ -93,8 +93,6 @@ class RegExpRoot extends RegExpTerm {
* Holds if this root term is relevant to the ReDoS analysis.
*/
predicate isRelevant() {
// there is at least one repetition
getRoot(any(InfiniteRepetitionQuantifier q)) = this and
// is actually used as a RegExp
this.isUsedAsRegExp() and
// not excluded for library specific reasons
@@ -877,6 +875,107 @@ predicate isStartState(State state) {
*/
signature predicate isCandidateSig(State state, string pump);
/**
* Holds if `state` is a candidate for ReDoS.
*/
signature predicate isCandidateSig(State state);
/**
* Predicates for constructing a prefix string that leads to a given state.
*/
module PrefixConstruction<isCandidateSig/1 isCandidate> {
/**
* Holds if `state` is the textually last start state for the regular expression.
*/
private predicate lastStartState(RelevantState state) {
exists(RegExpRoot root |
state =
max(RelevantState s, Location l |
isStartState(s) and
getRoot(s.getRepr()) = root and
l = s.getRepr().getLocation()
|
s
order by
l.getStartLine(), l.getStartColumn(), s.getRepr().toString(), l.getEndColumn(),
l.getEndLine()
)
)
}
/**
* Holds if there exists any transition (Epsilon() or other) from `a` to `b`.
*/
private predicate existsTransition(State a, State b) { delta(a, _, b) }
/**
* Gets the minimum number of transitions it takes to reach `state` from the `start` state.
*/
int prefixLength(State start, State state) =
shortestDistances(lastStartState/1, existsTransition/2)(start, state, result)
/**
* Gets the minimum number of transitions it takes to reach `state` from the start state.
*/
private int lengthFromStart(State state) { result = prefixLength(_, state) }
/**
* Gets a string for which the regular expression will reach `state`.
*
* Has at most one result for any given `state`.
* This predicate will not always have a result even if there is a ReDoS issue in
* the regular expression.
*/
string prefix(State state) {
lastStartState(state) and
result = ""
or
// the search stops past the last redos candidate state.
lengthFromStart(state) <= max(lengthFromStart(any(State s | isCandidate(s)))) and
exists(State prev |
// select a unique predecessor (by an arbitrary measure)
prev =
min(State s, Location loc |
lengthFromStart(s) = lengthFromStart(state) - 1 and
loc = s.getRepr().getLocation() and
delta(s, _, state)
|
s
order by
loc.getStartLine(), loc.getStartColumn(), loc.getEndLine(), loc.getEndColumn(),
s.getRepr().toString()
)
|
// greedy search for the shortest prefix
result = prefix(prev) and delta(prev, Epsilon(), state)
or
not delta(prev, Epsilon(), state) and
result = prefix(prev) + getCanonicalEdgeChar(prev, state)
)
}
/**
* Gets a canonical char for which there exists a transition from `prev` to `next` in the NFA.
*/
private string getCanonicalEdgeChar(State prev, State next) {
result =
min(string c | delta(prev, any(InputSymbol symbol | c = intersect(Any(), symbol)), next))
}
/** A state within a regular expression that contains a candidate state. */
class RelevantState instanceof State {
RelevantState() {
exists(State s | isCandidate(s) | getRoot(s.getRepr()) = getRoot(this.getRepr()))
}
/** Gets a string representation for this state in a regular expression. */
string toString() { result = State.super.toString() }
/** Gets the term represented by this state. */
RegExpTerm getRepr() { result = State.super.getRepr() }
}
}
/**
* A module for pruning candidate ReDoS states.
* The candidates are specified by the `isCandidate` signature predicate.
@@ -910,95 +1009,11 @@ module ReDoSPruning<isCandidateSig/2 isCandidate> {
/** Gets a state that can reach the `accept-any` state using only epsilon steps. */
private State acceptsAnySuffix() { epsilonSucc*(result) = AcceptAnySuffix(_) }
/**
* Predicates for constructing a prefix string that leads to a given state.
*/
private module PrefixConstruction {
/**
* Holds if `state` is the textually last start state for the regular expression.
*/
private predicate lastStartState(State state) {
exists(RegExpRoot root |
state =
max(State s, Location l |
s = stateInPumpableRegexp() and
isStartState(s) and
getRoot(s.getRepr()) = root and
l = s.getRepr().getLocation()
|
s
order by
l.getStartLine(), l.getStartColumn(), s.getRepr().toString(), l.getEndColumn(),
l.getEndLine()
)
)
}
predicate isCandidateState(State s) { isReDoSCandidate(s, _) }
/**
* Holds if there exists any transition (Epsilon() or other) from `a` to `b`.
*/
private predicate existsTransition(State a, State b) { delta(a, _, b) }
import PrefixConstruction<isCandidateState/1> as Prefix
/**
* Gets the minimum number of transitions it takes to reach `state` from the `start` state.
*/
int prefixLength(State start, State state) =
shortestDistances(lastStartState/1, existsTransition/2)(start, state, result)
/**
* Gets the minimum number of transitions it takes to reach `state` from the start state.
*/
private int lengthFromStart(State state) { result = prefixLength(_, state) }
/**
* Gets a string for which the regular expression will reach `state`.
*
* Has at most one result for any given `state`.
* This predicate will not always have a result even if there is a ReDoS issue in
* the regular expression.
*/
string prefix(State state) {
lastStartState(state) and
result = ""
or
// the search stops past the last redos candidate state.
lengthFromStart(state) <= max(lengthFromStart(any(State s | isReDoSCandidate(s, _)))) and
exists(State prev |
// select a unique predecessor (by an arbitrary measure)
prev =
min(State s, Location loc |
lengthFromStart(s) = lengthFromStart(state) - 1 and
loc = s.getRepr().getLocation() and
delta(s, _, state)
|
s
order by
loc.getStartLine(), loc.getStartColumn(), loc.getEndLine(), loc.getEndColumn(),
s.getRepr().toString()
)
|
// greedy search for the shortest prefix
result = prefix(prev) and delta(prev, Epsilon(), state)
or
not delta(prev, Epsilon(), state) and
result = prefix(prev) + getCanonicalEdgeChar(prev, state)
)
}
/**
* Gets a canonical char for which there exists a transition from `prev` to `next` in the NFA.
*/
private string getCanonicalEdgeChar(State prev, State next) {
result =
min(string c | delta(prev, any(InputSymbol symbol | c = intersect(Any(), symbol)), next))
}
/** Gets a state within a regular expression that has a pumpable state. */
pragma[noinline]
State stateInPumpableRegexp() {
exists(State s | isReDoSCandidate(s, _) | getRoot(s.getRepr()) = getRoot(result.getRepr()))
}
}
class RelevantState = Prefix::RelevantState;
/**
* Predicates for testing the presence of a rejecting suffix.
@@ -1018,8 +1033,6 @@ module ReDoSPruning<isCandidateSig/2 isCandidate> {
* using epsilon transitions. But any attempt at repeating `w` will end in a state that accepts all suffixes.
*/
private module SuffixConstruction {
import PrefixConstruction
/**
* Holds if all states reachable from `fork` by repeating `w`
* are likely rejectable by appending some suffix.
@@ -1035,32 +1048,26 @@ module ReDoSPruning<isCandidateSig/2 isCandidate> {
* This predicate might find impossible suffixes when searching for suffixes of length > 1, which can cause FPs.
*/
pragma[noinline]
private predicate isLikelyRejectable(State s) {
s = stateInPumpableRegexp() and
(
// exists a reject edge with some char.
hasRejectEdge(s)
or
hasEdgeToLikelyRejectable(s)
or
// stopping here is rejection
isRejectState(s)
)
private predicate isLikelyRejectable(RelevantState s) {
// exists a reject edge with some char.
hasRejectEdge(s)
or
hasEdgeToLikelyRejectable(s)
or
// stopping here is rejection
isRejectState(s)
}
/**
* Holds if `s` is not an accept state, and there is no epsilon transition to an accept state.
*/
predicate isRejectState(State s) {
s = stateInPumpableRegexp() and not epsilonSucc*(s) = Accept(_)
}
predicate isRejectState(RelevantState s) { not epsilonSucc*(s) = Accept(_) }
/**
* Holds if there is likely a non-empty suffix leading to rejection starting in `s`.
*/
pragma[noopt]
predicate hasEdgeToLikelyRejectable(State s) {
s = stateInPumpableRegexp() and
predicate hasEdgeToLikelyRejectable(RelevantState s) {
// all edges (at least one) with some char leads to another state that is rejectable.
// the `next` states might not share a common suffix, which can cause FPs.
exists(string char | char = hasEdgeToLikelyRejectableHelper(s) |
@@ -1075,8 +1082,7 @@ module ReDoSPruning<isCandidateSig/2 isCandidate> {
* and `s` has not been found to be rejectable by `hasRejectEdge` or `isRejectState`.
*/
pragma[noinline]
private string hasEdgeToLikelyRejectableHelper(State s) {
s = stateInPumpableRegexp() and
private string hasEdgeToLikelyRejectableHelper(RelevantState s) {
not hasRejectEdge(s) and
not isRejectState(s) and
deltaClosedChar(s, result, _)
@@ -1087,9 +1093,7 @@ module ReDoSPruning<isCandidateSig/2 isCandidate> {
* along epsilon edges, such that there is a transition from
* `prev` to `next` that the character symbol `char`.
*/
predicate deltaClosedChar(State prev, string char, State next) {
prev = stateInPumpableRegexp() and
next = stateInPumpableRegexp() and
predicate deltaClosedChar(RelevantState prev, string char, RelevantState next) {
deltaClosed(prev, getAnInputSymbolMatchingRelevant(char), next)
}
@@ -1099,18 +1103,28 @@ module ReDoSPruning<isCandidateSig/2 isCandidate> {
result = getAnInputSymbolMatching(char)
}
pragma[noinline]
RegExpRoot relevantRoot() {
exists(RegExpTerm term, State s |
s.getRepr() = term and isCandidateState(s) and result = term.getRootTerm()
)
}
/**
* Gets a char used for finding possible suffixes inside `root`.
*/
pragma[noinline]
private string relevant(RegExpRoot root) {
exists(ascii(result)) and exists(root)
or
exists(InputSymbol s | belongsTo(s, root) | result = intersect(s, _))
or
// The characters from `hasSimpleRejectEdge`. Only `\n` is really needed (as `\n` is not in the `ascii` relation).
// The three chars must be kept in sync with `hasSimpleRejectEdge`.
result = ["|", "\n", "Z"] and exists(root)
root = relevantRoot() and
(
exists(ascii(result)) and exists(root)
or
exists(InputSymbol s | belongsTo(s, root) | result = intersect(s, _))
or
// The characters from `hasSimpleRejectEdge`. Only `\n` is really needed (as `\n` is not in the `ascii` relation).
// The three chars must be kept in sync with `hasSimpleRejectEdge`.
result = ["|", "\n", "Z"] and exists(root)
)
}
/**
@@ -1208,12 +1222,12 @@ module ReDoSPruning<isCandidateSig/2 isCandidate> {
predicate hasReDoSResult(RegExpTerm t, string pump, State s, string prefixMsg) {
isReDoSAttackable(t, pump, s) and
(
prefixMsg = "starting with '" + escape(PrefixConstruction::prefix(s)) + "' and " and
not PrefixConstruction::prefix(s) = ""
prefixMsg = "starting with '" + escape(Prefix::prefix(s)) + "' and " and
not Prefix::prefix(s) = ""
or
PrefixConstruction::prefix(s) = "" and prefixMsg = ""
Prefix::prefix(s) = "" and prefixMsg = ""
or
not exists(PrefixConstruction::prefix(s)) and prefixMsg = ""
not exists(Prefix::prefix(s)) and prefixMsg = ""
)
}

View File

@@ -63,14 +63,6 @@ module RegExpFlags {
root.getLiteral().isIgnoreCase()
}
/**
* Gets the flags for `root`, or the empty string if `root` has no flags.
*/
string getFlags(RegExpTerm root) {
root.isRootTerm() and
result = root.getLiteral().getFlags()
}
/**
* Holds if `root` has the `s` flag for multi-line matching.
*/

View File

@@ -67,10 +67,12 @@ private predicate viableParam(
)
}
private predicate callStep(ExprNodes::CallCfgNode call, Node nodeFrom, Node nodeTo) {
/** Holds if there is flow from `arg` to `p` via the call `call`. */
pragma[nomagic]
predicate callStep(ExprNodes::CallCfgNode call, Node arg, DataFlowPrivate::ParameterNodeImpl p) {
exists(DataFlowDispatch::ParameterPosition pos |
argumentPositionMatch(call, nodeFrom, pos) and
viableParam(call, nodeTo, pos)
argumentPositionMatch(call, arg, pos) and
viableParam(call, p, pos)
)
}

View File

@@ -5,3 +5,5 @@ extractor: ruby
dbscheme: ruby.dbscheme
upgrades: upgrades
library: true
dependencies:
codeql/ssa: 0.0.1

View File

@@ -247,20 +247,16 @@ ruby_binary_def(
int right: @ruby_underscore_expression ref
);
ruby_block_body(
unique int ruby_block: @ruby_block ref,
unique int body: @ruby_block_body ref
);
ruby_block_parameters(
unique int ruby_block: @ruby_block ref,
unique int parameters: @ruby_block_parameters ref
);
@ruby_block_child_type = @ruby_token_empty_statement | @ruby_underscore_statement
#keyset[ruby_block, index]
ruby_block_child(
int ruby_block: @ruby_block ref,
int index: int ref,
unique int child: @ruby_block_child_type ref
);
ruby_block_def(
unique int id: @ruby_block
);
@@ -274,6 +270,19 @@ ruby_block_argument_def(
unique int id: @ruby_block_argument
);
@ruby_block_body_child_type = @ruby_token_empty_statement | @ruby_underscore_statement
#keyset[ruby_block_body, index]
ruby_block_body_child(
int ruby_block_body: @ruby_block_body ref,
int index: int ref,
unique int child: @ruby_block_body_child_type ref
);
ruby_block_body_def(
unique int id: @ruby_block_body
);
ruby_block_parameter_name(
unique int ruby_block_parameter: @ruby_block_parameter ref,
unique int name: @ruby_token_identifier ref
@@ -303,6 +312,19 @@ ruby_block_parameters_def(
unique int id: @ruby_block_parameters
);
@ruby_body_statement_child_type = @ruby_else | @ruby_ensure | @ruby_rescue | @ruby_token_empty_statement | @ruby_underscore_statement
#keyset[ruby_body_statement, index]
ruby_body_statement_child(
int ruby_body_statement: @ruby_body_statement ref,
int index: int ref,
unique int child: @ruby_body_statement_child_type ref
);
ruby_body_statement_def(
unique int id: @ruby_body_statement
);
ruby_break_child(
unique int ruby_break: @ruby_break ref,
unique int child: @ruby_argument_list ref
@@ -391,6 +413,11 @@ ruby_chained_string_def(
unique int id: @ruby_chained_string
);
ruby_class_body(
unique int ruby_class: @ruby_class ref,
unique int body: @ruby_body_statement ref
);
@ruby_class_name_type = @ruby_scope_resolution | @ruby_token_constant
ruby_class_superclass(
@@ -398,15 +425,6 @@ ruby_class_superclass(
unique int superclass: @ruby_superclass ref
);
@ruby_class_child_type = @ruby_else | @ruby_ensure | @ruby_rescue | @ruby_token_empty_statement | @ruby_underscore_statement
#keyset[ruby_class, index]
ruby_class_child(
int ruby_class: @ruby_class ref,
int index: int ref,
unique int child: @ruby_class_child_type ref
);
ruby_class_def(
unique int id: @ruby_class,
int name: @ruby_class_name_type ref
@@ -478,20 +496,16 @@ ruby_do_def(
unique int id: @ruby_do
);
ruby_do_block_body(
unique int ruby_do_block: @ruby_do_block ref,
unique int body: @ruby_body_statement ref
);
ruby_do_block_parameters(
unique int ruby_do_block: @ruby_do_block ref,
unique int parameters: @ruby_block_parameters ref
);
@ruby_do_block_child_type = @ruby_else | @ruby_ensure | @ruby_rescue | @ruby_token_empty_statement | @ruby_underscore_statement
#keyset[ruby_do_block, index]
ruby_do_block_child(
int ruby_do_block: @ruby_do_block ref,
int index: int ref,
unique int child: @ruby_do_block_child_type ref
);
ruby_do_block_def(
unique int id: @ruby_do_block
);
@@ -724,7 +738,7 @@ ruby_in_clause_def(
int pattern: @ruby_underscore_pattern_top_expr_body ref
);
@ruby_interpolation_child_type = @ruby_token_empty_statement | @ruby_underscore_statement
@ruby_interpolation_child_type = @ruby_token_empty_statement | @ruby_underscore_nonlocal_variable | @ruby_underscore_statement
#keyset[ruby_interpolation, index]
ruby_interpolation_child(
@@ -797,20 +811,18 @@ ruby_left_assignment_list_def(
unique int id: @ruby_left_assignment_list
);
@ruby_method_body_type = @ruby_body_statement | @ruby_rescue_modifier | @ruby_underscore_arg
ruby_method_body(
unique int ruby_method: @ruby_method ref,
unique int body: @ruby_method_body_type ref
);
ruby_method_parameters(
unique int ruby_method: @ruby_method ref,
unique int parameters: @ruby_method_parameters ref
);
@ruby_method_child_type = @ruby_else | @ruby_ensure | @ruby_rescue | @ruby_token_empty_statement | @ruby_underscore_arg | @ruby_underscore_statement
#keyset[ruby_method, index]
ruby_method_child(
int ruby_method: @ruby_method ref,
int index: int ref,
unique int child: @ruby_method_child_type ref
);
ruby_method_def(
unique int id: @ruby_method,
int name: @ruby_underscore_method_name ref
@@ -829,17 +841,13 @@ ruby_method_parameters_def(
unique int id: @ruby_method_parameters
);
@ruby_module_name_type = @ruby_scope_resolution | @ruby_token_constant
@ruby_module_child_type = @ruby_else | @ruby_ensure | @ruby_rescue | @ruby_token_empty_statement | @ruby_underscore_statement
#keyset[ruby_module, index]
ruby_module_child(
int ruby_module: @ruby_module ref,
int index: int ref,
unique int child: @ruby_module_child_type ref
ruby_module_body(
unique int ruby_module: @ruby_module ref,
unique int body: @ruby_body_statement ref
);
@ruby_module_name_type = @ruby_scope_resolution | @ruby_token_constant
ruby_module_def(
unique int id: @ruby_module,
int name: @ruby_module_name_type ref
@@ -1074,13 +1082,9 @@ ruby_setter_def(
int name: @ruby_token_identifier ref
);
@ruby_singleton_class_child_type = @ruby_else | @ruby_ensure | @ruby_rescue | @ruby_token_empty_statement | @ruby_underscore_statement
#keyset[ruby_singleton_class, index]
ruby_singleton_class_child(
int ruby_singleton_class: @ruby_singleton_class ref,
int index: int ref,
unique int child: @ruby_singleton_class_child_type ref
ruby_singleton_class_body(
unique int ruby_singleton_class: @ruby_singleton_class ref,
unique int body: @ruby_body_statement ref
);
ruby_singleton_class_def(
@@ -1088,6 +1092,13 @@ ruby_singleton_class_def(
int value: @ruby_underscore_arg ref
);
@ruby_singleton_method_body_type = @ruby_body_statement | @ruby_rescue_modifier | @ruby_underscore_arg
ruby_singleton_method_body(
unique int ruby_singleton_method: @ruby_singleton_method ref,
unique int body: @ruby_singleton_method_body_type ref
);
@ruby_singleton_method_object_type = @ruby_underscore_arg | @ruby_underscore_variable
ruby_singleton_method_parameters(
@@ -1095,15 +1106,6 @@ ruby_singleton_method_parameters(
unique int parameters: @ruby_method_parameters ref
);
@ruby_singleton_method_child_type = @ruby_else | @ruby_ensure | @ruby_rescue | @ruby_token_empty_statement | @ruby_underscore_arg | @ruby_underscore_statement
#keyset[ruby_singleton_method, index]
ruby_singleton_method_child(
int ruby_singleton_method: @ruby_singleton_method ref,
int index: int ref,
unique int child: @ruby_singleton_method_child_type ref
);
ruby_singleton_method_def(
unique int id: @ruby_singleton_method,
int name: @ruby_underscore_method_name ref,
@@ -1344,7 +1346,7 @@ case @ruby_token.kind of
;
@ruby_ast_node = @ruby_alias | @ruby_alternative_pattern | @ruby_argument_list | @ruby_array | @ruby_array_pattern | @ruby_as_pattern | @ruby_assignment | @ruby_bare_string | @ruby_bare_symbol | @ruby_begin | @ruby_begin_block | @ruby_binary | @ruby_block | @ruby_block_argument | @ruby_block_parameter | @ruby_block_parameters | @ruby_break | @ruby_call | @ruby_case__ | @ruby_case_match | @ruby_chained_string | @ruby_class | @ruby_complex | @ruby_conditional | @ruby_delimited_symbol | @ruby_destructured_left_assignment | @ruby_destructured_parameter | @ruby_do | @ruby_do_block | @ruby_element_reference | @ruby_else | @ruby_elsif | @ruby_end_block | @ruby_ensure | @ruby_exception_variable | @ruby_exceptions | @ruby_expression_reference_pattern | @ruby_find_pattern | @ruby_for | @ruby_hash | @ruby_hash_pattern | @ruby_hash_splat_argument | @ruby_hash_splat_parameter | @ruby_heredoc_body | @ruby_if | @ruby_if_guard | @ruby_if_modifier | @ruby_in | @ruby_in_clause | @ruby_interpolation | @ruby_keyword_parameter | @ruby_keyword_pattern | @ruby_lambda | @ruby_lambda_parameters | @ruby_left_assignment_list | @ruby_method | @ruby_method_parameters | @ruby_module | @ruby_next | @ruby_operator_assignment | @ruby_optional_parameter | @ruby_pair | @ruby_parenthesized_pattern | @ruby_parenthesized_statements | @ruby_pattern | @ruby_program | @ruby_range | @ruby_rational | @ruby_redo | @ruby_regex | @ruby_rescue | @ruby_rescue_modifier | @ruby_rest_assignment | @ruby_retry | @ruby_return | @ruby_right_assignment_list | @ruby_scope_resolution | @ruby_setter | @ruby_singleton_class | @ruby_singleton_method | @ruby_splat_argument | @ruby_splat_parameter | @ruby_string__ | @ruby_string_array | @ruby_subshell | @ruby_superclass | @ruby_symbol_array | @ruby_then | @ruby_token | @ruby_unary | @ruby_undef | @ruby_unless | @ruby_unless_guard | @ruby_unless_modifier | @ruby_until | @ruby_until_modifier | @ruby_variable_reference_pattern | @ruby_when | @ruby_while | @ruby_while_modifier | @ruby_yield
@ruby_ast_node = @ruby_alias | @ruby_alternative_pattern | @ruby_argument_list | @ruby_array | @ruby_array_pattern | @ruby_as_pattern | @ruby_assignment | @ruby_bare_string | @ruby_bare_symbol | @ruby_begin | @ruby_begin_block | @ruby_binary | @ruby_block | @ruby_block_argument | @ruby_block_body | @ruby_block_parameter | @ruby_block_parameters | @ruby_body_statement | @ruby_break | @ruby_call | @ruby_case__ | @ruby_case_match | @ruby_chained_string | @ruby_class | @ruby_complex | @ruby_conditional | @ruby_delimited_symbol | @ruby_destructured_left_assignment | @ruby_destructured_parameter | @ruby_do | @ruby_do_block | @ruby_element_reference | @ruby_else | @ruby_elsif | @ruby_end_block | @ruby_ensure | @ruby_exception_variable | @ruby_exceptions | @ruby_expression_reference_pattern | @ruby_find_pattern | @ruby_for | @ruby_hash | @ruby_hash_pattern | @ruby_hash_splat_argument | @ruby_hash_splat_parameter | @ruby_heredoc_body | @ruby_if | @ruby_if_guard | @ruby_if_modifier | @ruby_in | @ruby_in_clause | @ruby_interpolation | @ruby_keyword_parameter | @ruby_keyword_pattern | @ruby_lambda | @ruby_lambda_parameters | @ruby_left_assignment_list | @ruby_method | @ruby_method_parameters | @ruby_module | @ruby_next | @ruby_operator_assignment | @ruby_optional_parameter | @ruby_pair | @ruby_parenthesized_pattern | @ruby_parenthesized_statements | @ruby_pattern | @ruby_program | @ruby_range | @ruby_rational | @ruby_redo | @ruby_regex | @ruby_rescue | @ruby_rescue_modifier | @ruby_rest_assignment | @ruby_retry | @ruby_return | @ruby_right_assignment_list | @ruby_scope_resolution | @ruby_setter | @ruby_singleton_class | @ruby_singleton_method | @ruby_splat_argument | @ruby_splat_parameter | @ruby_string__ | @ruby_string_array | @ruby_subshell | @ruby_superclass | @ruby_symbol_array | @ruby_then | @ruby_token | @ruby_unary | @ruby_undef | @ruby_unless | @ruby_unless_guard | @ruby_unless_modifier | @ruby_until | @ruby_until_modifier | @ruby_variable_reference_pattern | @ruby_when | @ruby_while | @ruby_while_modifier | @ruby_yield
@ruby_ast_node_parent = @file | @ruby_ast_node

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,68 @@
class AstNode extends @ruby_ast_node {
string toString() { none() }
}
class Location extends @location {
string toString() { none() }
}
class File extends @file {
string toString() { none() }
}
private predicate body_statement(AstNode body, int index, Location loc) {
exists(AstNode node, AstNode child | ruby_ast_node_info(child, _, _, loc) |
ruby_class_def(node, _) and
ruby_ast_node_info(body, node, _, _) and
ruby_tokeninfo(body, _, "end") and
ruby_class_child(node, index, child)
or
ruby_do_block_def(node) and
ruby_ast_node_info(body, node, _, _) and
ruby_tokeninfo(body, _, "end") and
ruby_do_block_child(node, index, child)
or
ruby_method_def(node, _) and
ruby_ast_node_info(body, node, _, _) and
ruby_tokeninfo(body, _, "end") and
ruby_method_child(node, index, child)
or
ruby_module_def(node, _) and
ruby_ast_node_info(body, node, _, _) and
ruby_tokeninfo(body, _, "end") and
ruby_module_child(node, index, child)
or
ruby_singleton_class_def(node, _) and
ruby_ast_node_info(body, node, _, _) and
ruby_tokeninfo(body, _, "end") and
ruby_singleton_class_child(node, index, child)
or
ruby_singleton_method_def(node, _, _) and
ruby_ast_node_info(body, node, _, _) and
ruby_tokeninfo(body, _, "end") and
ruby_singleton_method_child(node, index, child)
or
ruby_block_def(node) and
ruby_ast_node_info(body, node, _, _) and
ruby_tokeninfo(body, _, "}") and
ruby_block_child(node, index, child)
)
}
from Location loc, File file, int start_line, int start_column, int end_line, int end_column
where
locations_default(loc, file, start_line, start_column, end_line, end_column) and
not exists(AstNode node | ruby_ast_node_info(node, _, _, loc) and body_statement(node, _, _))
or
exists(AstNode node |
ruby_ast_node_info(node, _, _, loc) and
exists(Location first |
body_statement(node, 0, first) and
locations_default(first, pragma[only_bind_into](file), start_line, start_column, _, _)
) and
exists(Location last |
last = max(Location l, int i | body_statement(node, i, l) | l order by i) and
locations_default(last, pragma[only_bind_into](file), _, _, end_line, end_column)
)
)
select loc, file, start_line, start_column, end_line, end_column

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,76 @@
class AstNode extends @ruby_ast_node_parent {
string toString() { none() }
}
class Location extends @location {
string toString() { none() }
}
private predicate body_statement(AstNode body, AstNode firstChild) {
exists(AstNode node |
ruby_class_def(node, _) and
ruby_ast_node_info(body, node, _, _) and
ruby_tokeninfo(body, _, "end") and
ruby_class_child(node, 0, firstChild)
or
ruby_do_block_def(node) and
ruby_ast_node_info(body, node, _, _) and
ruby_tokeninfo(body, _, "end") and
ruby_do_block_child(node, 0, firstChild)
or
ruby_method_def(node, _) and
ruby_ast_node_info(body, node, _, _) and
ruby_tokeninfo(body, _, "end") and
ruby_method_child(node, 0, firstChild)
or
ruby_module_def(node, _) and
ruby_ast_node_info(body, node, _, _) and
ruby_tokeninfo(body, _, "end") and
ruby_module_child(node, 0, firstChild)
or
ruby_singleton_class_def(node, _) and
ruby_ast_node_info(body, node, _, _) and
ruby_tokeninfo(body, _, "end") and
ruby_singleton_class_child(node, 0, firstChild)
or
ruby_singleton_method_def(node, _, _) and
ruby_ast_node_info(body, node, _, _) and
ruby_tokeninfo(body, _, "end") and
ruby_singleton_method_child(node, 0, firstChild)
or
ruby_block_def(node) and
ruby_ast_node_info(body, node, _, _) and
ruby_tokeninfo(body, _, "}") and
ruby_block_child(node, 0, firstChild)
)
}
private predicate body_statement_child(AstNode body, int index, AstNode child) {
exists(AstNode parent, AstNode firstChild, int firstChildIndex |
body_statement(body, firstChild) and
ruby_ast_node_info(firstChild, parent, firstChildIndex, _) and
child =
rank[index + 1](AstNode c, int i |
ruby_ast_node_info(c, parent, i, _) and i >= firstChildIndex and c != body
|
c order by i
)
)
}
private predicate astNodeInfo(AstNode node, AstNode parent, int parent_index, Location loc) {
ruby_ast_node_info(node, parent, parent_index, loc) and
not body_statement(node, _) and
not body_statement_child(_, _, node)
}
from AstNode node, AstNode parent, int parent_index, Location loc
where
astNodeInfo(node, parent, parent_index, loc)
or
body_statement_child(parent, parent_index, node) and ruby_ast_node_info(node, _, _, loc)
or
body_statement(node, _) and
ruby_ast_node_info(node, parent, _, loc) and
parent_index = count(AstNode n | astNodeInfo(n, parent, _, _))
select node, parent, parent_index, loc

View File

@@ -0,0 +1,11 @@
class AstNode extends @ruby_ast_node {
string toString() { none() }
}
from AstNode ruby_block, AstNode body
where
ruby_block_def(ruby_block) and
ruby_ast_node_info(body, ruby_block, _, _) and
ruby_tokeninfo(body, _, "}") and
ruby_block_child(ruby_block, _, _)
select ruby_block, body

View File

@@ -0,0 +1,11 @@
class AstNode extends @ruby_ast_node {
string toString() { none() }
}
from AstNode ruby_block, AstNode body, int index, AstNode child
where
ruby_block_def(ruby_block) and
ruby_ast_node_info(body, ruby_block, _, _) and
ruby_tokeninfo(body, _, "}") and
ruby_block_child(ruby_block, index, child)
select body, index, child

View File

@@ -0,0 +1,18 @@
class AstNode extends @ruby_ast_node {
string toString() { none() }
}
/*
* It's not possible to generate fresh IDs for the new ruby_block_body nodes,
* therefore we re-purpose the "}"-token that closes the block and use its ID instead.
* As a result the AST will be missing the "}" tokens, but those are unlikely to be used
* for anything.
*/
from AstNode ruby_block, AstNode body
where
ruby_block_def(ruby_block) and
ruby_ast_node_info(body, ruby_block, _, _) and
ruby_tokeninfo(body, _, "}") and
ruby_block_child(ruby_block, _, _)
select body

View File

@@ -0,0 +1,36 @@
class AstNode extends @ruby_ast_node {
string toString() { none() }
}
from AstNode node, AstNode body, int index, AstNode child
where
ruby_class_def(node, _) and
ruby_ast_node_info(body, node, _, _) and
ruby_tokeninfo(body, _, "end") and
ruby_class_child(node, index, child)
or
ruby_do_block_def(node) and
ruby_ast_node_info(body, node, _, _) and
ruby_tokeninfo(body, _, "end") and
ruby_do_block_child(node, index, child)
or
ruby_method_def(node, _) and
ruby_ast_node_info(body, node, _, _) and
ruby_tokeninfo(body, _, "end") and
ruby_method_child(node, index, child)
or
ruby_module_def(node, _) and
ruby_ast_node_info(body, node, _, _) and
ruby_tokeninfo(body, _, "end") and
ruby_module_child(node, index, child)
or
ruby_singleton_class_def(node, _) and
ruby_ast_node_info(body, node, _, _) and
ruby_tokeninfo(body, _, "end") and
ruby_singleton_class_child(node, index, child)
or
ruby_singleton_method_def(node, _, _) and
ruby_ast_node_info(body, node, _, _) and
ruby_tokeninfo(body, _, "end") and
ruby_singleton_method_child(node, index, child)
select body, index, child

View File

@@ -0,0 +1,43 @@
class AstNode extends @ruby_ast_node {
string toString() { none() }
}
/*
* It's not possible to generate fresh IDs for the new ruby_body_statement nodes,
* therefore we re-purpose the "end"-token that closes the block and use its ID instead.
* As a result the AST will be missing the "end" tokens, but those are unlikely to be used
* for anything.
*/
from AstNode node, AstNode body
where
ruby_class_def(node, _) and
ruby_ast_node_info(body, node, _, _) and
ruby_tokeninfo(body, _, "end") and
ruby_class_child(node, _, _)
or
ruby_do_block_def(node) and
ruby_ast_node_info(body, node, _, _) and
ruby_tokeninfo(body, _, "end") and
ruby_do_block_child(node, _, _)
or
ruby_method_def(node, _) and
ruby_ast_node_info(body, node, _, _) and
ruby_tokeninfo(body, _, "end") and
ruby_method_child(node, _, _)
or
ruby_module_def(node, _) and
ruby_ast_node_info(body, node, _, _) and
ruby_tokeninfo(body, _, "end") and
ruby_module_child(node, _, _)
or
ruby_singleton_class_def(node, _) and
ruby_ast_node_info(body, node, _, _) and
ruby_tokeninfo(body, _, "end") and
ruby_singleton_class_child(node, _, _)
or
ruby_singleton_method_def(node, _, _) and
ruby_ast_node_info(body, node, _, _) and
ruby_tokeninfo(body, _, "end") and
ruby_singleton_method_child(node, _, _)
select body

View File

@@ -0,0 +1,11 @@
class AstNode extends @ruby_ast_node {
string toString() { none() }
}
from AstNode node, AstNode body
where
ruby_class_def(node, _) and
ruby_ast_node_info(body, node, _, _) and
ruby_tokeninfo(body, _, "end") and
ruby_class_child(node, _, _)
select node, body

View File

@@ -0,0 +1,11 @@
class AstNode extends @ruby_ast_node {
string toString() { none() }
}
from AstNode node, AstNode body
where
ruby_do_block_def(node) and
ruby_ast_node_info(body, node, _, _) and
ruby_tokeninfo(body, _, "end") and
ruby_do_block_child(node, _, _)
select node, body

View File

@@ -0,0 +1,20 @@
class AstNode extends @ruby_ast_node {
string toString() { none() }
}
from AstNode node, AstNode body
where
ruby_method_def(node, _) and
(
ruby_ast_node_info(body, node, _, _) and
ruby_tokeninfo(body, _, "end") and
ruby_method_child(node, _, _)
or
ruby_method_child(node, 0, body) and
not exists(AstNode n |
ruby_ast_node_info(n, node, _, _) and
ruby_tokeninfo(n, _, "end")
)
)
// TODO : handle end-less methods
select node, body

View File

@@ -0,0 +1,11 @@
class AstNode extends @ruby_ast_node {
string toString() { none() }
}
from AstNode node, AstNode body
where
ruby_module_def(node, _) and
ruby_ast_node_info(body, node, _, _) and
ruby_tokeninfo(body, _, "end") and
ruby_module_child(node, _, _)
select node, body

View File

@@ -0,0 +1,11 @@
class AstNode extends @ruby_ast_node {
string toString() { none() }
}
from AstNode node, AstNode body
where
ruby_singleton_class_def(node, _) and
ruby_ast_node_info(body, node, _, _) and
ruby_tokeninfo(body, _, "end") and
ruby_singleton_class_child(node, _, _)
select node, body

View File

@@ -0,0 +1,17 @@
class AstNode extends @ruby_ast_node {
string toString() { none() }
}
from AstNode node, AstNode body
where
ruby_singleton_method_def(node, _, _) and
ruby_ast_node_info(body, node, _, _) and
ruby_tokeninfo(body, _, "end") and
ruby_singleton_method_child(node, _, _)
or
ruby_singleton_method_child(node, 0, body) and
not exists(AstNode n |
ruby_ast_node_info(n, node, _, _) and
ruby_tokeninfo(n, _, "end")
)
select node, body

View File

@@ -0,0 +1,46 @@
class AstNode extends @ruby_ast_node {
string toString() { none() }
}
private predicate body_statement(AstNode body) {
exists(AstNode node |
ruby_class_def(node, _) and
ruby_ast_node_info(body, node, _, _) and
ruby_tokeninfo(body, _, "end") and
ruby_class_child(node, _, _)
or
ruby_do_block_def(node) and
ruby_ast_node_info(body, node, _, _) and
ruby_tokeninfo(body, _, "end") and
ruby_do_block_child(node, _, _)
or
ruby_method_def(node, _) and
ruby_ast_node_info(body, node, _, _) and
ruby_tokeninfo(body, _, "end") and
ruby_method_child(node, _, _)
or
ruby_module_def(node, _) and
ruby_ast_node_info(body, node, _, _) and
ruby_tokeninfo(body, _, "end") and
ruby_module_child(node, _, _)
or
ruby_singleton_class_def(node, _) and
ruby_ast_node_info(body, node, _, _) and
ruby_tokeninfo(body, _, "end") and
ruby_singleton_class_child(node, _, _)
or
ruby_singleton_method_def(node, _, _) and
ruby_ast_node_info(body, node, _, _) and
ruby_tokeninfo(body, _, "end") and
ruby_singleton_method_child(node, _, _)
or
ruby_block_def(node) and
ruby_ast_node_info(body, node, _, _) and
ruby_tokeninfo(body, _, "}") and
ruby_block_child(node, _, _)
)
}
from AstNode token, int kind, string value
where ruby_tokeninfo(token, kind, value) and not body_statement(token)
select token, kind, value

View File

@@ -0,0 +1,33 @@
description: Wrap class, module, method, and block bodies in a named node
compatibility: partial
ruby_block_body.rel: run ruby_block_body.qlo
ruby_block_body_def.rel: run ruby_block_body_def.qlo
ruby_block_body_child.rel: run ruby_block_body_child.qlo
ruby_block_child.rel: delete
ruby_body_statement_child.rel: run ruby_body_statement_child.qlo
ruby_body_statement_def.rel: run ruby_body_statement_def.qlo
ruby_class_body.rel: run ruby_class_body.qlo
ruby_class_child.rel: delete
ruby_do_block_body.rel: run ruby_do_block_body.qlo
ruby_do_block_child.rel: delete
ruby_method_body.rel: run ruby_method_body.qlo
ruby_method_child.rel: delete
ruby_module_body.rel: run ruby_module_body.qlo
ruby_module_child.rel: delete
ruby_singleton_class_body.rel: run ruby_singleton_class_body.qlo
ruby_singleton_class_child.rel: delete
ruby_singleton_method_body.rel: run ruby_singleton_method_body.qlo
ruby_singleton_method_child.rel: delete
ruby_ast_node_info.rel: run ruby_ast_node_info.qlo
ruby_tokeninfo.rel: run ruby_tokeninfo.qlo
locations_default.rel: run locations_default.qlo

View File

@@ -0,0 +1,4 @@
---
category: newQuery
---
* Added a new query, `rb/hardcoded-data-interpreted-as-code`, to detect cases where hardcoded data is executed as code, a technique associated with backdoors.

View File

@@ -0,0 +1,4 @@
---
category: minorAnalysis
---
* The alert message of many queries have been changed to make the message consistent with other languages.

View File

@@ -22,5 +22,5 @@ import DataFlow::PathGraph
from Configuration cfg, DataFlow::PathNode source, DataFlow::PathNode sink
where cfg.hasFlowPath(source, sink)
select sink.getNode(), source, sink, "This path depends on $@.", source.getNode(),
"a user-provided value"
select sink.getNode(), source, sink, "$@ flows to here and is used in a path.", source.getNode(),
"User-provided value"

View File

@@ -20,4 +20,4 @@ import DataFlow::PathGraph
from ReflectedXss::Configuration config, DataFlow::PathNode source, DataFlow::PathNode sink
where config.hasFlowPath(source, sink)
select sink.getNode(), source, sink, "Cross-site scripting vulnerability due to $@.",
source.getNode(), "a user-provided value"
source.getNode(), "user-provided value"

View File

@@ -18,5 +18,5 @@ import DataFlow::PathGraph
from StoredXss::Configuration config, DataFlow::PathNode source, DataFlow::PathNode sink
where config.hasFlowPath(source, sink)
select sink.getNode(), source, sink, "Cross-site scripting vulnerability due to $@",
select sink.getNode(), source, sink, "Stored cross-site scripting vulnerability due to $@.",
source.getNode(), "stored value"

View File

@@ -22,5 +22,5 @@ from Configuration config, DataFlow::PathNode source, DataFlow::PathNode sink, S
where
config.hasFlowPath(source, sink) and
sourceNode = source.getNode()
select sink.getNode(), source, sink, "This code execution depends on $@.", sourceNode,
"a user-provided value"
select sink.getNode(), source, sink, "$@ flows to here and is interpreted as code.",
source.getNode(), "User-provided value"

View File

@@ -15,6 +15,20 @@ import ruby
import codeql.ruby.Concepts
import codeql.ruby.DataFlow
from HTTP::Client::Request request, DataFlow::Node disablingNode
where request.disablesCertificateValidation(disablingNode)
select request, "This request may run with $@.", disablingNode, "certificate validation disabled"
from
Http::Client::Request request, DataFlow::Node disablingNode, DataFlow::Node origin, string ending
where
request.disablesCertificateValidation(disablingNode, origin) and
// Showing the origin is only useful when it's a different node than the one disabling
// certificate validation, for example in `requests.get(..., verify=arg)`, `arg` would
// be the `disablingNode`, and the `origin` would be the place were `arg` got its
// value from.
//
// NOTE: We compare the locations instead of DataFlow::Nodes directly, since for
// snippet `Excon.defaults[:ssl_verify_peer] = false`, `disablingNode = argumentNode`
// does NOT hold.
if disablingNode.getLocation() = origin.getLocation()
then ending = "."
else ending = " by the value from $@."
select request, "This request may run without certificate validation because it is $@" + ending,
disablingNode, "disabled here", origin, "here"

Some files were not shown because too many files have changed in this diff Show More