Merge pull request #13683 from asgerf/rb/api-graph-noobject

Ruby: exclude Object class from API graph
This commit is contained in:
Asger F
2023-07-10 12:51:15 +02:00
committed by GitHub
43 changed files with 2449 additions and 1305 deletions

View File

@@ -55,10 +55,9 @@ private module Cached {
) )
} }
pragma[nomagic] /** Gets a type tracker with no content and the call bit set to the given value. */
private TypeTracker noContentTypeTracker(boolean hasCall) { cached
result = MkTypeTracker(hasCall, noContent()) TypeTracker noContentTypeTracker(boolean hasCall) { result = MkTypeTracker(hasCall, noContent()) }
}
/** Gets the summary resulting from appending `step` to type-tracking summary `tt`. */ /** Gets the summary resulting from appending `step` to type-tracking summary `tt`. */
cached cached
@@ -318,6 +317,8 @@ class StepSummary extends TStepSummary {
/** Provides predicates for updating step summaries (`StepSummary`s). */ /** Provides predicates for updating step summaries (`StepSummary`s). */
module StepSummary { module StepSummary {
predicate append = Cached::append/2;
/** /**
* Gets the summary that corresponds to having taken a forwards * Gets the summary that corresponds to having taken a forwards
* inter-procedural step from `nodeFrom` to `nodeTo`. * inter-procedural step from `nodeFrom` to `nodeTo`.
@@ -378,6 +379,35 @@ module StepSummary {
} }
deprecated predicate localSourceStoreStep = flowsToStoreStep/3; deprecated predicate localSourceStoreStep = flowsToStoreStep/3;
/** Gets the step summary for a level step. */
StepSummary levelStep() { result = LevelStep() }
/** Gets the step summary for a call step. */
StepSummary callStep() { result = CallStep() }
/** Gets the step summary for a return step. */
StepSummary returnStep() { result = ReturnStep() }
/** Gets the step summary for storing into `content`. */
StepSummary storeStep(TypeTrackerContent content) { result = StoreStep(content) }
/** Gets the step summary for loading from `content`. */
StepSummary loadStep(TypeTrackerContent content) { result = LoadStep(content) }
/** Gets the step summary for loading from `load` and then storing into `store`. */
StepSummary loadStoreStep(TypeTrackerContent load, TypeTrackerContent store) {
result = LoadStoreStep(load, store)
}
/** Gets the step summary for a step that only permits contents matched by `filter`. */
StepSummary withContent(ContentFilter filter) { result = WithContent(filter) }
/** Gets the step summary for a step that blocks contents matched by `filter`. */
StepSummary withoutContent(ContentFilter filter) { result = WithoutContent(filter) }
/** Gets the step summary for a jump step. */
StepSummary jumpStep() { result = JumpStep() }
} }
/** /**
@@ -540,6 +570,13 @@ module TypeTracker {
* Gets a valid end point of type tracking. * Gets a valid end point of type tracking.
*/ */
TypeTracker end() { result.end() } TypeTracker end() { result.end() }
/**
* INTERNAL USE ONLY.
*
* Gets a valid end point of type tracking with the call bit set to the given value.
*/
predicate end = Cached::noContentTypeTracker/1;
} }
pragma[nomagic] pragma[nomagic]

View File

@@ -0,0 +1,7 @@
---
category: majorAnalysis
---
* The API graph library (`codeql.ruby.ApiGraphs`) has been significantly improved, with better support for inheritance,
and data-flow nodes can now be converted to API nodes by calling `.track()` or `.backtrack()` on the node.
API graphs allow for efficient modelling of how a given value is used by the code base, or how values produced by the code base
are consumed by a library. See the documentation for `API::Node` for details and examples.

File diff suppressed because it is too large Load Diff

View File

@@ -6,12 +6,17 @@ private import codeql.ruby.typetracking.TypeTracker
private import codeql.ruby.dataflow.SSA private import codeql.ruby.dataflow.SSA
private import FlowSummaryImpl as FlowSummaryImpl private import FlowSummaryImpl as FlowSummaryImpl
private import SsaImpl as SsaImpl private import SsaImpl as SsaImpl
private import codeql.ruby.ApiGraphs
/** /**
* An element, viewed as a node in a data flow graph. Either an expression * An element, viewed as a node in a data flow graph. Either an expression
* (`ExprNode`) or a parameter (`ParameterNode`). * (`ExprNode`) or a parameter (`ParameterNode`).
*/ */
class Node extends TNode { class Node extends TNode {
/** Starts backtracking from this node using API graphs. */
pragma[inline]
API::Node backtrack() { result = API::Internal::getNodeForBacktracking(this) }
/** Gets the expression corresponding to this node, if any. */ /** Gets the expression corresponding to this node, if any. */
CfgNodes::ExprCfgNode asExpr() { result = this.(ExprNode).getExprNode() } CfgNodes::ExprCfgNode asExpr() { result = this.(ExprNode).getExprNode() }
@@ -76,6 +81,11 @@ class Node extends TNode {
result.asCallableAstNode() = c.asCallable() result.asCallableAstNode() = c.asCallable()
) )
} }
/** Gets the enclosing method, if any. */
MethodNode getEnclosingMethod() {
result.asCallableAstNode() = this.asExpr().getExpr().getEnclosingMethod()
}
} }
/** A data-flow node corresponding to a call in the control-flow graph. */ /** A data-flow node corresponding to a call in the control-flow graph. */
@@ -144,6 +154,18 @@ class CallNode extends LocalSourceNode, ExprNode {
result.asExpr() = pair.getValue() result.asExpr() = pair.getValue()
) )
} }
/**
* Gets a potential target of this call, if any.
*/
final CallableNode getATarget() {
result.asCallableAstNode() = this.asExpr().getExpr().(Call).getATarget()
}
/**
* Holds if this is a `super` call.
*/
final predicate isSuperCall() { this.asExpr().getExpr() instanceof SuperCall }
} }
/** /**
@@ -217,6 +239,10 @@ class SelfParameterNode extends ParameterNode instanceof SelfParameterNodeImpl {
class LocalSourceNode extends Node { class LocalSourceNode extends Node {
LocalSourceNode() { isLocalSourceNode(this) } LocalSourceNode() { isLocalSourceNode(this) }
/** Starts tracking this node forward using API graphs. */
pragma[inline]
API::Node track() { result = API::Internal::getNodeForForwardTracking(this) }
/** Holds if this `LocalSourceNode` can flow to `nodeTo` in one or more local flow steps. */ /** Holds if this `LocalSourceNode` can flow to `nodeTo` in one or more local flow steps. */
pragma[inline] pragma[inline]
predicate flowsTo(Node nodeTo) { hasLocalSource(nodeTo, this) } predicate flowsTo(Node nodeTo) { hasLocalSource(nodeTo, this) }
@@ -359,6 +385,11 @@ private module Cached {
) )
} }
cached
predicate methodHasSuperCall(MethodNode method, CallNode call) {
call.isSuperCall() and method = call.getEnclosingMethod()
}
/** /**
* A place in which a named constant can be looked up during constant lookup. * A place in which a named constant can be looked up during constant lookup.
*/ */
@@ -387,6 +418,39 @@ private module Cached {
result.asExpr().getExpr() = access result.asExpr().getExpr() = access
} }
/**
* Gets a module for which `constRef` is the reference to an ancestor module.
*
* For example, `M` is the ancestry target of `C` in the following examples:
* ```rb
* class M < C {}
*
* module M
* include C
* end
*
* module M
* prepend C
* end
* ```
*/
private ModuleNode getAncestryTarget(ConstRef constRef) { result.getAnAncestorExpr() = constRef }
/**
* Gets a scope in which a constant lookup may access the contents of the module referenced by `constRef`.
*/
cached
TConstLookupScope getATargetScope(ConstRef constRef) {
result = MkAncestorLookup(getAncestryTarget(constRef).getAnImmediateDescendent*())
or
constRef.asConstantAccess() = any(ConstantAccess ac).getScopeExpr() and
result = MkQualifiedLookup(constRef.asConstantAccess())
or
result = MkNestedLookup(getAncestryTarget(constRef))
or
result = MkExactLookup(constRef.asConstantAccess().(Namespace).getModule())
}
cached cached
predicate forceCachingInSameStage() { any() } predicate forceCachingInSameStage() { any() }
@@ -1028,6 +1092,33 @@ class ModuleNode instanceof Module {
* this predicate. * this predicate.
*/ */
ModuleNode getNestedModule(string name) { result = super.getNestedModule(name) } ModuleNode getNestedModule(string name) { result = super.getNestedModule(name) }
/**
* Starts tracking the module object using API graphs.
*
* Concretely, this tracks forward from the following starting points:
* - A constant access that resolves to this module.
* - `self` in the module scope or in a singleton method of the module.
* - A call to `self.class` in an instance method of this module or an ancestor module.
*/
bindingset[this]
pragma[inline]
API::Node trackModule() { result = API::Internal::getModuleNode(this) }
/**
* Starts tracking instances of this module forward using API graphs.
*
* Concretely, this tracks forward from the following starting points:
* - `self` in instance methods of this module and ancestor modules
* - Calls to `new` on the module object
*
* Note that this includes references to `self` in ancestor modules, but not in descendent modules.
* This is usually the desired behavior, particularly if this module was itself found using
* a call to `getADescendentModule()`.
*/
bindingset[this]
pragma[inline]
API::Node trackInstance() { result = API::Internal::getModuleInstance(this) }
} }
/** /**
@@ -1216,6 +1307,9 @@ class MethodNode extends CallableNode {
/** Holds if this method is protected. */ /** Holds if this method is protected. */
predicate isProtected() { this.asCallableAstNode().isProtected() } predicate isProtected() { this.asCallableAstNode().isProtected() }
/** Gets a `super` call in this method. */
CallNode getASuperCall() { methodHasSuperCall(this, result) }
} }
/** /**
@@ -1334,24 +1428,6 @@ class ConstRef extends LocalSourceNode {
not exists(access.getScopeExpr()) not exists(access.getScopeExpr())
} }
/**
* Gets a module for which this constant is the reference to an ancestor module.
*
* For example, `M` is the ancestry target of `C` in the following examples:
* ```rb
* class M < C {}
*
* module M
* include C
* end
*
* module M
* prepend C
* end
* ```
*/
private ModuleNode getAncestryTarget() { result.getAnAncestorExpr() = this }
/** /**
* Gets the known target module. * Gets the known target module.
* *
@@ -1359,22 +1435,6 @@ class ConstRef extends LocalSourceNode {
*/ */
private Module getExactTarget() { result.getAnImmediateReference() = access } private Module getExactTarget() { result.getAnImmediateReference() = access }
/**
* Gets a scope in which a constant lookup may access the contents of the module referenced by this constant.
*/
cached
private TConstLookupScope getATargetScope() {
forceCachingInSameStage() and
result = MkAncestorLookup(this.getAncestryTarget().getAnImmediateDescendent*())
or
access = any(ConstantAccess ac).getScopeExpr() and
result = MkQualifiedLookup(access)
or
result = MkNestedLookup(this.getAncestryTarget())
or
result = MkExactLookup(access.(Namespace).getModule())
}
/** /**
* Gets the scope expression, or the immediately enclosing `Namespace` (skipping over singleton classes). * Gets the scope expression, or the immediately enclosing `Namespace` (skipping over singleton classes).
* *
@@ -1436,7 +1496,7 @@ class ConstRef extends LocalSourceNode {
pragma[inline] pragma[inline]
ConstRef getConstant(string name) { ConstRef getConstant(string name) {
exists(TConstLookupScope scope | exists(TConstLookupScope scope |
pragma[only_bind_into](scope) = pragma[only_bind_out](this).getATargetScope() and pragma[only_bind_into](scope) = getATargetScope(pragma[only_bind_out](this)) and
result.accesses(pragma[only_bind_out](scope), name) result.accesses(pragma[only_bind_out](scope), name)
) )
} }
@@ -1458,7 +1518,9 @@ class ConstRef extends LocalSourceNode {
* end * end
* ``` * ```
*/ */
ModuleNode getADescendentModule() { MkAncestorLookup(result) = this.getATargetScope() } bindingset[this]
pragma[inline_late]
ModuleNode getADescendentModule() { MkAncestorLookup(result) = getATargetScope(this) }
} }
/** /**

View File

@@ -91,19 +91,19 @@ class Configuration extends TaintTracking::Configuration {
// unicode_utils // unicode_utils
exists(API::MethodAccessNode mac | exists(API::MethodAccessNode mac |
mac = API::getTopLevelMember("UnicodeUtils").getMethod(["nfkd", "nfc", "nfd", "nfkc"]) and mac = API::getTopLevelMember("UnicodeUtils").getMethod(["nfkd", "nfc", "nfd", "nfkc"]) and
sink = mac.getParameter(0).asSink() sink = mac.getArgument(0).asSink()
) )
or or
// eprun // eprun
exists(API::MethodAccessNode mac | exists(API::MethodAccessNode mac |
mac = API::getTopLevelMember("Eprun").getMethod("normalize") and mac = API::getTopLevelMember("Eprun").getMethod("normalize") and
sink = mac.getParameter(0).asSink() sink = mac.getArgument(0).asSink()
) )
or or
// unf // unf
exists(API::MethodAccessNode mac | exists(API::MethodAccessNode mac |
mac = API::getTopLevelMember("UNF").getMember("Normalizer").getMethod("normalize") and mac = API::getTopLevelMember("UNF").getMember("Normalizer").getMethod("normalize") and
sink = mac.getParameter(0).asSink() sink = mac.getArgument(0).asSink()
) )
or or
// ActiveSupport::Multibyte::Chars // ActiveSupport::Multibyte::Chars
@@ -113,7 +113,7 @@ class Configuration extends TaintTracking::Configuration {
.getMember("Multibyte") .getMember("Multibyte")
.getMember("Chars") .getMember("Chars")
.getMethod("new") .getMethod("new")
.getCallNode() and .asCall() and
n = cn.getAMethodCall("normalize") and n = cn.getAMethodCall("normalize") and
sink = cn.getArgument(0) sink = cn.getArgument(0)
) )

View File

@@ -89,7 +89,7 @@ module ZipSlip {
// If argument refers to a string object, then it's a hardcoded path and // If argument refers to a string object, then it's a hardcoded path and
// this file is safe. // this file is safe.
not zipOpen not zipOpen
.getCallNode() .asCall()
.getArgument(0) .getArgument(0)
.getALocalSource() .getALocalSource()
.getConstantValue() .getConstantValue()

View File

@@ -83,8 +83,8 @@ class ActionControllerClass extends DataFlow::ClassNode {
} }
} }
private DataFlow::LocalSourceNode actionControllerInstance() { private API::Node actionControllerInstance() {
result = any(ActionControllerClass cls).getSelf() result = any(ActionControllerClass cls).getSelf().track()
} }
/** /**
@@ -222,19 +222,19 @@ private class ActionControllerRenderToCall extends RenderToCallImpl {
} }
} }
pragma[nomagic]
private DataFlow::CallNode renderCall() {
// ActionController#render is an alias for ActionController::Renderer#render
result =
[
any(ActionControllerClass c).trackModule().getAMethodCall("render"),
any(ActionControllerClass c).trackModule().getReturn("renderer").getAMethodCall("render")
]
}
/** A call to `ActionController::Renderer#render`. */ /** A call to `ActionController::Renderer#render`. */
private class RendererRenderCall extends RenderCallImpl { private class RendererRenderCall extends RenderCallImpl {
RendererRenderCall() { RendererRenderCall() { this = renderCall().asExpr().getExpr() }
this =
[
// ActionController#render is an alias for ActionController::Renderer#render
any(ActionControllerClass c).getAnImmediateReference().getAMethodCall("render"),
any(ActionControllerClass c)
.getAnImmediateReference()
.getAMethodCall("renderer")
.getAMethodCall("render")
].asExpr().getExpr()
}
} }
/** A call to `html_escape` from within a controller. */ /** A call to `html_escape` from within a controller. */
@@ -260,6 +260,7 @@ class RedirectToCall extends MethodCall {
this = this =
controller controller
.getSelf() .getSelf()
.track()
.getAMethodCall(["redirect_to", "redirect_back", "redirect_back_or_to"]) .getAMethodCall(["redirect_to", "redirect_back", "redirect_back_or_to"])
.asExpr() .asExpr()
.getExpr() .getExpr()
@@ -600,9 +601,7 @@ private module ParamsSummaries {
* response. * response.
*/ */
private module Response { private module Response {
DataFlow::LocalSourceNode response() { API::Node response() { result = actionControllerInstance().getReturn("response") }
result = actionControllerInstance().getAMethodCall("response")
}
class BodyWrite extends DataFlow::CallNode, Http::Server::HttpResponse::Range { class BodyWrite extends DataFlow::CallNode, Http::Server::HttpResponse::Range {
BodyWrite() { this = response().getAMethodCall("body=") } BodyWrite() { this = response().getAMethodCall("body=") }
@@ -628,7 +627,7 @@ private module Response {
HeaderWrite() { HeaderWrite() {
// response.header[key] = val // response.header[key] = val
// response.headers[key] = val // response.headers[key] = val
this = response().getAMethodCall(["header", "headers"]).getAMethodCall("[]=") this = response().getReturn(["header", "headers"]).getAMethodCall("[]=")
or or
// response.set_header(key) = val // response.set_header(key) = val
// response[header] = val // response[header] = val
@@ -673,18 +672,12 @@ private module Response {
} }
} }
private class ActionControllerLoggerInstance extends DataFlow::Node {
ActionControllerLoggerInstance() {
this = actionControllerInstance().getAMethodCall("logger")
or
any(ActionControllerLoggerInstance i).(DataFlow::LocalSourceNode).flowsTo(this)
}
}
private class ActionControllerLoggingCall extends DataFlow::CallNode, Logging::Range { private class ActionControllerLoggingCall extends DataFlow::CallNode, Logging::Range {
ActionControllerLoggingCall() { ActionControllerLoggingCall() {
this.getReceiver() instanceof ActionControllerLoggerInstance and this =
this.getMethodName() = ["debug", "error", "fatal", "info", "unknown", "warn"] actionControllerInstance()
.getReturn("logger")
.getAMethodCall(["debug", "error", "fatal", "info", "unknown", "warn"])
} }
// Note: this is identical to the definition `stdlib.Logger.LoggerInfoStyleCall`. // Note: this is identical to the definition `stdlib.Logger.LoggerInfoStyleCall`.

View File

@@ -27,8 +27,8 @@ module ActionMailbox {
Mail() { Mail() {
this = this =
[ [
controller().getAnInstanceSelf().getAMethodCall("inbound_email").getAMethodCall("mail"), controller().trackInstance().getReturn("inbound_email").getAMethodCall("mail"),
controller().getAnInstanceSelf().getAMethodCall("mail") controller().trackInstance().getAMethodCall("mail")
] ]
} }
} }
@@ -40,7 +40,7 @@ module ActionMailbox {
RemoteContent() { RemoteContent() {
this = this =
any(Mail m) any(Mail m)
.(DataFlow::LocalSourceNode) .track()
.getAMethodCall([ .getAMethodCall([
"body", "to", "from", "raw_source", "subject", "from_address", "body", "to", "from", "raw_source", "subject", "from_address",
"recipients_addresses", "cc_addresses", "bcc_addresses", "in_reply_to", "recipients_addresses", "cc_addresses", "bcc_addresses", "in_reply_to",

View File

@@ -4,12 +4,26 @@
private import codeql.ruby.AST private import codeql.ruby.AST
private import codeql.ruby.ApiGraphs private import codeql.ruby.ApiGraphs
private import codeql.ruby.DataFlow
private import codeql.ruby.frameworks.internal.Rails private import codeql.ruby.frameworks.internal.Rails
/** /**
* Provides modeling for the `ActionMailer` library. * Provides modeling for the `ActionMailer` library.
*/ */
module ActionMailer { module ActionMailer {
private DataFlow::ClassNode actionMailerClass() {
result =
[
DataFlow::getConstant("ActionMailer").getConstant("Base"),
// In Rails applications `ApplicationMailer` typically extends
// `ActionMailer::Base`, but we treat it separately in case the
// `ApplicationMailer` definition is not in the database.
DataFlow::getConstant("ApplicationMailer")
].getADescendentModule()
}
private API::Node actionMailerInstance() { result = actionMailerClass().trackInstance() }
/** /**
* A `ClassDeclaration` for a class that extends `ActionMailer::Base`. * A `ClassDeclaration` for a class that extends `ActionMailer::Base`.
* For example, * For example,
@@ -21,33 +35,11 @@ module ActionMailer {
* ``` * ```
*/ */
class MailerClass extends ClassDeclaration { class MailerClass extends ClassDeclaration {
MailerClass() { MailerClass() { this = actionMailerClass().getADeclaration() }
this.getSuperclassExpr() =
[
API::getTopLevelMember("ActionMailer").getMember("Base"),
// In Rails applications `ApplicationMailer` typically extends
// `ActionMailer::Base`, but we treat it separately in case the
// `ApplicationMailer` definition is not in the database.
API::getTopLevelMember("ApplicationMailer")
].getASubclass().getAValueReachableFromSource().asExpr().getExpr()
}
}
/** A method call with a `self` receiver from within a mailer class */
private class ContextCall extends MethodCall {
private MailerClass mailerClass;
ContextCall() {
this.getReceiver() instanceof SelfVariableAccess and
this.getEnclosingModule() = mailerClass
}
/** Gets the mailer class containing this method. */
MailerClass getMailerClass() { result = mailerClass }
} }
/** A call to `params` from within a mailer. */ /** A call to `params` from within a mailer. */
class ParamsCall extends ContextCall, ParamsCallImpl { class ParamsCall extends ParamsCallImpl {
ParamsCall() { this.getMethodName() = "params" } ParamsCall() { this = actionMailerInstance().getAMethodCall("params").asExpr().getExpr() }
} }
} }

View File

@@ -30,10 +30,8 @@ private predicate isBuiltInMethodForActiveRecordModelInstance(string methodName)
methodName = objectInstanceMethodName() methodName = objectInstanceMethodName()
} }
private API::Node activeRecordClassApiNode() { private API::Node activeRecordBaseClass() {
result = result =
// class Foo < ActiveRecord::Base
// class Bar < Foo
[ [
API::getTopLevelMember("ActiveRecord").getMember("Base"), API::getTopLevelMember("ActiveRecord").getMember("Base"),
// In Rails applications `ApplicationRecord` typically extends `ActiveRecord::Base`, but we // In Rails applications `ApplicationRecord` typically extends `ActiveRecord::Base`, but we
@@ -42,6 +40,46 @@ private API::Node activeRecordClassApiNode() {
] ]
} }
/**
* Gets an object with methods from the ActiveRecord query interface.
*/
private API::Node activeRecordQueryBuilder() {
result = activeRecordBaseClass()
or
result = activeRecordBaseClass().getInstance()
or
// Assume any method call might return an ActiveRecord::Relation
// These are dynamically generated
result = activeRecordQueryBuilderMethodAccess(_).getReturn()
}
/** Gets a call targeting the ActiveRecord query interface. */
private API::MethodAccessNode activeRecordQueryBuilderMethodAccess(string name) {
result = activeRecordQueryBuilder().getMethod(name) and
// Due to the heuristic tracking of query builder objects, add a restriction for methods with a known call target
not isUnlikelyExternalCall(result)
}
/** Gets a call targeting the ActiveRecord query interface. */
private DataFlow::CallNode activeRecordQueryBuilderCall(string name) {
result = activeRecordQueryBuilderMethodAccess(name).asCall()
}
/**
* Holds if `call` is unlikely to call into an external library, since it has a possible
* call target in its enclosing module.
*/
private predicate isUnlikelyExternalCall(API::MethodAccessNode node) {
exists(DataFlow::ModuleNode mod, DataFlow::CallNode call | call = node.asCall() |
call.getATarget() = [mod.getAnOwnSingletonMethod(), mod.getAnOwnInstanceMethod()] and
call.getEnclosingMethod() = [mod.getAnOwnSingletonMethod(), mod.getAnOwnInstanceMethod()]
)
}
private API::Node activeRecordConnectionInstance() {
result = activeRecordBaseClass().getReturn("connection")
}
/** /**
* A `ClassDeclaration` for a class that inherits from `ActiveRecord::Base`. For example, * A `ClassDeclaration` for a class that inherits from `ActiveRecord::Base`. For example,
* *
@@ -55,20 +93,19 @@ private API::Node activeRecordClassApiNode() {
* ``` * ```
*/ */
class ActiveRecordModelClass extends ClassDeclaration { class ActiveRecordModelClass extends ClassDeclaration {
private DataFlow::ClassNode cls;
ActiveRecordModelClass() { ActiveRecordModelClass() {
this.getSuperclassExpr() = cls = activeRecordBaseClass().getADescendentModule() and this = cls.getADeclaration()
activeRecordClassApiNode().getASubclass().getAValueReachableFromSource().asExpr().getExpr()
} }
// Gets the class declaration for this class and all of its super classes // Gets the class declaration for this class and all of its super classes
private ModuleBase getAllClassDeclarations() { private ModuleBase getAllClassDeclarations() { result = cls.getAnAncestor().getADeclaration() }
result = this.getModule().getSuperClass*().getADeclaration()
}
/** /**
* Gets methods defined in this class that may access a field from the database. * Gets methods defined in this class that may access a field from the database.
*/ */
Method getAPotentialFieldAccessMethod() { deprecated Method getAPotentialFieldAccessMethod() {
// It's a method on this class or one of its super classes // It's a method on this class or one of its super classes
result = this.getAllClassDeclarations().getAMethod() and result = this.getAllClassDeclarations().getAMethod() and
// There is a value that can be returned by this method which may include field data // There is a value that can be returned by this method which may include field data
@@ -90,58 +127,84 @@ class ActiveRecordModelClass extends ClassDeclaration {
) )
) )
} }
/** Gets the class as a `DataFlow::ClassNode`. */
DataFlow::ClassNode getClassNode() { result = cls }
} }
/** A class method call whose receiver is an `ActiveRecordModelClass`. */ /**
class ActiveRecordModelClassMethodCall extends MethodCall { * Gets a potential reference to an ActiveRecord class object.
private ActiveRecordModelClass recvCls; */
deprecated private API::Node getAnActiveRecordModelClassRef() {
result = any(ActiveRecordModelClass cls).getClassNode().trackModule()
or
// For methods with an unknown call target, assume this might be a database field, thus returning another ActiveRecord object.
// In this case we do not know which class it belongs to, which is why this predicate can't associate the reference with a specific class.
result = getAnUnknownActiveRecordModelClassCall().getReturn()
}
/**
* Gets a call performed on an ActiveRecord class object, without a known call target in the codebase.
*/
deprecated private API::MethodAccessNode getAnUnknownActiveRecordModelClassCall() {
result = getAnActiveRecordModelClassRef().getMethod(_) and
result.asCall().asExpr().getExpr() instanceof UnknownMethodCall
}
/**
* DEPRECATED. Use `ActiveRecordModelClass.getClassNode().trackModule().getMethod()` instead.
*
* A class method call whose receiver is an `ActiveRecordModelClass`.
*/
deprecated class ActiveRecordModelClassMethodCall extends MethodCall {
ActiveRecordModelClassMethodCall() { ActiveRecordModelClassMethodCall() {
// e.g. Foo.where(...) this = getAnUnknownActiveRecordModelClassCall().asCall().asExpr().getExpr()
recvCls.getModule() = this.getReceiver().(ConstantReadAccess).getModule()
or
// e.g. Foo.joins(:bars).where(...)
recvCls = this.getReceiver().(ActiveRecordModelClassMethodCall).getReceiverClass()
or
// e.g. self.where(...) within an ActiveRecordModelClass
this.getReceiver() instanceof SelfVariableAccess and
this.getEnclosingModule() = recvCls
} }
/** The `ActiveRecordModelClass` of the receiver of this method. */ /** Gets the `ActiveRecordModelClass` of the receiver of this method, if it can be determined. */
ActiveRecordModelClass getReceiverClass() { result = recvCls } ActiveRecordModelClass getReceiverClass() {
this = result.getClassNode().trackModule().getMethod(_).asCall().asExpr().getExpr()
}
} }
private Expr sqlFragmentArgument(MethodCall call) { private predicate sqlFragmentArgumentInner(DataFlow::CallNode call, DataFlow::Node sink) {
exists(string methodName | call =
methodName = call.getMethodName() and activeRecordQueryBuilderCall([
( "delete_all", "delete_by", "destroy_all", "destroy_by", "exists?", "find_by", "find_by!",
methodName = "find_or_create_by", "find_or_create_by!", "find_or_initialize_by", "find_by_sql", "from",
[ "group", "having", "joins", "lock", "not", "order", "reorder", "pluck", "where", "rewhere",
"delete_all", "delete_by", "destroy_all", "destroy_by", "exists?", "find_by", "find_by!", "select", "reselect", "update_all"
"find_or_create_by", "find_or_create_by!", "find_or_initialize_by", "find_by_sql", "from", ]) and
"group", "having", "joins", "lock", "not", "order", "reorder", "pluck", "where", sink = call.getArgument(0)
"rewhere", "select", "reselect", "update_all" or
] and call = activeRecordQueryBuilderCall("calculate") and
result = call.getArgument(0) sink = call.getArgument(1)
or or
methodName = "calculate" and result = call.getArgument(1) call =
or activeRecordQueryBuilderCall(["average", "count", "maximum", "minimum", "sum", "count_by_sql"]) and
methodName in ["average", "count", "maximum", "minimum", "sum", "count_by_sql"] and sink = call.getArgument(0)
result = call.getArgument(0) or
or // This format was supported until Rails 2.3.8
// This format was supported until Rails 2.3.8 call = activeRecordQueryBuilderCall(["all", "find", "first", "last"]) and
methodName = ["all", "find", "first", "last"] and sink = call.getKeywordArgument("conditions")
result = call.getKeywordArgument("conditions") or
or call = activeRecordQueryBuilderCall("reload") and
methodName = "reload" and sink = call.getKeywordArgument("lock")
result = call.getKeywordArgument("lock") or
or // Calls to `annotate` can be used to add block comments to SQL queries. These are potentially vulnerable to
// Calls to `annotate` can be used to add block comments to SQL queries. These are potentially vulnerable to // SQLi if user supplied input is passed in as an argument.
// SQLi if user supplied input is passed in as an argument. call = activeRecordQueryBuilderCall("annotate") and
methodName = "annotate" and sink = call.getArgument(_)
result = call.getArgument(_) or
) call = activeRecordConnectionInstance().getAMethodCall("execute") and
sink = call.getArgument(0)
}
private predicate sqlFragmentArgument(DataFlow::CallNode call, DataFlow::Node sink) {
exists(DataFlow::Node arg |
sqlFragmentArgumentInner(call, arg) and
sink = [arg, arg.(DataFlow::ArrayLiteralNode).getElement(0)] and
unsafeSqlExpr(sink.asExpr().getExpr())
) )
} }
@@ -162,6 +225,8 @@ private predicate unsafeSqlExpr(Expr sqlFragmentExpr) {
} }
/** /**
* DEPRECATED. Use the `SqlExecution` concept or `ActiveRecordSqlExecutionRange`.
*
* A method call that may result in executing unintended user-controlled SQL * A method call that may result in executing unintended user-controlled SQL
* queries if the `getSqlFragmentSinkArgument()` expression is tainted by * queries if the `getSqlFragmentSinkArgument()` expression is tainted by
* unsanitized user-controlled input. For example, supposing that `User` is an * unsanitized user-controlled input. For example, supposing that `User` is an
@@ -175,55 +240,32 @@ private predicate unsafeSqlExpr(Expr sqlFragmentExpr) {
* as `"') OR 1=1 --"` could result in the application looking up all users * as `"') OR 1=1 --"` could result in the application looking up all users
* rather than just one with a matching name. * rather than just one with a matching name.
*/ */
class PotentiallyUnsafeSqlExecutingMethodCall extends ActiveRecordModelClassMethodCall { deprecated class PotentiallyUnsafeSqlExecutingMethodCall extends ActiveRecordModelClassMethodCall {
// The SQL fragment argument itself private DataFlow::CallNode call;
private Expr sqlFragmentExpr;
PotentiallyUnsafeSqlExecutingMethodCall() { PotentiallyUnsafeSqlExecutingMethodCall() {
exists(Expr arg | call.asExpr().getExpr() = this and sqlFragmentArgument(call, _)
arg = sqlFragmentArgument(this) and
unsafeSqlExpr(sqlFragmentExpr) and
(
sqlFragmentExpr = arg
or
sqlFragmentExpr = arg.(ArrayLiteral).getElement(0)
) and
// Check that method has not been overridden
not exists(SingletonMethod m |
m.getName() = this.getMethodName() and
m.getOuterScope() = this.getReceiverClass()
)
)
} }
/** /**
* Gets the SQL fragment argument of this method call. * Gets the SQL fragment argument of this method call.
*/ */
Expr getSqlFragmentSinkArgument() { result = sqlFragmentExpr } Expr getSqlFragmentSinkArgument() {
exists(DataFlow::Node sink |
sqlFragmentArgument(call, sink) and result = sink.asExpr().getExpr()
)
}
} }
/** /**
* An `SqlExecution::Range` for an argument to a * A SQL execution arising from a call to the ActiveRecord library.
* `PotentiallyUnsafeSqlExecutingMethodCall` that may be vulnerable to being
* controlled by user input.
*/ */
class ActiveRecordSqlExecutionRange extends SqlExecution::Range { class ActiveRecordSqlExecutionRange extends SqlExecution::Range {
ActiveRecordSqlExecutionRange() { ActiveRecordSqlExecutionRange() { sqlFragmentArgument(_, this) }
exists(PotentiallyUnsafeSqlExecutingMethodCall mc |
this.asExpr().getNode() = mc.getSqlFragmentSinkArgument()
)
or
this = activeRecordConnectionInstance().getAMethodCall("execute").getArgument(0) and
unsafeSqlExpr(this.asExpr().getExpr())
}
override DataFlow::Node getSql() { result = this } override DataFlow::Node getSql() { result = this }
} }
private API::Node activeRecordConnectionInstance() {
result = activeRecordClassApiNode().getReturn("connection")
}
// TODO: model `ActiveRecord` sanitizers // TODO: model `ActiveRecord` sanitizers
// https://api.rubyonrails.org/classes/ActiveRecord/Sanitization/ClassMethods.html // https://api.rubyonrails.org/classes/ActiveRecord/Sanitization/ClassMethods.html
/** /**
@@ -241,15 +283,8 @@ abstract class ActiveRecordModelInstantiation extends OrmInstantiation::Range,
override predicate methodCallMayAccessField(string methodName) { override predicate methodCallMayAccessField(string methodName) {
// The method is not a built-in, and... // The method is not a built-in, and...
not isBuiltInMethodForActiveRecordModelInstance(methodName) and not isBuiltInMethodForActiveRecordModelInstance(methodName) and
( // ...There is no matching method definition in the class
// ...There is no matching method definition in the class, or... not exists(this.getClass().getMethod(methodName))
not exists(this.getClass().getMethod(methodName))
or
// ...the called method can access a field.
exists(Method m | m = this.getClass().getAPotentialFieldAccessMethod() |
m.getName() = methodName
)
)
} }
} }
@@ -317,21 +352,10 @@ private class ActiveRecordModelFinderCall extends ActiveRecordModelInstantiation
} }
// A `self` reference that may resolve to an active record model object // A `self` reference that may resolve to an active record model object
private class ActiveRecordModelClassSelfReference extends ActiveRecordModelInstantiation, private class ActiveRecordModelClassSelfReference extends ActiveRecordModelInstantiation {
DataFlow::SelfParameterNode
{
private ActiveRecordModelClass cls; private ActiveRecordModelClass cls;
ActiveRecordModelClassSelfReference() { ActiveRecordModelClassSelfReference() { this = cls.getClassNode().getAnOwnInstanceSelf() }
exists(MethodBase m |
m = this.getCallable() and
m.getEnclosingModule() = cls and
m = cls.getAMethod()
) and
// In a singleton method, `self` refers to the class itself rather than an
// instance of that class
not this.getSelfVariable().getDeclaringScope() instanceof SingletonMethod
}
final override ActiveRecordModelClass getClass() { result = cls } final override ActiveRecordModelClass getClass() { result = cls }
} }
@@ -342,7 +366,7 @@ private class ActiveRecordModelClassSelfReference extends ActiveRecordModelInsta
class ActiveRecordInstance extends DataFlow::Node { class ActiveRecordInstance extends DataFlow::Node {
private ActiveRecordModelInstantiation instantiation; private ActiveRecordModelInstantiation instantiation;
ActiveRecordInstance() { this = instantiation or instantiation.flowsTo(this) } ActiveRecordInstance() { this = instantiation.track().getAValueReachableFromSource() }
/** Gets the `ActiveRecordModelClass` that this is an instance of. */ /** Gets the `ActiveRecordModelClass` that this is an instance of. */
ActiveRecordModelClass getClass() { result = instantiation.getClass() } ActiveRecordModelClass getClass() { result = instantiation.getClass() }
@@ -380,12 +404,12 @@ private module Persistence {
/** A call to e.g. `User.create(name: "foo")` */ /** A call to e.g. `User.create(name: "foo")` */
private class CreateLikeCall extends DataFlow::CallNode, PersistentWriteAccess::Range { private class CreateLikeCall extends DataFlow::CallNode, PersistentWriteAccess::Range {
CreateLikeCall() { CreateLikeCall() {
exists(this.asExpr().getExpr().(ActiveRecordModelClassMethodCall).getReceiverClass()) and this =
this.getMethodName() = activeRecordBaseClass()
[ .getAMethodCall([
"create", "create!", "create_or_find_by", "create_or_find_by!", "find_or_create_by", "create", "create!", "create_or_find_by", "create_or_find_by!", "find_or_create_by",
"find_or_create_by!", "insert", "insert!" "find_or_create_by!", "insert", "insert!"
] ])
} }
override DataFlow::Node getValue() { override DataFlow::Node getValue() {
@@ -402,8 +426,7 @@ private module Persistence {
/** A call to e.g. `User.update(1, name: "foo")` */ /** A call to e.g. `User.update(1, name: "foo")` */
private class UpdateLikeClassMethodCall extends DataFlow::CallNode, PersistentWriteAccess::Range { private class UpdateLikeClassMethodCall extends DataFlow::CallNode, PersistentWriteAccess::Range {
UpdateLikeClassMethodCall() { UpdateLikeClassMethodCall() {
exists(this.asExpr().getExpr().(ActiveRecordModelClassMethodCall).getReceiverClass()) and this = activeRecordBaseClass().getAMethodCall(["update", "update!", "upsert"])
this.getMethodName() = ["update", "update!", "upsert"]
} }
override DataFlow::Node getValue() { override DataFlow::Node getValue() {
@@ -448,10 +471,7 @@ private module Persistence {
* ``` * ```
*/ */
private class TouchAllCall extends DataFlow::CallNode, PersistentWriteAccess::Range { private class TouchAllCall extends DataFlow::CallNode, PersistentWriteAccess::Range {
TouchAllCall() { TouchAllCall() { this = activeRecordQueryBuilderCall("touch_all") }
exists(this.asExpr().getExpr().(ActiveRecordModelClassMethodCall).getReceiverClass()) and
this.getMethodName() = "touch_all"
}
override DataFlow::Node getValue() { result = this.getKeywordArgument("time") } override DataFlow::Node getValue() { result = this.getKeywordArgument("time") }
} }
@@ -461,8 +481,7 @@ private module Persistence {
private ExprNodes::ArrayLiteralCfgNode arr; private ExprNodes::ArrayLiteralCfgNode arr;
InsertAllLikeCall() { InsertAllLikeCall() {
exists(this.asExpr().getExpr().(ActiveRecordModelClassMethodCall).getReceiverClass()) and this = activeRecordBaseClass().getAMethodCall(["insert_all", "insert_all!", "upsert_all"]) and
this.getMethodName() = ["insert_all", "insert_all!", "upsert_all"] and
arr = this.getArgument(0).asExpr() arr = this.getArgument(0).asExpr()
} }

View File

@@ -18,8 +18,12 @@ module ActiveResource {
* An ActiveResource model class. This is any (transitive) subclass of ActiveResource. * An ActiveResource model class. This is any (transitive) subclass of ActiveResource.
*/ */
pragma[nomagic] pragma[nomagic]
private API::Node modelApiNode() { private API::Node activeResourceBaseClass() {
result = API::getTopLevelMember("ActiveResource").getMember("Base").getASubclass() result = API::getTopLevelMember("ActiveResource").getMember("Base")
}
private DataFlow::ClassNode activeResourceClass() {
result = activeResourceBaseClass().getADescendentModule()
} }
/** /**
@@ -30,16 +34,8 @@ module ActiveResource {
* end * end
* ``` * ```
*/ */
class ModelClass extends ClassDeclaration { class ModelClassNode extends DataFlow::ClassNode {
API::Node model; ModelClassNode() { this = activeResourceClass() }
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. */ /** Gets a call to `site=`, which sets the base URL for this model. */
SiteAssignCall getASiteAssignment() { result.getModelClass() = this } SiteAssignCall getASiteAssignment() { result.getModelClass() = this }
@@ -49,6 +45,46 @@ module ActiveResource {
c = this.getASiteAssignment() and c = this.getASiteAssignment() and
c.disablesCertificateValidation() c.disablesCertificateValidation()
} }
/** Gets a method call on this class that returns an instance of the class. */
private DataFlow::CallNode getAChainedCall() {
result.(FindCall).getModelClass() = this
or
result.(CreateCall).getModelClass() = this
or
result.(CustomHttpCall).getModelClass() = this
or
result.(CollectionCall).getCollection().getModelClass() = this and
result.getMethodName() = ["first", "last"]
}
/** Gets an API node referring to an instance of this class. */
API::Node getAnInstanceReference() {
result = this.trackInstance()
or
result = this.getAChainedCall().track()
}
}
/** DEPRECATED. Use `ModelClassNode` instead. */
deprecated class ModelClass extends ClassDeclaration {
private ModelClassNode cls;
ModelClass() { this = cls.getADeclaration() }
/** Gets the class for which this is a declaration. */
ModelClassNode getClassNode() { result = cls }
/** Gets the API node for this class object. */
deprecated API::Node getModelApiNode() { result = cls.trackModule() }
/** Gets a call to `site=`, which sets the base URL for this model. */
SiteAssignCall getASiteAssignment() { result = cls.getASiteAssignment() }
/** Holds if `c` sets a base URL which does not use HTTPS. */
predicate disablesCertificateValidation(SiteAssignCall c) {
cls.disablesCertificateValidation(c)
}
} }
/** /**
@@ -62,25 +98,20 @@ module ActiveResource {
* ``` * ```
*/ */
class ModelClassMethodCall extends DataFlow::CallNode { class ModelClassMethodCall extends DataFlow::CallNode {
API::Node model; private ModelClassNode cls;
ModelClassMethodCall() { ModelClassMethodCall() { this = cls.trackModule().getAMethodCall(_) }
model = modelApiNode() and
this = classMethodCall(model, _)
}
/** Gets the model class for this call. */ /** Gets the model class for this call. */
ModelClass getModelClass() { result.getModelApiNode() = model } ModelClassNode getModelClass() { result = cls }
} }
/** /**
* A call to `site=` on an ActiveResource model class. * A call to `site=` on an ActiveResource model class.
* This sets the base URL for all HTTP requests made by this class. * This sets the base URL for all HTTP requests made by this class.
*/ */
private class SiteAssignCall extends DataFlow::CallNode { private class SiteAssignCall extends ModelClassMethodCall {
API::Node model; SiteAssignCall() { this.getMethodName() = "site=" }
SiteAssignCall() { model = modelApiNode() and this = classMethodCall(model, "site=") }
/** /**
* Gets a node that contributes to the URLs used for HTTP requests by the parent * Gets a node that contributes to the URLs used for HTTP requests by the parent
@@ -88,12 +119,10 @@ module ActiveResource {
*/ */
DataFlow::Node getAUrlPart() { result = this.getArgument(0) } 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. */ /** Holds if this site value specifies HTTP rather than HTTPS. */
predicate disablesCertificateValidation() { predicate disablesCertificateValidation() {
this.getAUrlPart() this.getAUrlPart()
// TODO: We should not need all this just to get the string value
.asExpr() .asExpr()
.(ExprNodes::AssignExprCfgNode) .(ExprNodes::AssignExprCfgNode)
.getRhs() .getRhs()
@@ -141,87 +170,70 @@ module ActiveResource {
} }
/** /**
* DEPRECATED. Use `ModelClassNode.getAnInstanceReference()` instead.
*
* An ActiveResource model object. * An ActiveResource model object.
*/ */
class ModelInstance extends DataFlow::Node { deprecated class ModelInstance extends DataFlow::Node {
ModelClass cls; private ModelClassNode cls;
ModelInstance() { ModelInstance() { this = cls.getAnInstanceReference().getAValueReachableFromSource() }
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. */ /** Gets the model class for this instance. */
ModelClass getModelClass() { result = cls } ModelClassNode getModelClass() { result = cls }
} }
/** /**
* A call to a method on an ActiveResource model object. * A call to a method on an ActiveResource model object.
*/ */
class ModelInstanceMethodCall extends DataFlow::CallNode { class ModelInstanceMethodCall extends DataFlow::CallNode {
ModelInstance i; private ModelClassNode cls;
ModelInstanceMethodCall() { this.getReceiver() = i } ModelInstanceMethodCall() { this = cls.getAnInstanceReference().getAMethodCall(_) }
/** Gets the model instance for this call. */ /** Gets the model instance for this call. */
ModelInstance getInstance() { result = i } deprecated ModelInstance getInstance() { result = this.getReceiver() }
/** Gets the model class for this call. */ /** Gets the model class for this call. */
ModelClass getModelClass() { result = i.getModelClass() } ModelClassNode getModelClass() { result = cls }
} }
/** /**
* A collection of ActiveResource model objects. * DEPRECATED. Use `CollectionSource` instead.
*
* A data flow node that may refer to a collection of ActiveResource model objects.
*/ */
class Collection extends DataFlow::Node { deprecated class Collection extends DataFlow::Node {
ModelClassMethodCall classMethodCall; Collection() { this = any(CollectionSource src).track().getAValueReachableFromSource() }
}
Collection() { /**
classMethodCall.flowsTo(this) and * A call that returns a collection of ActiveResource model objects.
( */
classMethodCall.getMethodName() = "all" class CollectionSource extends ModelClassMethodCall {
or CollectionSource() {
classMethodCall.getMethodName() = "find" and this.getMethodName() = "all"
classMethodCall.getArgument(0).asExpr().getConstantValue().isStringlikeValue("all") or
) this.getArgument(0).asExpr().getConstantValue().isStringlikeValue("all")
} }
/** Gets the model class for this collection. */
ModelClass getModelClass() { result = classMethodCall.getModelClass() }
} }
/** /**
* A method call on a collection. * A method call on a collection.
*/ */
class CollectionCall extends DataFlow::CallNode { class CollectionCall extends DataFlow::CallNode {
CollectionCall() { this.getReceiver() instanceof Collection } private CollectionSource collection;
CollectionCall() { this = collection.track().getAMethodCall(_) }
/** Gets the collection for this call. */ /** Gets the collection for this call. */
Collection getCollection() { result = this.getReceiver() } CollectionSource getCollection() { result = collection }
} }
private class ModelClassMethodCallAsHttpRequest extends Http::Client::Request::Range, private class ModelClassMethodCallAsHttpRequest extends Http::Client::Request::Range,
ModelClassMethodCall ModelClassMethodCall
{ {
ModelClass cls;
ModelClassMethodCallAsHttpRequest() { ModelClassMethodCallAsHttpRequest() {
this.getModelClass() = cls and
this.getMethodName() = ["all", "build", "create", "create!", "find", "first", "last"] this.getMethodName() = ["all", "build", "create", "create!", "find", "first", "last"]
} }
@@ -230,12 +242,14 @@ module ActiveResource {
override predicate disablesCertificateValidation( override predicate disablesCertificateValidation(
DataFlow::Node disablingNode, DataFlow::Node argumentOrigin DataFlow::Node disablingNode, DataFlow::Node argumentOrigin
) { ) {
cls.disablesCertificateValidation(disablingNode) and this.getModelClass().disablesCertificateValidation(disablingNode) and
// TODO: highlight real argument origin // TODO: highlight real argument origin
argumentOrigin = disablingNode argumentOrigin = disablingNode
} }
override DataFlow::Node getAUrlPart() { result = cls.getASiteAssignment().getAUrlPart() } override DataFlow::Node getAUrlPart() {
result = this.getModelClass().getASiteAssignment().getAUrlPart()
}
override DataFlow::Node getResponseBody() { result = this } override DataFlow::Node getResponseBody() { result = this }
} }
@@ -243,10 +257,7 @@ module ActiveResource {
private class ModelInstanceMethodCallAsHttpRequest extends Http::Client::Request::Range, private class ModelInstanceMethodCallAsHttpRequest extends Http::Client::Request::Range,
ModelInstanceMethodCall ModelInstanceMethodCall
{ {
ModelClass cls;
ModelInstanceMethodCallAsHttpRequest() { ModelInstanceMethodCallAsHttpRequest() {
this.getModelClass() = cls and
this.getMethodName() = this.getMethodName() =
[ [
"exists?", "reload", "save", "save!", "destroy", "delete", "get", "patch", "post", "put", "exists?", "reload", "save", "save!", "destroy", "delete", "get", "patch", "post", "put",
@@ -259,42 +270,15 @@ module ActiveResource {
override predicate disablesCertificateValidation( override predicate disablesCertificateValidation(
DataFlow::Node disablingNode, DataFlow::Node argumentOrigin DataFlow::Node disablingNode, DataFlow::Node argumentOrigin
) { ) {
cls.disablesCertificateValidation(disablingNode) and this.getModelClass().disablesCertificateValidation(disablingNode) and
// TODO: highlight real argument origin // TODO: highlight real argument origin
argumentOrigin = disablingNode argumentOrigin = disablingNode
} }
override DataFlow::Node getAUrlPart() { result = cls.getASiteAssignment().getAUrlPart() } override DataFlow::Node getAUrlPart() {
result = this.getModelClass().getASiteAssignment().getAUrlPart()
}
override DataFlow::Node getResponseBody() { result = this } 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

@@ -39,13 +39,8 @@ private API::Node graphQlSchema() { result = API::getTopLevelMember("GraphQL").g
*/ */
private class GraphqlRelayClassicMutationClass extends ClassDeclaration { private class GraphqlRelayClassicMutationClass extends ClassDeclaration {
GraphqlRelayClassicMutationClass() { GraphqlRelayClassicMutationClass() {
this.getSuperclassExpr() = this =
graphQlSchema() graphQlSchema().getMember("RelayClassicMutation").getADescendentModule().getADeclaration()
.getMember("RelayClassicMutation")
.getASubclass()
.getAValueReachableFromSource()
.asExpr()
.getExpr()
} }
} }
@@ -74,13 +69,7 @@ private class GraphqlRelayClassicMutationClass extends ClassDeclaration {
*/ */
private class GraphqlSchemaResolverClass extends ClassDeclaration { private class GraphqlSchemaResolverClass extends ClassDeclaration {
GraphqlSchemaResolverClass() { GraphqlSchemaResolverClass() {
this.getSuperclassExpr() = this = graphQlSchema().getMember("Resolver").getADescendentModule().getADeclaration()
graphQlSchema()
.getMember("Resolver")
.getASubclass()
.getAValueReachableFromSource()
.asExpr()
.getExpr()
} }
} }
@@ -103,13 +92,7 @@ private string getASupportedHttpMethod() { result = ["get", "post"] }
*/ */
class GraphqlSchemaObjectClass extends ClassDeclaration { class GraphqlSchemaObjectClass extends ClassDeclaration {
GraphqlSchemaObjectClass() { GraphqlSchemaObjectClass() {
this.getSuperclassExpr() = this = graphQlSchema().getMember("Object").getADescendentModule().getADeclaration()
graphQlSchema()
.getMember("Object")
.getASubclass()
.getAValueReachableFromSource()
.asExpr()
.getExpr()
} }
/** Gets a `GraphqlFieldDefinitionMethodCall` called in this class. */ /** Gets a `GraphqlFieldDefinitionMethodCall` called in this class. */

View File

@@ -15,21 +15,20 @@ private import codeql.ruby.Concepts
* https://github.com/sparklemotion/sqlite3-ruby * https://github.com/sparklemotion/sqlite3-ruby
*/ */
module Sqlite3 { module Sqlite3 {
private API::Node databaseConst() {
result = API::getTopLevelMember("SQLite3").getMember("Database")
}
private API::Node dbInstance() {
result = databaseConst().getInstance()
or
// e.g. SQLite3::Database.new("foo.db") |db| { db.some_method }
result = databaseConst().getMethod("new").getBlock().getParameter(0)
}
/** Gets a method call with a receiver that is a database instance. */ /** Gets a method call with a receiver that is a database instance. */
private DataFlow::CallNode getADatabaseMethodCall(string methodName) { private DataFlow::CallNode getADatabaseMethodCall(string methodName) {
exists(API::Node dbInstance | result = dbInstance().getAMethodCall(methodName)
dbInstance = API::getTopLevelMember("SQLite3").getMember("Database").getInstance() and
(
result = dbInstance.getAMethodCall(methodName)
or
// e.g. SQLite3::Database.new("foo.db") |db| { db.some_method }
exists(DataFlow::BlockNode block |
result.getMethodName() = methodName and
block = dbInstance.getAValueReachableFromSource().(DataFlow::CallNode).getBlock() and
block.getParameter(0).flowsTo(result.getReceiver())
)
)
)
} }
/** A prepared but unexecuted SQL statement. */ /** A prepared but unexecuted SQL statement. */

View File

@@ -16,50 +16,28 @@ module Twirp {
/** /**
* A Twirp service instantiation * A Twirp service instantiation
*/ */
class ServiceInstantiation extends DataFlow::CallNode { deprecated class ServiceInstantiation extends DataFlow::CallNode {
ServiceInstantiation() { ServiceInstantiation() {
this = API::getTopLevelMember("Twirp").getMember("Service").getAnInstantiation() this = API::getTopLevelMember("Twirp").getMember("Service").getAnInstantiation()
} }
/**
* Gets a local source node for the Service instantiation argument (the service handler).
*/
private DataFlow::LocalSourceNode getHandlerSource() {
result = this.getArgument(0).getALocalSource()
}
/**
* Gets the API::Node for the service handler's class.
*/
private API::Node getAHandlerClassApiNode() {
result.getAnInstantiation() = this.getHandlerSource()
}
/**
* Gets the AST module for the service handler's class.
*/
private Ast::Module getAHandlerClassAstNode() {
result =
this.getAHandlerClassApiNode()
.asSource()
.asExpr()
.(CfgNodes::ExprNodes::ConstantReadAccessCfgNode)
.getExpr()
.getModule()
}
/** /**
* Gets a handler's method. * Gets a handler's method.
*/ */
Ast::Method getAHandlerMethod() { DataFlow::MethodNode getAHandlerMethodNode() {
result = this.getAHandlerClassAstNode().getAnInstanceMethod() result = this.getArgument(0).backtrack().getMethod(_).asCallable()
} }
/**
* Gets a handler's method as an AST node.
*/
Ast::Method getAHandlerMethod() { result = this.getAHandlerMethodNode().asCallableAstNode() }
} }
/** /**
* A Twirp client * A Twirp client
*/ */
class ClientInstantiation extends DataFlow::CallNode { deprecated class ClientInstantiation extends DataFlow::CallNode {
ClientInstantiation() { ClientInstantiation() {
this = API::getTopLevelMember("Twirp").getMember("Client").getAnInstantiation() this = API::getTopLevelMember("Twirp").getMember("Client").getAnInstantiation()
} }
@@ -67,7 +45,10 @@ module Twirp {
/** The URL of a Twirp service, considered as a sink. */ /** The URL of a Twirp service, considered as a sink. */
class ServiceUrlAsSsrfSink extends ServerSideRequestForgery::Sink { class ServiceUrlAsSsrfSink extends ServerSideRequestForgery::Sink {
ServiceUrlAsSsrfSink() { exists(ClientInstantiation c | c.getArgument(0) = this) } ServiceUrlAsSsrfSink() {
this =
API::getTopLevelMember("Twirp").getMember("Client").getMethod("new").getArgument(0).asSink()
}
} }
/** A parameter that will receive parts of the url when handling an incoming request. */ /** A parameter that will receive parts of the url when handling an incoming request. */
@@ -75,7 +56,14 @@ module Twirp {
DataFlow::ParameterNode DataFlow::ParameterNode
{ {
UnmarshaledParameter() { UnmarshaledParameter() {
exists(ServiceInstantiation i | i.getAHandlerMethod().getParameter(0) = this.asParameter()) this =
API::getTopLevelMember("Twirp")
.getMember("Service")
.getMethod("new")
.getArgument(0)
.getMethod(_)
.getParameter(0)
.asSource()
} }
override string getSourceType() { result = "Twirp Unmarhaled Parameter" } override string getSourceType() { result = "Twirp Unmarhaled Parameter" }

View File

@@ -24,7 +24,7 @@ module Gem {
GemSpec() { GemSpec() {
this.getExtension() = "gemspec" and this.getExtension() = "gemspec" and
specCall = API::root().getMember("Gem").getMember("Specification").getMethod("new") and specCall = API::getTopLevelMember("Gem").getMember("Specification").getMethod("new") and
specCall.getLocation().getFile() = this specCall.getLocation().getFile() = this
} }
@@ -42,7 +42,7 @@ module Gem {
.getBlock() .getBlock()
.getParameter(0) .getParameter(0)
.getMethod(name + "=") .getMethod(name + "=")
.getParameter(0) .getArgument(0)
.asSink() .asSink()
.asExpr() .asExpr()
.getExpr() .getExpr()

View File

@@ -19,7 +19,8 @@ module Kernel {
*/ */
class KernelMethodCall extends DataFlow::CallNode { class KernelMethodCall extends DataFlow::CallNode {
KernelMethodCall() { KernelMethodCall() {
this = API::getTopLevelMember("Kernel").getAMethodCall(_) // Match Kernel calls using local flow, to avoid finding singleton calls on subclasses
this = DataFlow::getConstant("Kernel").getAMethodCall(_)
or or
this.asExpr().getExpr() instanceof UnknownMethodCall and this.asExpr().getExpr() instanceof UnknownMethodCall and
( (

View File

@@ -44,7 +44,7 @@ private class SummarizedCallableFromModel extends SummarizedCallable {
override Call getACall() { override Call getACall() {
exists(API::MethodAccessNode base | exists(API::MethodAccessNode base |
ModelOutput::resolvedSummaryBase(type, path, base) and ModelOutput::resolvedSummaryBase(type, path, base) and
result = base.getCallNode().asExpr().getExpr() result = base.asCall().asExpr().getExpr()
) )
} }

View File

@@ -99,9 +99,10 @@ API::Node getExtraNodeFromPath(string type, AccessPath path, int n) {
// A row of form `any;Method[foo]` should match any method named `foo`. // A row of form `any;Method[foo]` should match any method named `foo`.
type = "any" and type = "any" and
n = 1 and n = 1 and
exists(EntryPointFromAnyType entry | exists(string methodName, DataFlow::CallNode call |
methodMatchedByName(path, entry.getName()) and methodMatchedByName(path, methodName) and
result = entry.getANode() call.getMethodName() = methodName and
result.(API::MethodAccessNode).asCall() = call
) )
} }
@@ -112,20 +113,10 @@ API::Node getExtraNodeFromType(string type) {
constRef = getConstantFromConstPath(consts) constRef = getConstantFromConstPath(consts)
| |
suffix = "!" and suffix = "!" and
( result = constRef.track()
result.(API::Node::Internal).asSourceInternal() = constRef
or
result.(API::Node::Internal).asSourceInternal() =
constRef.getADescendentModule().getAnOwnModuleSelf()
)
or or
suffix = "" and suffix = "" and
( result = constRef.track().getInstance()
result.(API::Node::Internal).asSourceInternal() = constRef.getAMethodCall("new")
or
result.(API::Node::Internal).asSourceInternal() =
constRef.getADescendentModule().getAnInstanceSelf()
)
) )
or or
type = "" and type = "" and
@@ -145,21 +136,6 @@ private predicate methodMatchedByName(AccessPath path, string methodName) {
) )
} }
/**
* An API graph entry point corresponding to a method name such as `foo` in `;any;Method[foo]`.
*
* This ensures that the API graph rooted in that method call is materialized.
*/
private class EntryPointFromAnyType extends API::EntryPoint {
string name;
EntryPointFromAnyType() { this = "AnyMethod[" + name + "]" and methodMatchedByName(_, name) }
override DataFlow::CallNode getACall() { result.getMethodName() = name }
string getName() { result = name }
}
/** /**
* Gets a Ruby-specific API graph successor of `node` reachable by resolving `token`. * Gets a Ruby-specific API graph successor of `node` reachable by resolving `token`.
*/ */
@@ -175,9 +151,11 @@ API::Node getExtraSuccessorFromNode(API::Node node, AccessPathToken token) {
result = node.getInstance() result = node.getInstance()
or or
token.getName() = "Parameter" and token.getName() = "Parameter" and
result = exists(DataFlowDispatch::ArgumentPosition argPos, DataFlowDispatch::ParameterPosition paramPos |
node.getASuccessor(API::Label::getLabelFromParameterPosition(FlowSummaryImplSpecific::parseArgBody(token argPos = FlowSummaryImplSpecific::parseParamBody(token.getAnArgument()) and
.getAnArgument()))) DataFlowDispatch::parameterMatch(paramPos, argPos) and
result = node.getParameterAtPosition(paramPos)
)
or or
exists(DataFlow::ContentSet contents | exists(DataFlow::ContentSet contents |
SummaryComponent::content(contents) = FlowSummaryImplSpecific::interpretComponentSpecific(token) and SummaryComponent::content(contents) = FlowSummaryImplSpecific::interpretComponentSpecific(token) and
@@ -191,9 +169,11 @@ API::Node getExtraSuccessorFromNode(API::Node node, AccessPathToken token) {
bindingset[token] bindingset[token]
API::Node getExtraSuccessorFromInvoke(InvokeNode node, AccessPathToken token) { API::Node getExtraSuccessorFromInvoke(InvokeNode node, AccessPathToken token) {
token.getName() = "Argument" and token.getName() = "Argument" and
result = exists(DataFlowDispatch::ArgumentPosition argPos, DataFlowDispatch::ParameterPosition paramPos |
node.getASuccessor(API::Label::getLabelFromArgumentPosition(FlowSummaryImplSpecific::parseParamBody(token paramPos = FlowSummaryImplSpecific::parseArgBody(token.getAnArgument()) and
.getAnArgument()))) DataFlowDispatch::parameterMatch(paramPos, argPos) and
result = node.getArgumentAtPosition(argPos)
)
} }
/** /**
@@ -211,7 +191,7 @@ predicate invocationMatchesExtraCallSiteFilter(InvokeNode invoke, AccessPathToke
/** An API graph node representing a method call. */ /** An API graph node representing a method call. */
class InvokeNode extends API::MethodAccessNode { class InvokeNode extends API::MethodAccessNode {
/** Gets the number of arguments to the call. */ /** Gets the number of arguments to the call. */
int getNumArgument() { result = this.getCallNode().getNumberOfArguments() } int getNumArgument() { result = this.asCall().getNumberOfArguments() }
} }
/** Gets the `InvokeNode` corresponding to a specific invocation of `node`. */ /** Gets the `InvokeNode` corresponding to a specific invocation of `node`. */

View File

@@ -55,7 +55,8 @@ class AmbiguousPathCall extends DataFlow::CallNode {
} }
private predicate methodCallOnlyOnIO(DataFlow::CallNode node, string methodName) { private predicate methodCallOnlyOnIO(DataFlow::CallNode node, string methodName) {
node = API::getTopLevelMember("IO").getAMethodCall(methodName) and // Use local flow to find calls to 'IO' without subclasses
node = DataFlow::getConstant("IO").getAMethodCall(methodName) and
not node = API::getTopLevelMember("File").getAMethodCall(methodName) // needed in e.g. opal/opal, where some calls have both paths (opal implements an own corelib) not node = API::getTopLevelMember("File").getAMethodCall(methodName) // needed in e.g. opal/opal, where some calls have both paths (opal implements an own corelib)
} }

View File

@@ -597,7 +597,7 @@ private module Digest {
call = API::getTopLevelMember("OpenSSL").getMember("Digest").getMethod("new") call = API::getTopLevelMember("OpenSSL").getMember("Digest").getMethod("new")
| |
this = call.getReturn().getAMethodCall(["digest", "update", "<<"]) and this = call.getReturn().getAMethodCall(["digest", "update", "<<"]) and
algo.matchesName(call.getCallNode() algo.matchesName(call.asCall()
.getArgument(0) .getArgument(0)
.asExpr() .asExpr()
.getExpr() .getExpr()
@@ -619,7 +619,7 @@ private module Digest {
Cryptography::HashingAlgorithm algo; Cryptography::HashingAlgorithm algo;
DigestCallDirect() { DigestCallDirect() {
this = API::getTopLevelMember("OpenSSL").getMember("Digest").getMethod("digest").getCallNode() and this = API::getTopLevelMember("OpenSSL").getMember("Digest").getMethod("digest").asCall() and
algo.matchesName(this.getArgument(0).asExpr().getExpr().getConstantValue().getString()) algo.matchesName(this.getArgument(0).asExpr().getExpr().getConstantValue().getString())
} }

View File

@@ -0,0 +1,328 @@
/**
* Parts of API graphs that can be shared with other dynamic languages.
*
* Depends on TypeTrackerSpecific for the corresponding language.
*/
private import codeql.Locations
private import codeql.ruby.typetracking.TypeTracker
private import TypeTrackerSpecific
/**
* The signature to use when instantiating `ApiGraphShared`.
*
* The implementor should define a newtype with at least three branches as follows:
* ```ql
* newtype TApiNode =
* MkForwardNode(LocalSourceNode node, TypeTracker t) { isReachable(node, t) } or
* MkBackwardNode(LocalSourceNode node, TypeTracker t) { isReachable(node, t) } or
* MkSinkNode(Node node) { ... } or
* ...
* ```
*
* The three branches should be exposed through `getForwardNode`, `getBackwardNode`, and `getSinkNode`, respectively.
*/
signature module ApiGraphSharedSig {
/** A node in the API graph. */
class ApiNode {
/** Gets a string representation of this API node. */
string toString();
/** Gets the location associated with this API node, if any. */
Location getLocation();
}
/**
* Gets the forward node with the given type-tracking state.
*
* This node will have outgoing epsilon edges to its type-tracking successors.
*/
ApiNode getForwardNode(TypeTrackingNode node, TypeTracker t);
/**
* Gets the backward node with the given type-tracking state.
*
* This node will have outgoing epsilon edges to its type-tracking predecessors.
*/
ApiNode getBackwardNode(TypeTrackingNode node, TypeTracker t);
/**
* Gets the sink node corresponding to `node`.
*
* Since sinks are not generally `LocalSourceNode`s, such nodes are materialised separately in order for
* the API graph to include representatives for sinks. Note that there is no corresponding case for "source"
* nodes as these are represented as forward nodes with initial-state type-trackers.
*
* Sink nodes have outgoing epsilon edges to the backward nodes corresponding to their local sources.
*/
ApiNode getSinkNode(Node node);
/**
* Holds if a language-specific epsilon edge `pred -> succ` should be generated.
*/
predicate specificEpsilonEdge(ApiNode pred, ApiNode succ);
}
/**
* Parts of API graphs that can be shared between language implementations.
*/
module ApiGraphShared<ApiGraphSharedSig S> {
private import S
/** Gets a local source of `node`. */
bindingset[node]
pragma[inline_late]
TypeTrackingNode getALocalSourceStrict(Node node) { result = node.getALocalSource() }
cached
private module Cached {
/**
* Holds if there is an epsilon edge `pred -> succ`.
*
* That relation is reflexive, so `fastTC` produces the equivalent of a reflexive, transitive closure.
*/
pragma[noopt]
cached
predicate epsilonEdge(ApiNode pred, ApiNode succ) {
exists(
StepSummary summary, TypeTrackingNode predNode, TypeTracker predState,
TypeTrackingNode succNode, TypeTracker succState
|
StepSummary::stepCall(predNode, succNode, summary)
or
StepSummary::stepNoCall(predNode, succNode, summary)
|
pred = getForwardNode(predNode, predState) and
succState = StepSummary::append(predState, summary) and
succ = getForwardNode(succNode, succState)
or
succ = getBackwardNode(predNode, predState) and // swap order for backward flow
succState = StepSummary::append(predState, summary) and
pred = getBackwardNode(succNode, succState) // swap order for backward flow
)
or
exists(Node sink, TypeTrackingNode localSource |
pred = getSinkNode(sink) and
localSource = getALocalSourceStrict(sink) and
succ = getBackwardStartNode(localSource)
)
or
specificEpsilonEdge(pred, succ)
or
succ instanceof ApiNode and
succ = pred
}
/**
* Holds if `pred` can reach `succ` by zero or more epsilon edges.
*/
cached
predicate epsilonStar(ApiNode pred, ApiNode succ) = fastTC(epsilonEdge/2)(pred, succ)
/** Gets the API node to use when starting forward flow from `source` */
cached
ApiNode forwardStartNode(TypeTrackingNode source) {
result = getForwardNode(source, TypeTracker::end(false))
}
/** Gets the API node to use when starting backward flow from `sink` */
cached
ApiNode backwardStartNode(TypeTrackingNode sink) {
// There is backward flow A->B iff there is forward flow B->A.
// The starting point of backward flow corresponds to the end of a forward flow, and vice versa.
result = getBackwardNode(sink, TypeTracker::end(_))
}
/** Gets `node` as a data flow source. */
cached
TypeTrackingNode asSourceCached(ApiNode node) { node = forwardEndNode(result) }
/** Gets `node` as a data flow sink. */
cached
Node asSinkCached(ApiNode node) { node = getSinkNode(result) }
}
private import Cached
/** Gets an API node corresponding to the end of forward-tracking to `localSource`. */
pragma[nomagic]
private ApiNode forwardEndNode(TypeTrackingNode localSource) {
result = getForwardNode(localSource, TypeTracker::end(_))
}
/** Gets an API node corresponding to the end of backtracking to `localSource`. */
pragma[nomagic]
private ApiNode backwardEndNode(TypeTrackingNode localSource) {
result = getBackwardNode(localSource, TypeTracker::end(false))
}
/** Gets a node reachable from `node` by zero or more epsilon edges, including `node` itself. */
bindingset[node]
pragma[inline_late]
ApiNode getAnEpsilonSuccessorInline(ApiNode node) { epsilonStar(node, result) }
/** Gets `node` as a data flow sink. */
bindingset[node]
pragma[inline_late]
Node asSinkInline(ApiNode node) { result = asSinkCached(node) }
/** Gets `node` as a data flow source. */
bindingset[node]
pragma[inline_late]
TypeTrackingNode asSourceInline(ApiNode node) { result = asSourceCached(node) }
/** Gets a value reachable from `source`. */
bindingset[source]
pragma[inline_late]
Node getAValueReachableFromSourceInline(ApiNode source) {
exists(TypeTrackingNode src |
src = asSourceInline(getAnEpsilonSuccessorInline(source)) and
src.flowsTo(pragma[only_bind_into](result))
)
}
/** Gets a value that can reach `sink`. */
bindingset[sink]
pragma[inline_late]
Node getAValueReachingSinkInline(ApiNode sink) {
result = asSinkInline(getAnEpsilonSuccessorInline(sink))
}
/**
* Gets the starting point for forward-tracking at `node`.
*
* Should be used to obtain the successor of an edge when constructing labelled edges.
*/
bindingset[node]
pragma[inline_late]
ApiNode getForwardStartNode(Node node) { result = forwardStartNode(node) }
/**
* Gets the starting point of backtracking from `node`.
*
* Should be used to obtain the successor of an edge when constructing labelled edges.
*/
bindingset[node]
pragma[inline_late]
ApiNode getBackwardStartNode(Node node) { result = backwardStartNode(node) }
/**
* Gets a possible ending point of forward-tracking at `node`.
*
* Should be used to obtain the predecessor of an edge when constructing labelled edges.
*
* This is not backed by a `cached` predicate, and should only be used for materialising `cached`
* predicates in the API graph implementation - it should not be called in later stages.
*/
bindingset[node]
pragma[inline_late]
ApiNode getForwardEndNode(Node node) { result = forwardEndNode(node) }
/**
* Gets a possible ending point backtracking to `node`.
*
* Should be used to obtain the predecessor of an edge when constructing labelled edges.
*
* This is not backed by a `cached` predicate, and should only be used for materialising `cached`
* predicates in the API graph implementation - it should not be called in later stages.
*/
bindingset[node]
pragma[inline_late]
ApiNode getBackwardEndNode(Node node) { result = backwardEndNode(node) }
/**
* Gets a possible eding point of forward or backward tracking at `node`.
*
* Should be used to obtain the predecessor of an edge generated from store or load edges.
*/
bindingset[node]
pragma[inline_late]
ApiNode getForwardOrBackwardEndNode(Node node) {
result = getForwardEndNode(node) or result = getBackwardEndNode(node)
}
/** Gets an API node for tracking forward starting at `node`. This is the implementation of `DataFlow::LocalSourceNode.track()` */
bindingset[node]
pragma[inline_late]
ApiNode getNodeForForwardTracking(Node node) { result = forwardStartNode(node) }
/** Gets an API node for backtracking starting at `node`. The implementation of `DataFlow::Node.backtrack()`. */
bindingset[node]
pragma[inline_late]
ApiNode getNodeForBacktracking(Node node) {
result = getBackwardStartNode(getALocalSourceStrict(node))
}
/** Parts of the shared module to be re-exported by the user-facing `API` module. */
module Public {
/**
* The signature to use when instantiating the `ExplainFlow` module.
*/
signature module ExplainFlowSig {
/** Holds if `node` should be a source. */
predicate isSource(ApiNode node);
/** Holds if `node` should be a sink. */
default predicate isSink(ApiNode node) { any() }
/** Holds if `node` should be skipped in the generated paths. */
default predicate isHidden(ApiNode node) { none() }
}
/**
* Module to help debug and visualize the data flows underlying API graphs.
*
* This module exports the query predicates for a path-problem query, and should be imported
* into the top-level of such a query.
*
* The module argument should specify source and sink API nodes, and the resulting query
* will show paths of epsilon edges that go from a source to a sink. Only epsilon edges are visualized.
*
* To condense the output a bit, paths in which the source and sink are the same node are omitted.
*/
module ExplainFlow<ExplainFlowSig T> {
private import T
private ApiNode relevantNode() {
isSink(result) and
result = getAnEpsilonSuccessorInline(any(ApiNode node | isSource(node)))
or
epsilonEdge(result, relevantNode())
}
/** Holds if `node` is part of the graph to visualize. */
query predicate nodes(ApiNode node) { node = relevantNode() and not isHidden(node) }
private predicate edgeToHiddenNode(ApiNode pred, ApiNode succ) {
epsilonEdge(pred, succ) and
isHidden(succ) and
pred = relevantNode() and
succ = relevantNode()
}
/** Holds if `pred -> succ` is an edge in the graph to visualize. */
query predicate edges(ApiNode pred, ApiNode succ) {
nodes(pred) and
nodes(succ) and
exists(ApiNode mid |
edgeToHiddenNode*(pred, mid) and
epsilonEdge(mid, succ)
)
}
/** Holds for each source/sink pair to visualize in the graph. */
query predicate problems(
ApiNode location, ApiNode sourceNode, ApiNode sinkNode, string message
) {
nodes(sourceNode) and
nodes(sinkNode) and
isSource(sourceNode) and
isSink(sinkNode) and
sinkNode = getAnEpsilonSuccessorInline(sourceNode) and
sourceNode != sinkNode and
location = sinkNode and
message = "Node flows here"
}
}
}
}

View File

@@ -55,10 +55,9 @@ private module Cached {
) )
} }
pragma[nomagic] /** Gets a type tracker with no content and the call bit set to the given value. */
private TypeTracker noContentTypeTracker(boolean hasCall) { cached
result = MkTypeTracker(hasCall, noContent()) TypeTracker noContentTypeTracker(boolean hasCall) { result = MkTypeTracker(hasCall, noContent()) }
}
/** Gets the summary resulting from appending `step` to type-tracking summary `tt`. */ /** Gets the summary resulting from appending `step` to type-tracking summary `tt`. */
cached cached
@@ -318,6 +317,8 @@ class StepSummary extends TStepSummary {
/** Provides predicates for updating step summaries (`StepSummary`s). */ /** Provides predicates for updating step summaries (`StepSummary`s). */
module StepSummary { module StepSummary {
predicate append = Cached::append/2;
/** /**
* Gets the summary that corresponds to having taken a forwards * Gets the summary that corresponds to having taken a forwards
* inter-procedural step from `nodeFrom` to `nodeTo`. * inter-procedural step from `nodeFrom` to `nodeTo`.
@@ -378,6 +379,35 @@ module StepSummary {
} }
deprecated predicate localSourceStoreStep = flowsToStoreStep/3; deprecated predicate localSourceStoreStep = flowsToStoreStep/3;
/** Gets the step summary for a level step. */
StepSummary levelStep() { result = LevelStep() }
/** Gets the step summary for a call step. */
StepSummary callStep() { result = CallStep() }
/** Gets the step summary for a return step. */
StepSummary returnStep() { result = ReturnStep() }
/** Gets the step summary for storing into `content`. */
StepSummary storeStep(TypeTrackerContent content) { result = StoreStep(content) }
/** Gets the step summary for loading from `content`. */
StepSummary loadStep(TypeTrackerContent content) { result = LoadStep(content) }
/** Gets the step summary for loading from `load` and then storing into `store`. */
StepSummary loadStoreStep(TypeTrackerContent load, TypeTrackerContent store) {
result = LoadStoreStep(load, store)
}
/** Gets the step summary for a step that only permits contents matched by `filter`. */
StepSummary withContent(ContentFilter filter) { result = WithContent(filter) }
/** Gets the step summary for a step that blocks contents matched by `filter`. */
StepSummary withoutContent(ContentFilter filter) { result = WithoutContent(filter) }
/** Gets the step summary for a jump step. */
StepSummary jumpStep() { result = JumpStep() }
} }
/** /**
@@ -540,6 +570,13 @@ module TypeTracker {
* Gets a valid end point of type tracking. * Gets a valid end point of type tracking.
*/ */
TypeTracker end() { result.end() } TypeTracker end() { result.end() }
/**
* INTERNAL USE ONLY.
*
* Gets a valid end point of type tracking with the call bit set to the given value.
*/
predicate end = Cached::noContentTypeTracker/1;
} }
pragma[nomagic] pragma[nomagic]

View File

@@ -1,8 +1,8 @@
classMethodCalls classMethodCalls
| test1.rb:58:1:58:8 | Use getMember("M1").getMember("C1").getMethod("m").getReturn() | | test1.rb:58:1:58:8 | ForwardNode(call to m) |
| test1.rb:59:1:59:8 | Use getMember("M2").getMember("C3").getMethod("m").getReturn() | | test1.rb:59:1:59:8 | ForwardNode(call to m) |
instanceMethodCalls instanceMethodCalls
| test1.rb:61:1:61:12 | Use getMember("M1").getMember("C1").getMethod("new").getReturn().getMethod("m").getReturn() | | test1.rb:61:1:61:12 | ForwardNode(call to m) |
| test1.rb:62:1:62:12 | Use getMember("M2").getMember("C3").getMethod("new").getReturn().getMethod("m").getReturn() | | test1.rb:62:1:62:12 | ForwardNode(call to m) |
flowThroughArray flowThroughArray
| test1.rb:73:1:73:10 | call to m | | test1.rb:73:1:73:10 | call to m |

View File

@@ -0,0 +1,77 @@
import ruby
import codeql.ruby.ast.internal.TreeSitter
import codeql.ruby.dataflow.internal.AccessPathSyntax
import codeql.ruby.frameworks.data.internal.ApiGraphModels
import codeql.ruby.ApiGraphs
import TestUtilities.InlineExpectationsTest
class AccessPathFromExpectation extends AccessPath::Range {
AccessPathFromExpectation() { hasExpectationWithValue(_, this) }
}
API::Node evaluatePath(AccessPath path, int n) {
path instanceof AccessPathFromExpectation and
n = 1 and
exists(AccessPathToken token | token = path.getToken(0) |
token.getName() = "Member" and
result = API::getTopLevelMember(token.getAnArgument())
or
token.getName() = "Method" and
result = API::getTopLevelCall(token.getAnArgument())
or
token.getName() = "EntryPoint" and
result = token.getAnArgument().(API::EntryPoint).getANode()
)
or
result = getSuccessorFromNode(evaluatePath(path, n - 1), path.getToken(n - 1))
or
result = getSuccessorFromInvoke(evaluatePath(path, n - 1), path.getToken(n - 1))
or
// TODO this is a workaround, support parsing of Method['[]'] instead
path.getToken(n - 1).getName() = "MethodBracket" and
result = evaluatePath(path, n - 1).getMethod("[]")
}
API::Node evaluatePath(AccessPath path) { result = evaluatePath(path, path.getNumToken()) }
module ApiUseTest implements TestSig {
string getARelevantTag() { result = ["source", "sink", "call", "reachableFromSource"] }
predicate hasActualResult(Location location, string element, string tag, string value) {
// All results are considered optional
none()
}
predicate hasOptionalResult(Location location, string element, string tag, string value) {
exists(API::Node apiNode, DataFlow::Node dataflowNode |
apiNode = evaluatePath(value) and
(
tag = "source" and dataflowNode = apiNode.asSource()
or
tag = "reachableFromSource" and dataflowNode = apiNode.getAValueReachableFromSource()
or
tag = "sink" and dataflowNode = apiNode.asSink()
or
tag = "call" and dataflowNode = apiNode.asCall()
) and
location = dataflowNode.getLocation() and
element = dataflowNode.toString()
)
}
}
import MakeTest<ApiUseTest>
class CustomEntryPointCall extends API::EntryPoint {
CustomEntryPointCall() { this = "CustomEntryPointCall" }
override DataFlow::CallNode getACall() { result.getMethodName() = "customEntryPointCall" }
}
class CustomEntryPointUse extends API::EntryPoint {
CustomEntryPointUse() { this = "CustomEntryPointUse" }
override DataFlow::LocalSourceNode getASource() {
result.(DataFlow::CallNode).getMethodName() = "customEntryPointUse"
}
}

View File

@@ -1,39 +1,39 @@
Something.foo.withCallback do |a, b| #$ use=getMember("Something").getMethod("foo").getReturn() Something.foo.withCallback do |a, b| #$ source=Member[Something].Method[foo].ReturnValue
a.something #$ use=getMember("Something").getMethod("foo").getReturn().getMethod("withCallback").getBlock().getParameter(0).getMethod("something").getReturn() a.something #$ source=Member[Something].Method[foo].ReturnValue.Method[withCallback].Argument[block].Argument[0].Method[something].ReturnValue
b.somethingElse #$ use=getMember("Something").getMethod("foo").getReturn().getMethod("withCallback").getBlock().getParameter(1).getMethod("somethingElse").getReturn() b.somethingElse #$ source=Member[Something].Method[foo].ReturnValue.Method[withCallback].Argument[block].Argument[1].Method[somethingElse].ReturnValue
end #$ use=getMember("Something").getMethod("foo").getReturn().getMethod("withCallback").getReturn() end #$ source=Member[Something].Method[foo].ReturnValue.Method[withCallback].ReturnValue
Something.withNamedArg do |a:, b: nil| #$ use=getMember("Something") Something.withNamedArg do |a:, b: nil| #$ source=Member[Something]
a.something #$ use=getMember("Something").getMethod("withNamedArg").getBlock().getKeywordParameter("a").getMethod("something").getReturn() a.something #$ source=Member[Something].Method[withNamedArg].Argument[block].Parameter[a:].Method[something].ReturnValue
b.somethingElse #$ use=getMember("Something").getMethod("withNamedArg").getBlock().getKeywordParameter("b").getMethod("somethingElse").getReturn() b.somethingElse #$ source=Member[Something].Method[withNamedArg].Argument[block].Parameter[b:].Method[somethingElse].ReturnValue
end #$ use=getMember("Something").getMethod("withNamedArg").getReturn() end #$ source=Member[Something].Method[withNamedArg].ReturnValue
Something.withLambda ->(a, b) { #$ use=getMember("Something") Something.withLambda ->(a, b) { #$ source=Member[Something]
a.something #$ use=getMember("Something").getMethod("withLambda").getParameter(0).getParameter(0).getMethod("something").getReturn() a.something #$ source=Member[Something].Method[withLambda].Argument[0].Parameter[0].Method[something].ReturnValue
b.something #$ use=getMember("Something").getMethod("withLambda").getParameter(0).getParameter(1).getMethod("something").getReturn() b.something #$ source=Member[Something].Method[withLambda].Argument[0].Parameter[1].Method[something].ReturnValue
} #$ use=getMember("Something").getMethod("withLambda").getReturn() } #$ source=Member[Something].Method[withLambda].ReturnValue
Something.namedCallback( #$ use=getMember("Something") Something.namedCallback( #$ source=Member[Something]
onEvent: ->(a, b) { onEvent: ->(a, b) {
a.something #$ use=getMember("Something").getMethod("namedCallback").getKeywordParameter("onEvent").getParameter(0).getMethod("something").getReturn() a.something #$ source=Member[Something].Method[namedCallback].Argument[onEvent:].Parameter[0].Method[something].ReturnValue
b.something #$ use=getMember("Something").getMethod("namedCallback").getKeywordParameter("onEvent").getParameter(1).getMethod("something").getReturn() b.something #$ source=Member[Something].Method[namedCallback].Argument[onEvent:].Parameter[1].Method[something].ReturnValue
} }
) #$ use=getMember("Something").getMethod("namedCallback").getReturn() ) #$ source=Member[Something].Method[namedCallback].ReturnValue
Something.nestedCall1 do |a| #$ use=getMember("Something") Something.nestedCall1 do |a| #$ source=Member[Something]
a.nestedCall2 do |b:| #$ use=getMember("Something").getMethod("nestedCall1").getBlock().getParameter(0) a.nestedCall2 do |b:| #$ reachableFromSource=Member[Something].Method[nestedCall1].Argument[block].Parameter[0]
b.something #$ use=getMember("Something").getMethod("nestedCall1").getBlock().getParameter(0).getMethod("nestedCall2").getBlock().getKeywordParameter("b").getMethod("something").getReturn() b.something #$ source=Member[Something].Method[nestedCall1].Argument[block].Parameter[0].Method[nestedCall2].Argument[block].Parameter[b:].Method[something].ReturnValue
end #$ use=getMember("Something").getMethod("nestedCall1").getBlock().getParameter(0).getMethod("nestedCall2").getReturn() end #$ source=Member[Something].Method[nestedCall1].Argument[block].Parameter[0].Method[nestedCall2].ReturnValue
end #$ use=getMember("Something").getMethod("nestedCall1").getReturn() end #$ source=Member[Something].Method[nestedCall1].ReturnValue
def getCallback() def getCallback()
->(x) { ->(x) {
x.something #$ use=getMember("Something").getMethod("indirectCallback").getParameter(0).getParameter(0).getMethod("something").getReturn() x.something #$ source=Member[Something].Method[indirectCallback].Argument[0].Parameter[0].Method[something].ReturnValue
} }
end end
Something.indirectCallback(getCallback()) #$ use=getMember("Something").getMethod("indirectCallback").getReturn() Something.indirectCallback(getCallback()) #$ source=Member[Something].Method[indirectCallback].ReturnValue
Something.withMixed do |a, *args, b| #$ use=getMember("Something") Something.withMixed do |a, *args, b| #$ source=Member[Something]
a.something #$ use=getMember("Something").getMethod("withMixed").getBlock().getParameter(0).getMethod("something").getReturn() a.something #$ source=Member[Something].Method[withMixed].Argument[block].Parameter[0].Method[something].ReturnValue
# b.something # not currently handled correctly # b.something # not currently handled correctly
end #$ use=getMember("Something").getMethod("withMixed").getReturn() end #$ source=Member[Something].Method[withMixed].ReturnValue

View File

@@ -0,0 +1,31 @@
def chained_access1
Something.foo [[[
'sink' # $ sink=Member[Something].Method[foo].Argument[0].Element[0].Element[0].Element[0]
]]]
end
def chained_access2
array = []
array[0] = [[
'sink' # $ sink=Member[Something].Method[foo].Argument[0].Element[0].Element[0].Element[0]
]]
Something.foo array
end
def chained_access3
array = [[]]
array[0][0] = [
'sink' # $ sink=Member[Something].Method[foo].Argument[0].Element[0].Element[0].Element[0]
]
Something.foo array
end
def chained_access4
Something.foo {
:one => {
:two => {
:three => 'sink' # $ sink=Member[Something].Method[foo].Argument[0].Element[:one].Element[:two].Element[:three]
}
}
}
end

View File

@@ -0,0 +1,64 @@
class BaseClass
def inheritedInstanceMethod
yield "taint" # $ sink=Member[Something].Method[foo].Argument[block].ReturnValue.Method[inheritedInstanceMethod].Parameter[block].Argument[0]
end
def self.inheritedSingletonMethod
yield "taint" # $ sink=Member[Something].Method[bar].Argument[block].ReturnValue.Method[inheritedSingletonMethod].Parameter[block].Argument[0]
end
end
class ClassWithCallbacks < BaseClass
def instanceMethod
yield "taint" # $ sink=Member[Something].Method[foo].Argument[block].ReturnValue.Method[instanceMethod].Parameter[block].Argument[0]
end
def self.singletonMethod
yield "bar" # $ sink=Member[Something].Method[bar].Argument[block].ReturnValue.Method[singletonMethod].Parameter[block].Argument[0]
end
def escapeSelf
Something.baz { self }
end
def self.escapeSingletonSelf
Something.baz { self }
end
def self.foo x
x # $ reachableFromSource=Member[BaseClass].Method[foo].Parameter[0]
x # $ reachableFromSource=Member[ClassWithCallbacks].Method[foo].Parameter[0]
x # $ reachableFromSource=Member[Subclass].Method[foo].Parameter[0]
end
def bar x
x # $ reachableFromSource=Member[BaseClass].Instance.Method[bar].Parameter[0]
x # $ reachableFromSource=Member[ClassWithCallbacks].Instance.Method[bar].Parameter[0]
x # $ reachableFromSource=Member[Subclass].Instance.Method[bar].Parameter[0]
end
end
class Subclass < ClassWithCallbacks
def instanceMethodInSubclass
yield "bar" # $ sink=Member[Something].Method[baz].Argument[block].ReturnValue.Method[instanceMethodInSubclass].Parameter[block].Argument[0]
end
def self.singletonMethodInSubclass
yield "bar" # $ sink=Member[Something].Method[baz].Argument[block].ReturnValue.Method[singletonMethodInSubclass].Parameter[block].Argument[0]
end
end
Something.foo { ClassWithCallbacks.new }
Something.bar { ClassWithCallbacks }
class ClassWithCallMethod
def call x
x # $ reachableFromSource=Method[topLevelMethod].Argument[0].Parameter[0]
"bar" # $ sink=Method[topLevelMethod].Argument[0].ReturnValue
end
end
topLevelMethod ClassWithCallMethod.new
blah = topLevelMethod
blah # $ reachableFromSource=Method[topLevelMethod].ReturnValue

View File

@@ -0,0 +1,10 @@
module SelfDotClass
module Mixin
def foo
self.class.bar # $ call=Member[Foo].Method[bar]
end
end
class Subclass < Foo
include Mixin
end
end

View File

@@ -1,34 +1,34 @@
MyModule #$ use=getMember("MyModule") MyModule #$ source=Member[MyModule]
print MyModule.foo #$ use=getMember("MyModule").getMethod("foo").getReturn() print MyModule.foo #$ source=Member[MyModule].Method[foo].ReturnValue
Kernel.print(e) #$ use=getMember("Kernel").getMethod("print").getReturn() def=getMember("Kernel").getMethod("print").getParameter(0) Kernel.print(e) #$ source=Member[Kernel].Method[print].ReturnValue sink=Member[Kernel].Method[print].Argument[0]
Object::Kernel #$ use=getMember("Kernel") Object::Kernel #$ source=Member[Kernel]
Object::Kernel.print(e) #$ use=getMember("Kernel").getMethod("print").getReturn() Object::Kernel.print(e) #$ source=Member[Kernel].Method[print].ReturnValue
begin begin
print MyModule.bar #$ use=getMember("MyModule").getMethod("bar").getReturn() print MyModule.bar #$ source=Member[MyModule].Method[bar].ReturnValue
raise AttributeError #$ use=getMember("AttributeError") raise AttributeError #$ source=Member[AttributeError]
rescue AttributeError => e #$ use=getMember("AttributeError") rescue AttributeError => e #$ source=Member[AttributeError]
Kernel.print(e) #$ use=getMember("Kernel").getMethod("print").getReturn() Kernel.print(e) #$ source=Member[Kernel].Method[print].ReturnValue
end end
Unknown.new.run #$ use=getMember("Unknown").getMethod("new").getReturn().getMethod("run").getReturn() Unknown.new.run #$ source=Member[Unknown].Method[new].ReturnValue.Method[run].ReturnValue
Foo::Bar::Baz #$ use=getMember("Foo").getMember("Bar").getMember("Baz") Foo::Bar::Baz #$ source=Member[Foo].Member[Bar].Member[Baz]
Const = [1, 2, 3] #$ use=getMember("Array").getMethod("[]").getReturn() Const = [1, 2, 3] #$ source=Member[Array].MethodBracket.ReturnValue
Const.each do |c| #$ use=getMember("Const") Const.each do |c| #$ source=Member[Const]
puts c #$ use=getMember("Const").getMethod("each").getBlock().getParameter(0) use=getMember("Const").getContent(element) puts c #$ reachableFromSource=Member[Const].Method[each].Argument[block].Parameter[0] reachableFromSource=Member[Const].Element[any]
end #$ use=getMember("Const").getMethod("each").getReturn() def=getMember("Const").getMethod("each").getBlock() end #$ source=Member[Const].Method[each].ReturnValue sink=Member[Const].Method[each].Argument[block]
foo = Foo #$ use=getMember("Foo") foo = Foo #$ source=Member[Foo]
foo::Bar::Baz #$ use=getMember("Foo").getMember("Bar").getMember("Baz") foo::Bar::Baz #$ source=Member[Foo].Member[Bar].Member[Baz]
FooAlias = Foo #$ use=getMember("Foo") FooAlias = Foo #$ source=Member[Foo]
FooAlias::Bar::Baz #$ use=getMember("Foo").getMember("Bar").getMember("Baz") FooAlias::Bar::Baz #$ source=Member[Foo].Member[Bar].Member[Baz] source=Member[FooAlias].Member[Bar].Member[Baz]
module Outer module Outer
module Inner module Inner
end end
end end
Outer::Inner.foo #$ use=getMember("Outer").getMember("Inner").getMethod("foo").getReturn() Outer::Inner.foo #$ source=Member[Outer].Member[Inner].Method[foo].ReturnValue
module M1 module M1
class C1 class C1
@@ -40,36 +40,36 @@ module M1
end end
end end
class C2 < M1::C1 #$ use=getMember("M1").getMember("C1") class C2 < M1::C1 #$ source=Member[M1].Member[C1]
end end
module M2 module M2
class C3 < M1::C1 #$ use=getMember("M1").getMember("C1") class C3 < M1::C1 #$ source=Member[M1].Member[C1]
end end
class C4 < C2 #$ use=getMember("C2") class C4 < C2 #$ source=Member[C2]
end end
end end
C2 #$ use=getMember("C2") use=getMember("M1").getMember("C1").getASubclass() C2 #$ source=Member[C2] reachableFromSource=Member[M1].Member[C1]
M2::C3 #$ use=getMember("M2").getMember("C3") use=getMember("M1").getMember("C1").getASubclass() M2::C3 #$ source=Member[M2].Member[C3] reachableFromSource=Member[M1].Member[C1]
M2::C4 #$ use=getMember("M2").getMember("C4") use=getMember("C2").getASubclass() use=getMember("M1").getMember("C1").getASubclass().getASubclass() M2::C4 #$ source=Member[M2].Member[C4] reachableFromSource=Member[C2] reachableFromSource=Member[M1].Member[C1]
M1::C1.m #$ use=getMember("M1").getMember("C1").getMethod("m").getReturn() M1::C1.m #$ source=Member[M1].Member[C1].Method[m].ReturnValue
M2::C3.m #$ use=getMember("M2").getMember("C3").getMethod("m").getReturn() use=getMember("M1").getMember("C1").getASubclass().getMethod("m").getReturn() M2::C3.m #$ source=Member[M2].Member[C3].Method[m].ReturnValue source=Member[M1].Member[C1].Method[m].ReturnValue
M1::C1.new.m #$ use=getMember("M1").getMember("C1").getMethod("new").getReturn().getMethod("m").getReturn() M1::C1.new.m #$ source=Member[M1].Member[C1].Method[new].ReturnValue.Method[m].ReturnValue
M2::C3.new.m #$ use=getMember("M2").getMember("C3").getMethod("new").getReturn().getMethod("m").getReturn() M2::C3.new.m #$ source=Member[M2].Member[C3].Method[new].ReturnValue.Method[m].ReturnValue
Foo.foo(a,b:c) #$ use=getMember("Foo").getMethod("foo").getReturn() def=getMember("Foo").getMethod("foo").getParameter(0) def=getMember("Foo").getMethod("foo").getKeywordParameter("b") Foo.foo(a,b:c) #$ source=Member[Foo].Method[foo].ReturnValue sink=Member[Foo].Method[foo].Argument[0] sink=Member[Foo].Method[foo].Argument[b:]
def userDefinedFunction(x, y) def userDefinedFunction(x, y)
x.noApiGraph(y) x.noApiGraph(y)
x.customEntryPointCall(y) #$ call=entryPoint("CustomEntryPointCall") use=entryPoint("CustomEntryPointCall").getReturn() rhs=entryPoint("CustomEntryPointCall").getParameter(0) x.customEntryPointCall(y) #$ call=EntryPoint[CustomEntryPointCall] source=EntryPoint[CustomEntryPointCall].ReturnValue sink=EntryPoint[CustomEntryPointCall].Parameter[0]
x.customEntryPointUse(y) #$ use=entryPoint("CustomEntryPointUse") x.customEntryPointUse(y) #$ source=EntryPoint[CustomEntryPointUse]
end end
array = [A::B::C] #$ use=getMember("Array").getMethod("[]").getReturn() array = [A::B::C] #$ source=Member[Array].MethodBracket.ReturnValue
array[0].m #$ use=getMember("A").getMember("B").getMember("C").getMethod("m").getReturn() array[0].m #$ source=Member[A].Member[B].Member[C].Method[m].ReturnValue source=Member[Array].MethodBracket.ReturnValue.Element[0].Method[m].ReturnValue
A::B::C[0] #$ use=getMember("A").getMember("B").getMember("C").getContent(element_0) A::B::C[0] #$ source=Member[A].Member[B].Member[C].Element[0]

View File

@@ -1,88 +0,0 @@
import codeql.ruby.AST
import codeql.ruby.DataFlow
import TestUtilities.InlineExpectationsTest
import codeql.ruby.ApiGraphs
class CustomEntryPointCall extends API::EntryPoint {
CustomEntryPointCall() { this = "CustomEntryPointCall" }
override DataFlow::CallNode getACall() { result.getMethodName() = "customEntryPointCall" }
}
class CustomEntryPointUse extends API::EntryPoint {
CustomEntryPointUse() { this = "CustomEntryPointUse" }
override DataFlow::LocalSourceNode getASource() {
result.(DataFlow::CallNode).getMethodName() = "customEntryPointUse"
}
}
module ApiUseTest implements TestSig {
string getARelevantTag() { result = ["use", "def", "call"] }
private predicate relevantNode(API::Node a, DataFlow::Node n, Location l, string tag) {
l = n.getLocation() and
(
tag = "use" and
n = a.getAValueReachableFromSource()
or
tag = "def" and
n = a.asSink()
or
tag = "call" and
n = a.(API::MethodAccessNode).getCallNode()
)
}
predicate hasActualResult(Location location, string element, string tag, string value) {
tag = "use" and // def tags are always optional
exists(DataFlow::Node n | relevantNode(_, n, location, tag) |
// Only report the longest path on this line:
value =
max(API::Node a2, Location l2, DataFlow::Node n2 |
relevantNode(a2, n2, l2, tag) and
l2.getFile() = location.getFile() and
l2.getEndLine() = location.getEndLine()
|
a2.getPath()
order by
size(n2.asExpr().getExpr()), a2.getPath().length() desc, a2.getPath() desc
) and
element = n.toString()
)
}
// We also permit optional annotations for any other path on the line.
// This is used to test subclass paths, which typically have a shorter canonical path.
predicate hasOptionalResult(Location location, string element, string tag, string value) {
exists(API::Node a, DataFlow::Node n | relevantNode(a, n, location, tag) |
element = n.toString() and
value = getAPath(a, _)
)
}
}
import MakeTest<ApiUseTest>
private int size(AstNode n) { not n instanceof StmtSequence and result = count(n.getAChild*()) }
/**
* Gets a path of the given `length` from the root to the given node.
* This is a copy of `API::getAPath()` without the restriction on path length,
* which would otherwise rule out paths involving `getASubclass()`.
*/
string getAPath(API::Node node, int length) {
node instanceof API::Root and
length = 0 and
result = ""
or
exists(API::Node pred, API::Label::ApiLabel lbl, string predpath |
pred.getASuccessor(lbl) = node and
predpath = getAPath(pred, length - 1) and
exists(string dot | if length = 1 then dot = "" else dot = "." |
result = predpath + dot + lbl and
// avoid producing strings longer than 1MB
result.length() < 1000 * 1000
)
)
}

View File

@@ -1,6 +1,8 @@
sourceTest sourceTest
| hello_world_server.rb:8:13:8:15 | req | | hello_world_server.rb:8:13:8:15 | req |
| hello_world_server.rb:32:18:32:20 | req |
ssrfSinkTest ssrfSinkTest
| hello_world_client.rb:6:47:6:75 | "http://localhost:8080/twirp" | | hello_world_client.rb:6:47:6:75 | "http://localhost:8080/twirp" |
serviceInstantiationTest serviceInstantiationTest
| hello_world_server.rb:24:11:24:61 | call to new | | hello_world_server.rb:24:11:24:61 | call to new |
| hello_world_server.rb:38:1:38:57 | call to new |

View File

@@ -5,4 +5,4 @@ query predicate sourceTest(Twirp::UnmarshaledParameter source) { any() }
query predicate ssrfSinkTest(Twirp::ServiceUrlAsSsrfSink sink) { any() } query predicate ssrfSinkTest(Twirp::ServiceUrlAsSsrfSink sink) { any() }
query predicate serviceInstantiationTest(Twirp::ServiceInstantiation si) { any() } deprecated query predicate serviceInstantiationTest(Twirp::ServiceInstantiation si) { any() }

View File

@@ -5,7 +5,7 @@ require_relative 'hello_world/service_twirp.rb'
class HelloWorldHandler class HelloWorldHandler
# test: request # test: request
def hello(req, env) def hello(req, env)
puts ">> Hello #{req.name}" puts ">> Hello #{req.name}"
{message: "Hello #{req.name}"} {message: "Hello #{req.name}"}
end end
@@ -13,7 +13,7 @@ end
class FakeHelloWorldHandler class FakeHelloWorldHandler
# test: !request # test: !request
def hello(req, env) def hello(req, env)
puts ">> Hello #{req.name}" puts ">> Hello #{req.name}"
{message: "Hello #{req.name}"} {message: "Hello #{req.name}"}
end end
@@ -21,9 +21,18 @@ end
handler = HelloWorldHandler.new() handler = HelloWorldHandler.new()
# test: serviceInstantiation # test: serviceInstantiation
service = Example::HelloWorld::HelloWorldService.new(handler) service = Example::HelloWorld::HelloWorldService.new(handler)
path_prefix = "/twirp/" + service.full_name path_prefix = "/twirp/" + service.full_name
server = WEBrick::HTTPServer.new(Port: 8080) server = WEBrick::HTTPServer.new(Port: 8080)
server.mount path_prefix, Rack::Handler::WEBrick, service server.mount path_prefix, Rack::Handler::WEBrick, service
server.start server.start
class StaticHandler
def self.hello(req, env)
puts ">> Hello #{req.name}"
{message: "Hello #{req.name}"}
end
end
Example::HelloWorld::HelloWorldService.new(StaticHandler)

View File

@@ -55,12 +55,12 @@ underscore
| LotsOfCapitalLetters | lots_of_capital_letters | | LotsOfCapitalLetters | lots_of_capital_letters |
| invalid | invalid | | invalid | invalid |
mimeTypeInstances mimeTypeInstances
| mime_type.rb:2:6:2:28 | Use getMember("Mime").getContent(element_text/html) | | mime_type.rb:2:6:2:28 | ForwardNode(call to fetch) |
| mime_type.rb:3:6:3:32 | Use getMember("Mime").getMember("Type").getMethod("new").getReturn() | | mime_type.rb:3:6:3:32 | ForwardNode(call to new) |
| mime_type.rb:4:6:4:35 | Use getMember("Mime").getMember("Type").getMethod("lookup").getReturn() | | mime_type.rb:4:6:4:35 | ForwardNode(call to lookup) |
| mime_type.rb:5:6:5:43 | Use getMember("Mime").getMember("Type").getMethod("lookup_by_extension").getReturn() | | mime_type.rb:5:6:5:43 | ForwardNode(call to lookup_by_extension) |
| mime_type.rb:6:6:6:47 | Use getMember("Mime").getMember("Type").getMethod("register").getReturn() | | mime_type.rb:6:6:6:47 | ForwardNode(call to register) |
| mime_type.rb:7:6:7:64 | Use getMember("Mime").getMember("Type").getMethod("register_alias").getReturn() | | mime_type.rb:7:6:7:64 | ForwardNode(call to register_alias) |
mimeTypeMatchRegExpInterpretations mimeTypeMatchRegExpInterpretations
| mime_type.rb:11:11:11:19 | "foo/bar" | | mime_type.rb:11:11:11:19 | "foo/bar" |
| mime_type.rb:12:7:12:15 | "foo/bar" | | mime_type.rb:12:7:12:15 | "foo/bar" |

View File

@@ -10,6 +10,7 @@ activeRecordInstances
| ActiveRecord.rb:9:5:9:68 | call to find | | ActiveRecord.rb:9:5:9:68 | call to find |
| ActiveRecord.rb:13:5:13:40 | call to find_by | | ActiveRecord.rb:13:5:13:40 | call to find_by |
| ActiveRecord.rb:13:5:13:46 | call to users | | ActiveRecord.rb:13:5:13:46 | call to users |
| ActiveRecord.rb:35:5:35:51 | call to authenticate |
| ActiveRecord.rb:36:5:36:30 | call to find_by_name | | ActiveRecord.rb:36:5:36:30 | call to find_by_name |
| ActiveRecord.rb:55:5:57:7 | if ... | | ActiveRecord.rb:55:5:57:7 | if ... |
| ActiveRecord.rb:55:43:56:40 | then ... | | ActiveRecord.rb:55:43:56:40 | then ... |
@@ -107,12 +108,14 @@ activeRecordSqlExecutionRanges
| ActiveRecord.rb:19:16:19:24 | condition | | ActiveRecord.rb:19:16:19:24 | condition |
| ActiveRecord.rb:28:30:28:44 | ...[...] | | ActiveRecord.rb:28:30:28:44 | ...[...] |
| ActiveRecord.rb:29:20:29:42 | "id = '#{...}'" | | ActiveRecord.rb:29:20:29:42 | "id = '#{...}'" |
| ActiveRecord.rb:30:21:30:45 | call to [] |
| ActiveRecord.rb:30:22:30:44 | "id = '#{...}'" | | ActiveRecord.rb:30:22:30:44 | "id = '#{...}'" |
| ActiveRecord.rb:31:16:31:21 | <<-SQL | | ActiveRecord.rb:31:16:31:21 | <<-SQL |
| ActiveRecord.rb:34:20:34:47 | "user.id = '#{...}'" | | ActiveRecord.rb:34:20:34:47 | "user.id = '#{...}'" |
| ActiveRecord.rb:46:20:46:32 | ... + ... | | ActiveRecord.rb:46:20:46:32 | ... + ... |
| ActiveRecord.rb:52:16:52:28 | "name #{...}" | | ActiveRecord.rb:52:16:52:28 | "name #{...}" |
| ActiveRecord.rb:56:20:56:39 | "username = #{...}" | | ActiveRecord.rb:56:20:56:39 | "username = #{...}" |
| ActiveRecord.rb:68:21:68:44 | ...[...] |
| ActiveRecord.rb:106:27:106:76 | "this is an unsafe annotation:..." | | ActiveRecord.rb:106:27:106:76 | "this is an unsafe annotation:..." |
activeRecordModelClassMethodCalls activeRecordModelClassMethodCalls
| ActiveRecord.rb:2:3:2:17 | call to has_many | | ActiveRecord.rb:2:3:2:17 | call to has_many |
@@ -127,7 +130,6 @@ activeRecordModelClassMethodCalls
| ActiveRecord.rb:31:5:31:35 | call to where | | ActiveRecord.rb:31:5:31:35 | call to where |
| ActiveRecord.rb:34:5:34:14 | call to where | | ActiveRecord.rb:34:5:34:14 | call to where |
| ActiveRecord.rb:34:5:34:48 | call to not | | ActiveRecord.rb:34:5:34:48 | call to not |
| ActiveRecord.rb:35:5:35:51 | call to authenticate |
| ActiveRecord.rb:36:5:36:30 | call to find_by_name | | ActiveRecord.rb:36:5:36:30 | call to find_by_name |
| ActiveRecord.rb:37:5:37:36 | call to not_a_find_by_method | | ActiveRecord.rb:37:5:37:36 | call to not_a_find_by_method |
| ActiveRecord.rb:46:5:46:33 | call to delete_by | | ActiveRecord.rb:46:5:46:33 | call to delete_by |
@@ -135,7 +137,6 @@ activeRecordModelClassMethodCalls
| ActiveRecord.rb:56:7:56:40 | call to find_by | | ActiveRecord.rb:56:7:56:40 | call to find_by |
| ActiveRecord.rb:60:5:60:33 | call to find_by | | ActiveRecord.rb:60:5:60:33 | call to find_by |
| ActiveRecord.rb:62:5:62:34 | call to find | | ActiveRecord.rb:62:5:62:34 | call to find |
| ActiveRecord.rb:68:5:68:45 | call to delete_by |
| ActiveRecord.rb:72:5:72:24 | call to create | | ActiveRecord.rb:72:5:72:24 | call to create |
| ActiveRecord.rb:76:5:76:66 | call to create | | ActiveRecord.rb:76:5:76:66 | call to create |
| ActiveRecord.rb:80:5:80:68 | call to create | | ActiveRecord.rb:80:5:80:68 | call to create |
@@ -152,6 +153,96 @@ activeRecordModelClassMethodCalls
| associations.rb:12:3:12:32 | call to has_and_belongs_to_many | | associations.rb:12:3:12:32 | call to has_and_belongs_to_many |
| associations.rb:16:3:16:18 | call to belongs_to | | associations.rb:16:3:16:18 | call to belongs_to |
| associations.rb:19:11:19:20 | call to new | | associations.rb:19:11:19:20 | call to new |
| associations.rb:21:9:21:21 | call to posts |
| associations.rb:21:9:21:28 | call to create |
| associations.rb:23:12:23:25 | call to comments |
| associations.rb:23:12:23:32 | call to create |
| associations.rb:25:11:25:22 | call to author |
| associations.rb:27:9:27:21 | call to posts |
| associations.rb:27:9:27:28 | call to create |
| associations.rb:29:1:29:13 | call to posts |
| associations.rb:29:1:29:22 | ... << ... |
| associations.rb:31:1:31:12 | call to author= |
| associations.rb:35:1:35:14 | call to comments |
| associations.rb:35:1:35:21 | call to create |
| associations.rb:35:1:35:28 | call to create |
| associations.rb:37:1:37:13 | call to posts |
| associations.rb:37:1:37:20 | call to reload |
| associations.rb:37:1:37:27 | call to create |
| associations.rb:39:1:39:15 | call to build_tag |
| associations.rb:40:1:40:15 | call to build_tag |
| associations.rb:42:1:42:13 | call to posts |
| associations.rb:42:1:42:25 | call to push |
| associations.rb:43:1:43:13 | call to posts |
| associations.rb:43:1:43:27 | call to concat |
| associations.rb:44:1:44:13 | call to posts |
| associations.rb:44:1:44:19 | call to build |
| associations.rb:45:1:45:13 | call to posts |
| associations.rb:45:1:45:20 | call to create |
| associations.rb:46:1:46:13 | call to posts |
| associations.rb:46:1:46:21 | call to create! |
| associations.rb:47:1:47:13 | call to posts |
| associations.rb:47:1:47:20 | call to delete |
| associations.rb:48:1:48:13 | call to posts |
| associations.rb:48:1:48:24 | call to delete_all |
| associations.rb:49:1:49:13 | call to posts |
| associations.rb:49:1:49:21 | call to destroy |
| associations.rb:50:1:50:13 | call to posts |
| associations.rb:50:1:50:25 | call to destroy_all |
| associations.rb:51:1:51:13 | call to posts |
| associations.rb:51:1:51:22 | call to distinct |
| associations.rb:51:1:51:36 | call to find |
| associations.rb:52:1:52:13 | call to posts |
| associations.rb:52:1:52:19 | call to reset |
| associations.rb:52:1:52:33 | call to find |
| associations.rb:53:1:53:13 | call to posts |
| associations.rb:53:1:53:20 | call to reload |
| associations.rb:53:1:53:34 | call to find |
activeRecordModelClassMethodCallsReplacement
| ActiveRecord.rb:1:1:3:3 | UserGroup | ActiveRecord.rb:2:3:2:17 | call to has_many |
| ActiveRecord.rb:1:1:3:3 | UserGroup | ActiveRecord.rb:13:5:13:40 | call to find_by |
| ActiveRecord.rb:5:1:15:3 | User | ActiveRecord.rb:6:3:6:24 | call to belongs_to |
| ActiveRecord.rb:5:1:15:3 | User | ActiveRecord.rb:9:5:9:68 | call to find |
| ActiveRecord.rb:5:1:15:3 | User | ActiveRecord.rb:19:5:19:25 | call to destroy_by |
| ActiveRecord.rb:5:1:15:3 | User | ActiveRecord.rb:28:5:28:45 | call to calculate |
| ActiveRecord.rb:5:1:15:3 | User | ActiveRecord.rb:29:5:29:43 | call to delete_by |
| ActiveRecord.rb:5:1:15:3 | User | ActiveRecord.rb:30:5:30:46 | call to destroy_by |
| ActiveRecord.rb:5:1:15:3 | User | ActiveRecord.rb:31:5:31:35 | call to where |
| ActiveRecord.rb:5:1:15:3 | User | ActiveRecord.rb:34:5:34:14 | call to where |
| ActiveRecord.rb:5:1:15:3 | User | ActiveRecord.rb:35:5:35:51 | call to authenticate |
| ActiveRecord.rb:5:1:15:3 | User | ActiveRecord.rb:36:5:36:30 | call to find_by_name |
| ActiveRecord.rb:5:1:15:3 | User | ActiveRecord.rb:37:5:37:36 | call to not_a_find_by_method |
| ActiveRecord.rb:5:1:15:3 | User | ActiveRecord.rb:46:5:46:33 | call to delete_by |
| ActiveRecord.rb:5:1:15:3 | User | ActiveRecord.rb:52:5:52:29 | call to order |
| ActiveRecord.rb:5:1:15:3 | User | ActiveRecord.rb:56:7:56:40 | call to find_by |
| ActiveRecord.rb:5:1:15:3 | User | ActiveRecord.rb:60:5:60:33 | call to find_by |
| ActiveRecord.rb:5:1:15:3 | User | ActiveRecord.rb:62:5:62:34 | call to find |
| ActiveRecord.rb:5:1:15:3 | User | ActiveRecord.rb:68:5:68:45 | call to delete_by |
| ActiveRecord.rb:5:1:15:3 | User | ActiveRecord.rb:72:5:72:24 | call to create |
| ActiveRecord.rb:5:1:15:3 | User | ActiveRecord.rb:76:5:76:66 | call to create |
| ActiveRecord.rb:5:1:15:3 | User | ActiveRecord.rb:80:5:80:68 | call to create |
| ActiveRecord.rb:5:1:15:3 | User | ActiveRecord.rb:84:5:84:16 | call to create |
| ActiveRecord.rb:5:1:15:3 | User | ActiveRecord.rb:88:5:88:27 | call to update |
| ActiveRecord.rb:5:1:15:3 | User | ActiveRecord.rb:92:5:92:69 | call to update |
| ActiveRecord.rb:5:1:15:3 | User | ActiveRecord.rb:96:5:96:71 | call to update |
| ActiveRecord.rb:5:1:15:3 | User | ActiveRecord.rb:102:13:102:54 | call to annotate |
| ActiveRecord.rb:5:1:15:3 | User | ActiveRecord.rb:106:13:106:77 | call to annotate |
| ActiveRecord.rb:17:1:21:3 | Admin | ActiveRecord.rb:19:5:19:25 | call to destroy_by |
| ActiveRecord.rb:17:1:21:3 | Admin | ActiveRecord.rb:68:5:68:45 | call to delete_by |
| ActiveRecord.rb:17:1:21:3 | Admin | ActiveRecord.rb:72:5:72:24 | call to create |
| ActiveRecord.rb:17:1:21:3 | Admin | ActiveRecord.rb:76:5:76:66 | call to create |
| ActiveRecord.rb:17:1:21:3 | Admin | ActiveRecord.rb:80:5:80:68 | call to create |
| ActiveRecord.rb:17:1:21:3 | Admin | ActiveRecord.rb:84:5:84:16 | call to create |
| ActiveRecord.rb:17:1:21:3 | Admin | ActiveRecord.rb:88:5:88:27 | call to update |
| ActiveRecord.rb:17:1:21:3 | Admin | ActiveRecord.rb:92:5:92:69 | call to update |
| ActiveRecord.rb:17:1:21:3 | Admin | ActiveRecord.rb:96:5:96:71 | call to update |
| associations.rb:1:1:3:3 | Author | associations.rb:2:3:2:17 | call to has_many |
| associations.rb:1:1:3:3 | Author | associations.rb:19:11:19:20 | call to new |
| associations.rb:5:1:9:3 | Post | associations.rb:6:3:6:20 | call to belongs_to |
| associations.rb:5:1:9:3 | Post | associations.rb:7:3:7:20 | call to has_many |
| associations.rb:5:1:9:3 | Post | associations.rb:8:3:8:31 | call to has_and_belongs_to_many |
| associations.rb:11:1:13:3 | Tag | associations.rb:12:3:12:32 | call to has_and_belongs_to_many |
| associations.rb:15:1:17:3 | Comment | associations.rb:16:3:16:18 | call to belongs_to |
potentiallyUnsafeSqlExecutingMethodCall potentiallyUnsafeSqlExecutingMethodCall
| ActiveRecord.rb:9:5:9:68 | call to find | | ActiveRecord.rb:9:5:9:68 | call to find |
| ActiveRecord.rb:19:5:19:25 | call to destroy_by | | ActiveRecord.rb:19:5:19:25 | call to destroy_by |

View File

@@ -9,9 +9,19 @@ query predicate activeRecordInstances(ActiveRecordInstance i) { any() }
query predicate activeRecordSqlExecutionRanges(ActiveRecordSqlExecutionRange range) { any() } query predicate activeRecordSqlExecutionRanges(ActiveRecordSqlExecutionRange range) { any() }
query predicate activeRecordModelClassMethodCalls(ActiveRecordModelClassMethodCall call) { any() } deprecated query predicate activeRecordModelClassMethodCalls(ActiveRecordModelClassMethodCall call) {
any()
}
query predicate potentiallyUnsafeSqlExecutingMethodCall(PotentiallyUnsafeSqlExecutingMethodCall call) { query predicate activeRecordModelClassMethodCallsReplacement(
ActiveRecordModelClass cls, DataFlow::CallNode call
) {
call = cls.getClassNode().trackModule().getAMethodCall(_)
}
deprecated query predicate potentiallyUnsafeSqlExecutingMethodCall(
PotentiallyUnsafeSqlExecutingMethodCall call
) {
any() any()
} }

View File

@@ -33,6 +33,13 @@ modelInstances
| active_resource.rb:26:9:26:14 | people | | active_resource.rb:26:9:26:14 | people |
| active_resource.rb:26:9:26:20 | call to first | | active_resource.rb:26:9:26:20 | call to first |
| active_resource.rb:27:1:27:5 | alice | | active_resource.rb:27:1:27:5 | alice |
modelInstancesAsSource
| active_resource.rb:1:1:3:3 | Person | active_resource.rb:5:9:5:33 | call to new |
| active_resource.rb:1:1:3:3 | Person | active_resource.rb:8:9:8:22 | call to find |
| active_resource.rb:1:1:3:3 | Person | active_resource.rb:16:1:16:23 | call to new |
| active_resource.rb:1:1:3:3 | Person | active_resource.rb:18:1:18:22 | call to get |
| active_resource.rb:1:1:3:3 | Person | active_resource.rb:24:10:24:26 | call to find |
| active_resource.rb:1:1:3:3 | Person | active_resource.rb:26:9:26:20 | call to first |
modelInstanceMethodCalls modelInstanceMethodCalls
| active_resource.rb:6:1:6:10 | call to save | | active_resource.rb:6:1:6:10 | call to save |
| active_resource.rb:9:1:9:13 | call to address= | | active_resource.rb:9:1:9:13 | call to address= |
@@ -50,3 +57,6 @@ collections
| active_resource.rb:24:1:24:26 | ... = ... | | active_resource.rb:24:1:24:26 | ... = ... |
| active_resource.rb:24:10:24:26 | call to find | | active_resource.rb:24:10:24:26 | call to find |
| active_resource.rb:26:9:26:14 | people | | active_resource.rb:26:9:26:14 | people |
collectionSources
| active_resource.rb:23:10:23:19 | call to all |
| active_resource.rb:24:10:24:26 | call to find |

View File

@@ -3,7 +3,8 @@ import codeql.ruby.DataFlow
import codeql.ruby.frameworks.ActiveResource import codeql.ruby.frameworks.ActiveResource
query predicate modelClasses( query predicate modelClasses(
ActiveResource::ModelClass c, DataFlow::Node siteAssignCall, boolean disablesCertificateValidation ActiveResource::ModelClassNode c, DataFlow::Node siteAssignCall,
boolean disablesCertificateValidation
) { ) {
c.getASiteAssignment() = siteAssignCall and c.getASiteAssignment() = siteAssignCall and
if c.disablesCertificateValidation(siteAssignCall) if c.disablesCertificateValidation(siteAssignCall)
@@ -13,8 +14,16 @@ query predicate modelClasses(
query predicate modelClassMethodCalls(ActiveResource::ModelClassMethodCall c) { any() } query predicate modelClassMethodCalls(ActiveResource::ModelClassMethodCall c) { any() }
query predicate modelInstances(ActiveResource::ModelInstance c) { any() } deprecated query predicate modelInstances(ActiveResource::ModelInstance c) { any() }
query predicate modelInstancesAsSource(
ActiveResource::ModelClassNode cls, DataFlow::LocalSourceNode node
) {
node = cls.getAnInstanceReference().asSource()
}
query predicate modelInstanceMethodCalls(ActiveResource::ModelInstanceMethodCall c) { any() } query predicate modelInstanceMethodCalls(ActiveResource::ModelInstanceMethodCall c) { any() }
query predicate collections(ActiveResource::Collection c) { any() } deprecated query predicate collections(ActiveResource::Collection c) { any() }
query predicate collectionSources(ActiveResource::CollectionSource c) { any() }

View File

@@ -3,7 +3,6 @@ edges
| app/controllers/foo/stores_controller.rb:8:5:8:6 | dt | app/controllers/foo/stores_controller.rb:13:55:13:56 | dt | | app/controllers/foo/stores_controller.rb:8:5:8:6 | dt | app/controllers/foo/stores_controller.rb:13:55:13:56 | dt |
| app/controllers/foo/stores_controller.rb:8:10:8:29 | call to read | app/controllers/foo/stores_controller.rb:8:5:8:6 | dt | | app/controllers/foo/stores_controller.rb:8:10:8:29 | call to read | app/controllers/foo/stores_controller.rb:8:5:8:6 | dt |
| app/controllers/foo/stores_controller.rb:9:22:9:23 | dt | app/views/foo/stores/show.html.erb:37:3:37:16 | @instance_text | | app/controllers/foo/stores_controller.rb:9:22:9:23 | dt | app/views/foo/stores/show.html.erb:37:3:37:16 | @instance_text |
| app/controllers/foo/stores_controller.rb:12:28:12:48 | call to raw_name | app/views/foo/stores/show.html.erb:82:5:82:24 | @other_user_raw_name |
| app/controllers/foo/stores_controller.rb:13:55:13:56 | dt | app/views/foo/stores/show.html.erb:2:9:2:20 | call to display_text | | app/controllers/foo/stores_controller.rb:13:55:13:56 | dt | app/views/foo/stores/show.html.erb:2:9:2:20 | call to display_text |
| app/controllers/foo/stores_controller.rb:13:55:13:56 | dt | app/views/foo/stores/show.html.erb:5:9:5:21 | call to local_assigns [element :display_text] | | app/controllers/foo/stores_controller.rb:13:55:13:56 | dt | app/views/foo/stores/show.html.erb:5:9:5:21 | call to local_assigns [element :display_text] |
| app/controllers/foo/stores_controller.rb:13:55:13:56 | dt | app/views/foo/stores/show.html.erb:9:9:9:21 | call to local_assigns [element :display_text] | | app/controllers/foo/stores_controller.rb:13:55:13:56 | dt | app/views/foo/stores/show.html.erb:9:9:9:21 | call to local_assigns [element :display_text] |
@@ -22,7 +21,6 @@ nodes
| app/controllers/foo/stores_controller.rb:8:5:8:6 | dt | semmle.label | dt | | app/controllers/foo/stores_controller.rb:8:5:8:6 | dt | semmle.label | dt |
| app/controllers/foo/stores_controller.rb:8:10:8:29 | call to read | semmle.label | call to read | | app/controllers/foo/stores_controller.rb:8:10:8:29 | call to read | semmle.label | call to read |
| app/controllers/foo/stores_controller.rb:9:22:9:23 | dt | semmle.label | dt | | app/controllers/foo/stores_controller.rb:9:22:9:23 | dt | semmle.label | dt |
| app/controllers/foo/stores_controller.rb:12:28:12:48 | call to raw_name | semmle.label | call to raw_name |
| app/controllers/foo/stores_controller.rb:13:55:13:56 | dt | semmle.label | dt | | app/controllers/foo/stores_controller.rb:13:55:13:56 | dt | semmle.label | dt |
| app/views/foo/bars/_widget.html.erb:5:9:5:20 | call to display_text | semmle.label | call to display_text | | app/views/foo/bars/_widget.html.erb:5:9:5:20 | call to display_text | semmle.label | call to display_text |
| app/views/foo/bars/_widget.html.erb:8:9:8:21 | call to local_assigns [element :display_text] | semmle.label | call to local_assigns [element :display_text] | | app/views/foo/bars/_widget.html.erb:8:9:8:21 | call to local_assigns [element :display_text] | semmle.label | call to local_assigns [element :display_text] |
@@ -39,11 +37,7 @@ nodes
| app/views/foo/stores/show.html.erb:40:64:40:87 | ... + ... | semmle.label | ... + ... | | app/views/foo/stores/show.html.erb:40:64:40:87 | ... + ... | semmle.label | ... + ... |
| app/views/foo/stores/show.html.erb:40:76:40:87 | call to display_text | semmle.label | call to display_text | | app/views/foo/stores/show.html.erb:40:76:40:87 | call to display_text | semmle.label | call to display_text |
| app/views/foo/stores/show.html.erb:46:5:46:16 | call to handle | semmle.label | call to handle | | app/views/foo/stores/show.html.erb:46:5:46:16 | call to handle | semmle.label | call to handle |
| app/views/foo/stores/show.html.erb:49:5:49:18 | call to raw_name | semmle.label | call to raw_name |
| app/views/foo/stores/show.html.erb:63:3:63:18 | call to handle | semmle.label | call to handle | | app/views/foo/stores/show.html.erb:63:3:63:18 | call to handle | semmle.label | call to handle |
| app/views/foo/stores/show.html.erb:69:3:69:20 | call to raw_name | semmle.label | call to raw_name |
| app/views/foo/stores/show.html.erb:79:5:79:22 | call to display_name | semmle.label | call to display_name |
| app/views/foo/stores/show.html.erb:82:5:82:24 | @other_user_raw_name | semmle.label | @other_user_raw_name |
| app/views/foo/stores/show.html.erb:86:3:86:29 | call to sprintf | semmle.label | call to sprintf | | app/views/foo/stores/show.html.erb:86:3:86:29 | call to sprintf | semmle.label | call to sprintf |
| app/views/foo/stores/show.html.erb:86:17:86:28 | call to handle | semmle.label | call to handle | | app/views/foo/stores/show.html.erb:86:17:86:28 | call to handle | semmle.label | call to handle |
subpaths subpaths
@@ -57,9 +51,5 @@ subpaths
| app/views/foo/stores/show.html.erb:32:3:32:14 | call to display_text | app/controllers/foo/stores_controller.rb:8:10:8:29 | call to read | app/views/foo/stores/show.html.erb:32:3:32:14 | call to display_text | Stored cross-site scripting vulnerability due to $@. | app/controllers/foo/stores_controller.rb:8:10:8:29 | call to read | stored value | | app/views/foo/stores/show.html.erb:32:3:32:14 | call to display_text | app/controllers/foo/stores_controller.rb:8:10:8:29 | call to read | app/views/foo/stores/show.html.erb:32:3:32:14 | call to display_text | Stored cross-site scripting vulnerability due to $@. | app/controllers/foo/stores_controller.rb:8:10:8:29 | call to read | stored value |
| app/views/foo/stores/show.html.erb:37:3:37:16 | @instance_text | app/controllers/foo/stores_controller.rb:8:10:8:29 | call to read | app/views/foo/stores/show.html.erb:37:3:37:16 | @instance_text | Stored cross-site scripting vulnerability due to $@. | app/controllers/foo/stores_controller.rb:8:10:8:29 | call to read | stored value | | app/views/foo/stores/show.html.erb:37:3:37:16 | @instance_text | app/controllers/foo/stores_controller.rb:8:10:8:29 | call to read | app/views/foo/stores/show.html.erb:37:3:37:16 | @instance_text | Stored cross-site scripting vulnerability due to $@. | app/controllers/foo/stores_controller.rb:8:10:8:29 | call to read | stored value |
| app/views/foo/stores/show.html.erb:46:5:46:16 | call to handle | app/views/foo/stores/show.html.erb:46:5:46:16 | call to handle | app/views/foo/stores/show.html.erb:46:5:46:16 | call to handle | Stored cross-site scripting vulnerability due to $@. | app/views/foo/stores/show.html.erb:46:5:46:16 | call to handle | stored value | | app/views/foo/stores/show.html.erb:46:5:46:16 | call to handle | app/views/foo/stores/show.html.erb:46:5:46:16 | call to handle | app/views/foo/stores/show.html.erb:46:5:46:16 | call to handle | Stored cross-site scripting vulnerability due to $@. | app/views/foo/stores/show.html.erb:46:5:46:16 | call to handle | stored value |
| app/views/foo/stores/show.html.erb:49:5:49:18 | call to raw_name | app/views/foo/stores/show.html.erb:49:5:49:18 | call to raw_name | app/views/foo/stores/show.html.erb:49:5:49:18 | call to raw_name | Stored cross-site scripting vulnerability due to $@. | app/views/foo/stores/show.html.erb:49:5:49:18 | call to raw_name | stored value |
| app/views/foo/stores/show.html.erb:63:3:63:18 | call to handle | app/views/foo/stores/show.html.erb:63:3:63:18 | call to handle | app/views/foo/stores/show.html.erb:63:3:63:18 | call to handle | Stored cross-site scripting vulnerability due to $@. | app/views/foo/stores/show.html.erb:63:3:63:18 | call to handle | stored value | | app/views/foo/stores/show.html.erb:63:3:63:18 | call to handle | app/views/foo/stores/show.html.erb:63:3:63:18 | call to handle | app/views/foo/stores/show.html.erb:63:3:63:18 | call to handle | Stored cross-site scripting vulnerability due to $@. | app/views/foo/stores/show.html.erb:63:3:63:18 | call to handle | stored value |
| app/views/foo/stores/show.html.erb:69:3:69:20 | call to raw_name | app/views/foo/stores/show.html.erb:69:3:69:20 | call to raw_name | app/views/foo/stores/show.html.erb:69:3:69:20 | call to raw_name | Stored cross-site scripting vulnerability due to $@. | app/views/foo/stores/show.html.erb:69:3:69:20 | call to raw_name | stored value |
| app/views/foo/stores/show.html.erb:79:5:79:22 | call to display_name | app/views/foo/stores/show.html.erb:79:5:79:22 | call to display_name | app/views/foo/stores/show.html.erb:79:5:79:22 | call to display_name | Stored cross-site scripting vulnerability due to $@. | app/views/foo/stores/show.html.erb:79:5:79:22 | call to display_name | stored value |
| app/views/foo/stores/show.html.erb:82:5:82:24 | @other_user_raw_name | app/controllers/foo/stores_controller.rb:12:28:12:48 | call to raw_name | app/views/foo/stores/show.html.erb:82:5:82:24 | @other_user_raw_name | Stored cross-site scripting vulnerability due to $@. | app/controllers/foo/stores_controller.rb:12:28:12:48 | call to raw_name | stored value |
| app/views/foo/stores/show.html.erb:86:3:86:29 | call to sprintf | app/views/foo/stores/show.html.erb:86:17:86:28 | call to handle | app/views/foo/stores/show.html.erb:86:3:86:29 | call to sprintf | Stored cross-site scripting vulnerability due to $@. | app/views/foo/stores/show.html.erb:86:17:86:28 | call to handle | stored value | | app/views/foo/stores/show.html.erb:86:3:86:29 | call to sprintf | app/views/foo/stores/show.html.erb:86:17:86:28 | call to handle | app/views/foo/stores/show.html.erb:86:3:86:29 | call to sprintf | Stored cross-site scripting vulnerability due to $@. | app/views/foo/stores/show.html.erb:86:17:86:28 | call to handle | stored value |

View File

@@ -63,7 +63,7 @@
some_user.handle.html_safe some_user.handle.html_safe
%> %>
<%# BAD: Indirect to a database value without escaping %> <%# BAD: Indirect to a database value without escaping (currently missed due to lack of 'self' handling in ORM tracking) %>
<%= <%=
some_user = User.find 1 some_user = User.find 1
some_user.raw_name.html_safe some_user.raw_name.html_safe
@@ -75,10 +75,10 @@
some_user.handle some_user.handle
%> %>
<%# BAD: Indirect to a database value without escaping %> <%# BAD: Indirect to a database value without escaping (currently missed due to lack of 'self' handling in ORM tracking) %>
<%= @user.display_name.html_safe %> <%= @user.display_name.html_safe %>
<%# BAD: Indirect to a database value without escaping %> <%# BAD: Indirect to a database value without escaping (currently missed due to lack of 'self' handling in ORM tracking) %>
<%= @other_user_raw_name.html_safe %> <%= @other_user_raw_name.html_safe %>
<%# BAD: Kernel.sprintf is a taint-step %> <%# BAD: Kernel.sprintf is a taint-step %>

View File

@@ -22,6 +22,7 @@ edges
| ActiveRecordInjection.rb:70:38:70:50 | ...[...] | ActiveRecordInjection.rb:8:31:8:34 | pass | | ActiveRecordInjection.rb:70:38:70:50 | ...[...] | ActiveRecordInjection.rb:8:31:8:34 | pass |
| ActiveRecordInjection.rb:74:41:74:46 | call to params | ActiveRecordInjection.rb:74:41:74:51 | ...[...] | | ActiveRecordInjection.rb:74:41:74:46 | call to params | ActiveRecordInjection.rb:74:41:74:51 | ...[...] |
| ActiveRecordInjection.rb:74:41:74:51 | ...[...] | ActiveRecordInjection.rb:74:32:74:54 | "id = '#{...}'" | | ActiveRecordInjection.rb:74:41:74:51 | ...[...] | ActiveRecordInjection.rb:74:32:74:54 | "id = '#{...}'" |
| ActiveRecordInjection.rb:79:23:79:28 | call to params | ActiveRecordInjection.rb:79:23:79:35 | ...[...] |
| ActiveRecordInjection.rb:83:17:83:22 | call to params | ActiveRecordInjection.rb:83:17:83:31 | ...[...] | | ActiveRecordInjection.rb:83:17:83:22 | call to params | ActiveRecordInjection.rb:83:17:83:31 | ...[...] |
| ActiveRecordInjection.rb:84:19:84:24 | call to params | ActiveRecordInjection.rb:84:19:84:33 | ...[...] | | ActiveRecordInjection.rb:84:19:84:24 | call to params | ActiveRecordInjection.rb:84:19:84:33 | ...[...] |
| ActiveRecordInjection.rb:88:18:88:23 | call to params | ActiveRecordInjection.rb:88:18:88:35 | ...[...] | | ActiveRecordInjection.rb:88:18:88:23 | call to params | ActiveRecordInjection.rb:88:18:88:35 | ...[...] |
@@ -35,6 +36,7 @@ edges
| ActiveRecordInjection.rb:103:11:103:17 | ...[...] | ActiveRecordInjection.rb:103:5:103:7 | uid | | ActiveRecordInjection.rb:103:11:103:17 | ...[...] | ActiveRecordInjection.rb:103:5:103:7 | uid |
| ActiveRecordInjection.rb:104:5:104:9 | uidEq | ActiveRecordInjection.rb:108:20:108:32 | ... + ... | | ActiveRecordInjection.rb:104:5:104:9 | uidEq | ActiveRecordInjection.rb:108:20:108:32 | ... + ... |
| ActiveRecordInjection.rb:141:21:141:26 | call to params | ActiveRecordInjection.rb:141:21:141:44 | ...[...] | | ActiveRecordInjection.rb:141:21:141:26 | call to params | ActiveRecordInjection.rb:141:21:141:44 | ...[...] |
| ActiveRecordInjection.rb:141:21:141:26 | call to params | ActiveRecordInjection.rb:141:21:141:44 | ...[...] |
| ActiveRecordInjection.rb:141:21:141:44 | ...[...] | ActiveRecordInjection.rb:20:22:20:30 | condition | | ActiveRecordInjection.rb:141:21:141:44 | ...[...] | ActiveRecordInjection.rb:20:22:20:30 | condition |
| ActiveRecordInjection.rb:155:59:155:64 | call to params | ActiveRecordInjection.rb:155:59:155:74 | ...[...] | | ActiveRecordInjection.rb:155:59:155:64 | call to params | ActiveRecordInjection.rb:155:59:155:74 | ...[...] |
| ActiveRecordInjection.rb:155:59:155:74 | ...[...] | ActiveRecordInjection.rb:155:27:155:76 | "this is an unsafe annotation:..." | | ActiveRecordInjection.rb:155:59:155:74 | ...[...] | ActiveRecordInjection.rb:155:27:155:76 | "this is an unsafe annotation:..." |
@@ -102,6 +104,8 @@ nodes
| ActiveRecordInjection.rb:74:32:74:54 | "id = '#{...}'" | semmle.label | "id = '#{...}'" | | ActiveRecordInjection.rb:74:32:74:54 | "id = '#{...}'" | semmle.label | "id = '#{...}'" |
| ActiveRecordInjection.rb:74:41:74:46 | call to params | semmle.label | call to params | | ActiveRecordInjection.rb:74:41:74:46 | call to params | semmle.label | call to params |
| ActiveRecordInjection.rb:74:41:74:51 | ...[...] | semmle.label | ...[...] | | ActiveRecordInjection.rb:74:41:74:51 | ...[...] | semmle.label | ...[...] |
| ActiveRecordInjection.rb:79:23:79:28 | call to params | semmle.label | call to params |
| ActiveRecordInjection.rb:79:23:79:35 | ...[...] | semmle.label | ...[...] |
| ActiveRecordInjection.rb:83:17:83:22 | call to params | semmle.label | call to params | | ActiveRecordInjection.rb:83:17:83:22 | call to params | semmle.label | call to params |
| ActiveRecordInjection.rb:83:17:83:31 | ...[...] | semmle.label | ...[...] | | ActiveRecordInjection.rb:83:17:83:31 | ...[...] | semmle.label | ...[...] |
| ActiveRecordInjection.rb:84:19:84:24 | call to params | semmle.label | call to params | | ActiveRecordInjection.rb:84:19:84:24 | call to params | semmle.label | call to params |
@@ -123,6 +127,7 @@ nodes
| ActiveRecordInjection.rb:108:20:108:32 | ... + ... | semmle.label | ... + ... | | ActiveRecordInjection.rb:108:20:108:32 | ... + ... | semmle.label | ... + ... |
| ActiveRecordInjection.rb:141:21:141:26 | call to params | semmle.label | call to params | | ActiveRecordInjection.rb:141:21:141:26 | call to params | semmle.label | call to params |
| ActiveRecordInjection.rb:141:21:141:44 | ...[...] | semmle.label | ...[...] | | ActiveRecordInjection.rb:141:21:141:44 | ...[...] | semmle.label | ...[...] |
| ActiveRecordInjection.rb:141:21:141:44 | ...[...] | semmle.label | ...[...] |
| ActiveRecordInjection.rb:155:27:155:76 | "this is an unsafe annotation:..." | semmle.label | "this is an unsafe annotation:..." | | ActiveRecordInjection.rb:155:27:155:76 | "this is an unsafe annotation:..." | semmle.label | "this is an unsafe annotation:..." |
| ActiveRecordInjection.rb:155:59:155:64 | call to params | semmle.label | call to params | | ActiveRecordInjection.rb:155:59:155:64 | call to params | semmle.label | call to params |
| ActiveRecordInjection.rb:155:59:155:74 | ...[...] | semmle.label | ...[...] | | ActiveRecordInjection.rb:155:59:155:74 | ...[...] | semmle.label | ...[...] |
@@ -172,6 +177,7 @@ subpaths
| ActiveRecordInjection.rb:61:16:61:21 | <<-SQL | ActiveRecordInjection.rb:62:21:62:26 | call to params | ActiveRecordInjection.rb:61:16:61:21 | <<-SQL | This SQL query depends on a $@. | ActiveRecordInjection.rb:62:21:62:26 | call to params | user-provided value | | ActiveRecordInjection.rb:61:16:61:21 | <<-SQL | ActiveRecordInjection.rb:62:21:62:26 | call to params | ActiveRecordInjection.rb:61:16:61:21 | <<-SQL | This SQL query depends on a $@. | ActiveRecordInjection.rb:62:21:62:26 | call to params | user-provided value |
| ActiveRecordInjection.rb:68:20:68:47 | "user.id = '#{...}'" | ActiveRecordInjection.rb:68:34:68:39 | call to params | ActiveRecordInjection.rb:68:20:68:47 | "user.id = '#{...}'" | This SQL query depends on a $@. | ActiveRecordInjection.rb:68:34:68:39 | call to params | user-provided value | | ActiveRecordInjection.rb:68:20:68:47 | "user.id = '#{...}'" | ActiveRecordInjection.rb:68:34:68:39 | call to params | ActiveRecordInjection.rb:68:20:68:47 | "user.id = '#{...}'" | This SQL query depends on a $@. | ActiveRecordInjection.rb:68:34:68:39 | call to params | user-provided value |
| ActiveRecordInjection.rb:74:32:74:54 | "id = '#{...}'" | ActiveRecordInjection.rb:74:41:74:46 | call to params | ActiveRecordInjection.rb:74:32:74:54 | "id = '#{...}'" | This SQL query depends on a $@. | ActiveRecordInjection.rb:74:41:74:46 | call to params | user-provided value | | ActiveRecordInjection.rb:74:32:74:54 | "id = '#{...}'" | ActiveRecordInjection.rb:74:41:74:46 | call to params | ActiveRecordInjection.rb:74:32:74:54 | "id = '#{...}'" | This SQL query depends on a $@. | ActiveRecordInjection.rb:74:41:74:46 | call to params | user-provided value |
| ActiveRecordInjection.rb:79:23:79:35 | ...[...] | ActiveRecordInjection.rb:79:23:79:28 | call to params | ActiveRecordInjection.rb:79:23:79:35 | ...[...] | This SQL query depends on a $@. | ActiveRecordInjection.rb:79:23:79:28 | call to params | user-provided value |
| ActiveRecordInjection.rb:83:17:83:31 | ...[...] | ActiveRecordInjection.rb:83:17:83:22 | call to params | ActiveRecordInjection.rb:83:17:83:31 | ...[...] | This SQL query depends on a $@. | ActiveRecordInjection.rb:83:17:83:22 | call to params | user-provided value | | ActiveRecordInjection.rb:83:17:83:31 | ...[...] | ActiveRecordInjection.rb:83:17:83:22 | call to params | ActiveRecordInjection.rb:83:17:83:31 | ...[...] | This SQL query depends on a $@. | ActiveRecordInjection.rb:83:17:83:22 | call to params | user-provided value |
| ActiveRecordInjection.rb:84:19:84:33 | ...[...] | ActiveRecordInjection.rb:84:19:84:24 | call to params | ActiveRecordInjection.rb:84:19:84:33 | ...[...] | This SQL query depends on a $@. | ActiveRecordInjection.rb:84:19:84:24 | call to params | user-provided value | | ActiveRecordInjection.rb:84:19:84:33 | ...[...] | ActiveRecordInjection.rb:84:19:84:24 | call to params | ActiveRecordInjection.rb:84:19:84:33 | ...[...] | This SQL query depends on a $@. | ActiveRecordInjection.rb:84:19:84:24 | call to params | user-provided value |
| ActiveRecordInjection.rb:88:18:88:35 | ...[...] | ActiveRecordInjection.rb:88:18:88:23 | call to params | ActiveRecordInjection.rb:88:18:88:35 | ...[...] | This SQL query depends on a $@. | ActiveRecordInjection.rb:88:18:88:23 | call to params | user-provided value | | ActiveRecordInjection.rb:88:18:88:35 | ...[...] | ActiveRecordInjection.rb:88:18:88:23 | call to params | ActiveRecordInjection.rb:88:18:88:35 | ...[...] | This SQL query depends on a $@. | ActiveRecordInjection.rb:88:18:88:23 | call to params | user-provided value |
@@ -179,6 +185,7 @@ subpaths
| ActiveRecordInjection.rb:94:18:94:35 | ...[...] | ActiveRecordInjection.rb:94:18:94:23 | call to params | ActiveRecordInjection.rb:94:18:94:35 | ...[...] | This SQL query depends on a $@. | ActiveRecordInjection.rb:94:18:94:23 | call to params | user-provided value | | ActiveRecordInjection.rb:94:18:94:35 | ...[...] | ActiveRecordInjection.rb:94:18:94:23 | call to params | ActiveRecordInjection.rb:94:18:94:35 | ...[...] | This SQL query depends on a $@. | ActiveRecordInjection.rb:94:18:94:23 | call to params | user-provided value |
| ActiveRecordInjection.rb:96:23:96:47 | ...[...] | ActiveRecordInjection.rb:96:23:96:28 | call to params | ActiveRecordInjection.rb:96:23:96:47 | ...[...] | This SQL query depends on a $@. | ActiveRecordInjection.rb:96:23:96:28 | call to params | user-provided value | | ActiveRecordInjection.rb:96:23:96:47 | ...[...] | ActiveRecordInjection.rb:96:23:96:28 | call to params | ActiveRecordInjection.rb:96:23:96:47 | ...[...] | This SQL query depends on a $@. | ActiveRecordInjection.rb:96:23:96:28 | call to params | user-provided value |
| ActiveRecordInjection.rb:108:20:108:32 | ... + ... | ActiveRecordInjection.rb:102:10:102:15 | call to params | ActiveRecordInjection.rb:108:20:108:32 | ... + ... | This SQL query depends on a $@. | ActiveRecordInjection.rb:102:10:102:15 | call to params | user-provided value | | ActiveRecordInjection.rb:108:20:108:32 | ... + ... | ActiveRecordInjection.rb:102:10:102:15 | call to params | ActiveRecordInjection.rb:108:20:108:32 | ... + ... | This SQL query depends on a $@. | ActiveRecordInjection.rb:102:10:102:15 | call to params | user-provided value |
| ActiveRecordInjection.rb:141:21:141:44 | ...[...] | ActiveRecordInjection.rb:141:21:141:26 | call to params | ActiveRecordInjection.rb:141:21:141:44 | ...[...] | This SQL query depends on a $@. | ActiveRecordInjection.rb:141:21:141:26 | call to params | user-provided value |
| ActiveRecordInjection.rb:155:27:155:76 | "this is an unsafe annotation:..." | ActiveRecordInjection.rb:155:59:155:64 | call to params | ActiveRecordInjection.rb:155:27:155:76 | "this is an unsafe annotation:..." | This SQL query depends on a $@. | ActiveRecordInjection.rb:155:59:155:64 | call to params | user-provided value | | ActiveRecordInjection.rb:155:27:155:76 | "this is an unsafe annotation:..." | ActiveRecordInjection.rb:155:59:155:64 | call to params | ActiveRecordInjection.rb:155:27:155:76 | "this is an unsafe annotation:..." | This SQL query depends on a $@. | ActiveRecordInjection.rb:155:59:155:64 | call to params | user-provided value |
| ActiveRecordInjection.rb:168:37:168:41 | query | ActiveRecordInjection.rb:173:5:173:10 | call to params | ActiveRecordInjection.rb:168:37:168:41 | query | This SQL query depends on a $@. | ActiveRecordInjection.rb:173:5:173:10 | call to params | user-provided value | | ActiveRecordInjection.rb:168:37:168:41 | query | ActiveRecordInjection.rb:173:5:173:10 | call to params | ActiveRecordInjection.rb:168:37:168:41 | query | This SQL query depends on a $@. | ActiveRecordInjection.rb:173:5:173:10 | call to params | user-provided value |
| ActiveRecordInjection.rb:177:43:177:104 | "SELECT * FROM users WHERE id ..." | ActiveRecordInjection.rb:173:5:173:10 | call to params | ActiveRecordInjection.rb:177:43:177:104 | "SELECT * FROM users WHERE id ..." | This SQL query depends on a $@. | ActiveRecordInjection.rb:173:5:173:10 | call to params | user-provided value | | ActiveRecordInjection.rb:177:43:177:104 | "SELECT * FROM users WHERE id ..." | ActiveRecordInjection.rb:173:5:173:10 | call to params | ActiveRecordInjection.rb:177:43:177:104 | "SELECT * FROM users WHERE id ..." | This SQL query depends on a $@. | ActiveRecordInjection.rb:173:5:173:10 | call to params | user-provided value |
@@ -189,4 +196,4 @@ subpaths
| PgInjection.rb:20:22:20:25 | qry2 | PgInjection.rb:6:12:6:17 | call to params | PgInjection.rb:20:22:20:25 | qry2 | This SQL query depends on a $@. | PgInjection.rb:6:12:6:17 | call to params | user-provided value | | PgInjection.rb:20:22:20:25 | qry2 | PgInjection.rb:6:12:6:17 | call to params | PgInjection.rb:20:22:20:25 | qry2 | This SQL query depends on a $@. | PgInjection.rb:6:12:6:17 | call to params | user-provided value |
| PgInjection.rb:21:28:21:31 | qry2 | PgInjection.rb:6:12:6:17 | call to params | PgInjection.rb:21:28:21:31 | qry2 | This SQL query depends on a $@. | PgInjection.rb:6:12:6:17 | call to params | user-provided value | | PgInjection.rb:21:28:21:31 | qry2 | PgInjection.rb:6:12:6:17 | call to params | PgInjection.rb:21:28:21:31 | qry2 | This SQL query depends on a $@. | PgInjection.rb:6:12:6:17 | call to params | user-provided value |
| PgInjection.rb:32:29:32:32 | qry3 | PgInjection.rb:6:12:6:17 | call to params | PgInjection.rb:32:29:32:32 | qry3 | This SQL query depends on a $@. | PgInjection.rb:6:12:6:17 | call to params | user-provided value | | PgInjection.rb:32:29:32:32 | qry3 | PgInjection.rb:6:12:6:17 | call to params | PgInjection.rb:32:29:32:32 | qry3 | This SQL query depends on a $@. | PgInjection.rb:6:12:6:17 | call to params | user-provided value |
| PgInjection.rb:44:29:44:32 | qry3 | PgInjection.rb:6:12:6:17 | call to params | PgInjection.rb:44:29:44:32 | qry3 | This SQL query depends on a $@. | PgInjection.rb:6:12:6:17 | call to params | user-provided value | | PgInjection.rb:44:29:44:32 | qry3 | PgInjection.rb:6:12:6:17 | call to params | PgInjection.rb:44:29:44:32 | qry3 | This SQL query depends on a $@. | PgInjection.rb:6:12:6:17 | call to params | user-provided value |

View File

@@ -501,6 +501,19 @@ module Make<InlineExpectationsTestSig Impl> {
class FalseNegativeExpectation = LegacyTest::FalseNegativeTestExpectation; class FalseNegativeExpectation = LegacyTest::FalseNegativeTestExpectation;
class InvalidExpectation = LegacyTest::InvalidTestExpectation; class InvalidExpectation = LegacyTest::InvalidTestExpectation;
/**
* Holds if the expectation `tag=value` is found in one or more expectation comments.
*
* This can be used when writing tests where the set of possible values must be known in advance,
* for example, when testing a predicate for which `value` is part of the binding set.
*/
predicate hasExpectationWithValue(string tag, string value) {
exists(string tags |
getAnExpectation(_, _, _, tags, value) and
tag = tags.splitAt(",")
)
}
} }
/** /**