Merge remote-tracking branch 'origin/main' into rb/rack-env-query-string

This commit is contained in:
Alex Ford
2023-07-17 14:11:25 +01:00
644 changed files with 15035 additions and 2704 deletions

View File

@@ -1,3 +1,20 @@
## 0.7.0
### Deprecated APIs
* The `Configuration` taint flow configuration class from `codeql.ruby.security.InsecureDownloadQuery` has been deprecated. Use the `Flow` module instead.
### Minor Analysis Improvements
* More kinds of rack applications are now recognized.
* Rack::Response instances are now recognized as potential responses from rack applications.
* HTTP redirect responses from Rack applications are now recognized as a potential sink for open redirect alerts.
* Additional sinks for `rb/unsafe-deserialization` have been added. This includes various methods from the `yaml` and `plist` gems, which deserialize YAML and Property List data, respectively.
## 0.6.4
No user-facing changes.
## 0.6.3
### Minor Analysis Improvements

View File

@@ -1,4 +0,0 @@
---
category: minorAnalysis
---
* Additional sinks for `rb/unsafe-deserialization` have been added. This includes various methods from the `yaml` and `plist` gems, which deserialize YAML and Property List data, respectively.

View File

@@ -1,4 +0,0 @@
---
category: minorAnalysis
---
* HTTP redirect responses from Rack applications are now recognized as a potential sink for open redirect alerts.

View File

@@ -1,4 +0,0 @@
---
category: deprecated
---
* The `Configuration` taint flow configuration class from `codeql.ruby.security.InsecureDownloadQuery` has been deprecated. Use the `Flow` module instead.

View File

@@ -1,5 +0,0 @@
---
category: minorAnalysis
---
* More kinds of rack applications are now recognized.
* Rack::Response instances are now recognized as potential responses from rack applications.

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.

View File

@@ -0,0 +1,5 @@
---
category: minorAnalysis
---
* Query parameters and cookies from `Rack::Response` objects are recognized as potential sources of remote flow input.
* Calls to `Rack::Utils.parse_query` now propagate taint.

View File

@@ -0,0 +1,6 @@
---
category: feature
---
* The `DataFlow::StateConfigSig` signature module has gained default implementations for `isBarrier/2` and `isAdditionalFlowStep/4`.
Hence it is no longer needed to provide `none()` implementations of these predicates if they are not needed.

View File

@@ -0,0 +1,3 @@
## 0.6.4
No user-facing changes.

View File

@@ -0,0 +1,12 @@
## 0.7.0
### Deprecated APIs
* The `Configuration` taint flow configuration class from `codeql.ruby.security.InsecureDownloadQuery` has been deprecated. Use the `Flow` module instead.
### Minor Analysis Improvements
* More kinds of rack applications are now recognized.
* Rack::Response instances are now recognized as potential responses from rack applications.
* HTTP redirect responses from Rack applications are now recognized as a potential sink for open redirect alerts.
* Additional sinks for `rb/unsafe-deserialization` have been added. This includes various methods from the `yaml` and `plist` gems, which deserialize YAML and Property List data, respectively.

View File

@@ -1,2 +1,2 @@
---
lastReleaseVersion: 0.6.3
lastReleaseVersion: 0.7.0

File diff suppressed because it is too large Load Diff

View File

@@ -843,6 +843,58 @@ module XmlParserCall {
}
}
/**
* A data-flow node that constructs an XPath expression.
*
* If it is important that the XPath expression is indeed executed, then use `XPathExecution`.
*
* Extend this class to refine existing API models. If you want to model new APIs,
* extend `XPathConstruction::Range` instead.
*/
class XPathConstruction extends DataFlow::Node instanceof XPathConstruction::Range {
/** Gets the argument that specifies the XPath expressions to be constructed. */
DataFlow::Node getXPath() { result = super.getXPath() }
}
/** Provides a class for modeling new XPath construction APIs. */
module XPathConstruction {
/**
* A data-flow node that constructs an XPath expression.
*
* Extend this class to model new APIs. If you want to refine existing API models,
* extend `XPathConstruction` instead.
*/
abstract class Range extends DataFlow::Node {
/** Gets the argument that specifies the XPath expressions to be constructed. */
abstract DataFlow::Node getXPath();
}
}
/**
* A data-flow node that executes an XPath expression.
*
* Extend this class to refine existing API models. If you want to model new APIs,
* extend `XPathExecution::Range` instead.
*/
class XPathExecution extends DataFlow::Node instanceof XPathExecution::Range {
/** Gets the argument that specifies the XPath expressions to be executed. */
DataFlow::Node getXPath() { result = super.getXPath() }
}
/** Provides a class for modeling new XPath execution APIs. */
module XPathExecution {
/**
* A data-flow node that executes an XPath expression.
*
* Extend this class to model new APIs. If you want to refine existing API models,
* extend `XPathExecution` instead.
*/
abstract class Range extends DataFlow::Node {
/** Gets the argument that specifies the XPath expressions to be executed. */
abstract DataFlow::Node getXPath();
}
}
/**
* A data-flow node that may represent a database object in an ORM system.
*

View File

@@ -114,7 +114,7 @@ signature module StateConfigSig {
* Holds if data flow through `node` is prohibited when the flow state is
* `state`.
*/
predicate isBarrier(Node node, FlowState state);
default predicate isBarrier(Node node, FlowState state) { none() }
/** Holds if data flow into `node` is prohibited. */
default predicate isBarrierIn(Node node) { none() }
@@ -131,7 +131,9 @@ signature module StateConfigSig {
* Holds if data may flow from `node1` to `node2` in addition to the normal data-flow steps.
* This step is only applicable in `state1` and updates the flow state to `state2`.
*/
predicate isAdditionalFlowStep(Node node1, FlowState state1, Node node2, FlowState state2);
default predicate isAdditionalFlowStep(Node node1, FlowState state1, Node node2, FlowState state2) {
none()
}
/**
* Holds if an arbitrary number of implicit read steps of content `c` may be

View File

@@ -254,6 +254,11 @@ module Impl<FullStateConfigSig Config> {
not fullBarrier(node2)
}
pragma[nomagic]
private predicate isUnreachableInCall1(NodeEx n, LocalCallContextSpecificCall cc) {
isUnreachableInCallCached(n.asNode(), cc.getCall())
}
/**
* Holds if data can flow in one local step from `node1` to `node2`.
*/
@@ -2108,7 +2113,7 @@ module Impl<FullStateConfigSig Config> {
NodeEx node1, FlowState state, NodeEx node2, boolean preservesValue, DataFlowType t,
LocalCallContext cc
) {
not isUnreachableInCallCached(node2.asNode(), cc.(LocalCallContextSpecificCall).getCall()) and
not isUnreachableInCall1(node2, cc) and
(
localFlowEntry(node1, pragma[only_bind_into](state)) and
(
@@ -2123,7 +2128,7 @@ module Impl<FullStateConfigSig Config> {
) and
node1 != node2 and
cc.relevantFor(node1.getEnclosingCallable()) and
not isUnreachableInCallCached(node1.asNode(), cc.(LocalCallContextSpecificCall).getCall())
not isUnreachableInCall1(node1, cc)
or
exists(NodeEx mid |
localFlowStepPlus(node1, pragma[only_bind_into](state), mid, preservesValue, t, cc) and
@@ -2160,10 +2165,8 @@ module Impl<FullStateConfigSig Config> {
preservesValue = false and
t = node2.getDataFlowType() and
callContext.relevantFor(node1.getEnclosingCallable()) and
not exists(DataFlowCall call | call = callContext.(LocalCallContextSpecificCall).getCall() |
isUnreachableInCallCached(node1.asNode(), call) or
isUnreachableInCallCached(node2.asNode(), call)
)
not isUnreachableInCall1(node1, callContext) and
not isUnreachableInCall1(node2, callContext)
}
}
@@ -2703,7 +2706,7 @@ module Impl<FullStateConfigSig Config> {
ParamNodeEx getParamNode() { result = p }
override string toString() { result = p + ": " + ap }
override string toString() { result = p + concat(" : " + ppReprType(t)) + " " + ap }
predicate hasLocationInfo(
string filepath, int startline, int startcolumn, int endline, int endcolumn
@@ -2755,12 +2758,21 @@ module Impl<FullStateConfigSig Config> {
)
}
private predicate forceUnfold(AccessPathApprox apa) {
forceHighPrecision(apa.getHead())
or
exists(Content c2 |
apa = TConsCons(_, _, c2, _) and
forceHighPrecision(c2)
)
}
/**
* Holds with `unfold = false` if a precise head-tail representation of `apa` is
* expected to be expensive. Holds with `unfold = true` otherwise.
*/
private predicate evalUnfold(AccessPathApprox apa, boolean unfold) {
if forceHighPrecision(apa.getHead())
if forceUnfold(apa)
then unfold = true
else
exists(int aps, int nodes, int apLimit, int tupleLimit |
@@ -3089,6 +3101,12 @@ module Impl<FullStateConfigSig Config> {
result = " <" + this.(PathNodeMid).getCallContext().toString() + ">"
}
private string ppSummaryCtx() {
this instanceof PathNodeSink and result = ""
or
result = " <" + this.(PathNodeMid).getSummaryCtx().toString() + ">"
}
/** Gets a textual representation of this element. */
string toString() { result = this.getNodeEx().toString() + this.ppType() + this.ppAp() }
@@ -3097,7 +3115,9 @@ module Impl<FullStateConfigSig Config> {
* representation of the call context.
*/
string toStringWithContext() {
result = this.getNodeEx().toString() + this.ppType() + this.ppAp() + this.ppCtx()
result =
this.getNodeEx().toString() + this.ppType() + this.ppAp() + this.ppCtx() +
this.ppSummaryCtx()
}
/**

View File

@@ -1333,11 +1333,20 @@ predicate lambdaCreation(Node creation, LambdaCallKind kind, DataFlowCallable c)
creation.asExpr() =
any(CfgNodes::ExprNodes::MethodCallCfgNode mc |
c.asCallable() = mc.getBlock().getExpr() and
mc.getExpr().getMethodName() = ["lambda", "proc"]
isProcCreationCall(mc.getExpr())
)
)
}
/** Holds if `call` is a call to `lambda`, `proc`, or `Proc.new` */
pragma[nomagic]
private predicate isProcCreationCall(MethodCall call) {
call.getMethodName() = ["proc", "lambda"]
or
call.getMethodName() = "new" and
call.getReceiver().(ConstantReadAccess).getAQualifiedName() = "Proc"
}
/**
* Holds if `call` is a from-source lambda call of kind `kind` where `receiver`
* is the lambda expression.

View File

@@ -6,12 +6,17 @@ private import codeql.ruby.typetracking.TypeTracker
private import codeql.ruby.dataflow.SSA
private import FlowSummaryImpl as FlowSummaryImpl
private import SsaImpl as SsaImpl
private import codeql.ruby.ApiGraphs
/**
* An element, viewed as a node in a data flow graph. Either an expression
* (`ExprNode`) or a parameter (`ParameterNode`).
*/
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. */
CfgNodes::ExprCfgNode asExpr() { result = this.(ExprNode).getExprNode() }
@@ -76,6 +81,11 @@ class Node extends TNode {
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. */
@@ -144,6 +154,18 @@ class CallNode extends LocalSourceNode, ExprNode {
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 {
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. */
pragma[inline]
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.
*/
@@ -387,6 +418,39 @@ private module Cached {
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
predicate forceCachingInSameStage() { any() }
@@ -1028,6 +1092,33 @@ class ModuleNode instanceof Module {
* this predicate.
*/
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. */
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())
}
/**
* 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.
*
@@ -1359,22 +1435,6 @@ class ConstRef extends LocalSourceNode {
*/
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).
*
@@ -1436,7 +1496,7 @@ class ConstRef extends LocalSourceNode {
pragma[inline]
ConstRef getConstant(string name) {
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)
)
}
@@ -1458,7 +1518,9 @@ class ConstRef extends LocalSourceNode {
* 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
exists(API::MethodAccessNode mac |
mac = API::getTopLevelMember("UnicodeUtils").getMethod(["nfkd", "nfc", "nfd", "nfkc"]) and
sink = mac.getParameter(0).asSink()
sink = mac.getArgument(0).asSink()
)
or
// eprun
exists(API::MethodAccessNode mac |
mac = API::getTopLevelMember("Eprun").getMethod("normalize") and
sink = mac.getParameter(0).asSink()
sink = mac.getArgument(0).asSink()
)
or
// unf
exists(API::MethodAccessNode mac |
mac = API::getTopLevelMember("UNF").getMember("Normalizer").getMethod("normalize") and
sink = mac.getParameter(0).asSink()
sink = mac.getArgument(0).asSink()
)
or
// ActiveSupport::Multibyte::Chars
@@ -113,7 +113,7 @@ class Configuration extends TaintTracking::Configuration {
.getMember("Multibyte")
.getMember("Chars")
.getMethod("new")
.getCallNode() and
.asCall() and
n = cn.getAMethodCall("normalize") and
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
// this file is safe.
not zipOpen
.getCallNode()
.asCall()
.getArgument(0)
.getALocalSource()
.getConstantValue()

View File

@@ -83,8 +83,8 @@ class ActionControllerClass extends DataFlow::ClassNode {
}
}
private DataFlow::LocalSourceNode actionControllerInstance() {
result = any(ActionControllerClass cls).getSelf()
private API::Node actionControllerInstance() {
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`. */
private class RendererRenderCall extends RenderCallImpl {
RendererRenderCall() {
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()
}
RendererRenderCall() { this = renderCall().asExpr().getExpr() }
}
/** A call to `html_escape` from within a controller. */
@@ -260,6 +260,7 @@ class RedirectToCall extends MethodCall {
this =
controller
.getSelf()
.track()
.getAMethodCall(["redirect_to", "redirect_back", "redirect_back_or_to"])
.asExpr()
.getExpr()
@@ -600,9 +601,7 @@ private module ParamsSummaries {
* response.
*/
private module Response {
DataFlow::LocalSourceNode response() {
result = actionControllerInstance().getAMethodCall("response")
}
API::Node response() { result = actionControllerInstance().getReturn("response") }
class BodyWrite extends DataFlow::CallNode, Http::Server::HttpResponse::Range {
BodyWrite() { this = response().getAMethodCall("body=") }
@@ -628,7 +627,7 @@ private module Response {
HeaderWrite() {
// response.header[key] = val
// response.headers[key] = val
this = response().getAMethodCall(["header", "headers"]).getAMethodCall("[]=")
this = response().getReturn(["header", "headers"]).getAMethodCall("[]=")
or
// response.set_header(key) = 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 {
ActionControllerLoggingCall() {
this.getReceiver() instanceof ActionControllerLoggerInstance and
this.getMethodName() = ["debug", "error", "fatal", "info", "unknown", "warn"]
this =
actionControllerInstance()
.getReturn("logger")
.getAMethodCall(["debug", "error", "fatal", "info", "unknown", "warn"])
}
// Note: this is identical to the definition `stdlib.Logger.LoggerInfoStyleCall`.

View File

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

View File

@@ -4,12 +4,26 @@
private import codeql.ruby.AST
private import codeql.ruby.ApiGraphs
private import codeql.ruby.DataFlow
private import codeql.ruby.frameworks.internal.Rails
/**
* Provides modeling for the `ActionMailer` library.
*/
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`.
* For example,
@@ -21,33 +35,11 @@ module ActionMailer {
* ```
*/
class MailerClass extends ClassDeclaration {
MailerClass() {
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 }
MailerClass() { this = actionMailerClass().getADeclaration() }
}
/** A call to `params` from within a mailer. */
class ParamsCall extends ContextCall, ParamsCallImpl {
ParamsCall() { this.getMethodName() = "params" }
class ParamsCall extends ParamsCallImpl {
ParamsCall() { this = actionMailerInstance().getAMethodCall("params").asExpr().getExpr() }
}
}

View File

@@ -30,10 +30,8 @@ private predicate isBuiltInMethodForActiveRecordModelInstance(string methodName)
methodName = objectInstanceMethodName()
}
private API::Node activeRecordClassApiNode() {
private API::Node activeRecordBaseClass() {
result =
// class Foo < ActiveRecord::Base
// class Bar < Foo
[
API::getTopLevelMember("ActiveRecord").getMember("Base"),
// 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,
*
@@ -55,20 +93,19 @@ private API::Node activeRecordClassApiNode() {
* ```
*/
class ActiveRecordModelClass extends ClassDeclaration {
private DataFlow::ClassNode cls;
ActiveRecordModelClass() {
this.getSuperclassExpr() =
activeRecordClassApiNode().getASubclass().getAValueReachableFromSource().asExpr().getExpr()
cls = activeRecordBaseClass().getADescendentModule() and this = cls.getADeclaration()
}
// Gets the class declaration for this class and all of its super classes
private ModuleBase getAllClassDeclarations() {
result = this.getModule().getSuperClass*().getADeclaration()
}
private ModuleBase getAllClassDeclarations() { result = cls.getAnAncestor().getADeclaration() }
/**
* 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
result = this.getAllClassDeclarations().getAMethod() and
// 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 {
private ActiveRecordModelClass recvCls;
/**
* Gets a potential reference to an ActiveRecord class object.
*/
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() {
// e.g. Foo.where(...)
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
this = getAnUnknownActiveRecordModelClassCall().asCall().asExpr().getExpr()
}
/** The `ActiveRecordModelClass` of the receiver of this method. */
ActiveRecordModelClass getReceiverClass() { result = recvCls }
/** Gets the `ActiveRecordModelClass` of the receiver of this method, if it can be determined. */
ActiveRecordModelClass getReceiverClass() {
this = result.getClassNode().trackModule().getMethod(_).asCall().asExpr().getExpr()
}
}
private Expr sqlFragmentArgument(MethodCall call) {
exists(string methodName |
methodName = call.getMethodName() and
(
methodName =
[
"delete_all", "delete_by", "destroy_all", "destroy_by", "exists?", "find_by", "find_by!",
"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", "select", "reselect", "update_all"
] and
result = call.getArgument(0)
or
methodName = "calculate" and result = call.getArgument(1)
or
methodName in ["average", "count", "maximum", "minimum", "sum", "count_by_sql"] and
result = call.getArgument(0)
or
// This format was supported until Rails 2.3.8
methodName = ["all", "find", "first", "last"] and
result = call.getKeywordArgument("conditions")
or
methodName = "reload" and
result = call.getKeywordArgument("lock")
or
// 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.
methodName = "annotate" and
result = call.getArgument(_)
)
private predicate sqlFragmentArgumentInner(DataFlow::CallNode call, DataFlow::Node sink) {
call =
activeRecordQueryBuilderCall([
"delete_all", "delete_by", "destroy_all", "destroy_by", "exists?", "find_by", "find_by!",
"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",
"select", "reselect", "update_all"
]) and
sink = call.getArgument(0)
or
call = activeRecordQueryBuilderCall("calculate") and
sink = call.getArgument(1)
or
call =
activeRecordQueryBuilderCall(["average", "count", "maximum", "minimum", "sum", "count_by_sql"]) and
sink = call.getArgument(0)
or
// This format was supported until Rails 2.3.8
call = activeRecordQueryBuilderCall(["all", "find", "first", "last"]) and
sink = call.getKeywordArgument("conditions")
or
call = activeRecordQueryBuilderCall("reload") and
sink = call.getKeywordArgument("lock")
or
// 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.
call = activeRecordQueryBuilderCall("annotate") and
sink = 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
* queries if the `getSqlFragmentSinkArgument()` expression is tainted by
* 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
* rather than just one with a matching name.
*/
class PotentiallyUnsafeSqlExecutingMethodCall extends ActiveRecordModelClassMethodCall {
// The SQL fragment argument itself
private Expr sqlFragmentExpr;
deprecated class PotentiallyUnsafeSqlExecutingMethodCall extends ActiveRecordModelClassMethodCall {
private DataFlow::CallNode call;
PotentiallyUnsafeSqlExecutingMethodCall() {
exists(Expr arg |
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()
)
)
call.asExpr().getExpr() = this and sqlFragmentArgument(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
* `PotentiallyUnsafeSqlExecutingMethodCall` that may be vulnerable to being
* controlled by user input.
* A SQL execution arising from a call to the ActiveRecord library.
*/
class ActiveRecordSqlExecutionRange extends SqlExecution::Range {
ActiveRecordSqlExecutionRange() {
exists(PotentiallyUnsafeSqlExecutingMethodCall mc |
this.asExpr().getNode() = mc.getSqlFragmentSinkArgument()
)
or
this = activeRecordConnectionInstance().getAMethodCall("execute").getArgument(0) and
unsafeSqlExpr(this.asExpr().getExpr())
}
ActiveRecordSqlExecutionRange() { sqlFragmentArgument(_, this) }
override DataFlow::Node getSql() { result = this }
}
private API::Node activeRecordConnectionInstance() {
result = activeRecordClassApiNode().getReturn("connection")
}
// TODO: model `ActiveRecord` sanitizers
// https://api.rubyonrails.org/classes/ActiveRecord/Sanitization/ClassMethods.html
/**
@@ -241,15 +283,8 @@ abstract class ActiveRecordModelInstantiation extends OrmInstantiation::Range,
override predicate methodCallMayAccessField(string methodName) {
// The method is not a built-in, and...
not isBuiltInMethodForActiveRecordModelInstance(methodName) and
(
// ...There is no matching method definition in the class, or...
not exists(this.getClass().getMethod(methodName))
or
// ...the called method can access a field.
exists(Method m | m = this.getClass().getAPotentialFieldAccessMethod() |
m.getName() = methodName
)
)
// ...There is no matching method definition in the class
not exists(this.getClass().getMethod(methodName))
}
}
@@ -317,21 +352,10 @@ private class ActiveRecordModelFinderCall extends ActiveRecordModelInstantiation
}
// A `self` reference that may resolve to an active record model object
private class ActiveRecordModelClassSelfReference extends ActiveRecordModelInstantiation,
DataFlow::SelfParameterNode
{
private class ActiveRecordModelClassSelfReference extends ActiveRecordModelInstantiation {
private ActiveRecordModelClass cls;
ActiveRecordModelClassSelfReference() {
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
}
ActiveRecordModelClassSelfReference() { this = cls.getClassNode().getAnOwnInstanceSelf() }
final override ActiveRecordModelClass getClass() { result = cls }
}
@@ -342,7 +366,7 @@ private class ActiveRecordModelClassSelfReference extends ActiveRecordModelInsta
class ActiveRecordInstance extends DataFlow::Node {
private ActiveRecordModelInstantiation instantiation;
ActiveRecordInstance() { this = instantiation or instantiation.flowsTo(this) }
ActiveRecordInstance() { this = instantiation.track().getAValueReachableFromSource() }
/** Gets the `ActiveRecordModelClass` that this is an instance of. */
ActiveRecordModelClass getClass() { result = instantiation.getClass() }
@@ -380,12 +404,12 @@ private module Persistence {
/** A call to e.g. `User.create(name: "foo")` */
private class CreateLikeCall extends DataFlow::CallNode, PersistentWriteAccess::Range {
CreateLikeCall() {
exists(this.asExpr().getExpr().(ActiveRecordModelClassMethodCall).getReceiverClass()) and
this.getMethodName() =
[
"create", "create!", "create_or_find_by", "create_or_find_by!", "find_or_create_by",
"find_or_create_by!", "insert", "insert!"
]
this =
activeRecordBaseClass()
.getAMethodCall([
"create", "create!", "create_or_find_by", "create_or_find_by!", "find_or_create_by",
"find_or_create_by!", "insert", "insert!"
])
}
override DataFlow::Node getValue() {
@@ -402,8 +426,7 @@ private module Persistence {
/** A call to e.g. `User.update(1, name: "foo")` */
private class UpdateLikeClassMethodCall extends DataFlow::CallNode, PersistentWriteAccess::Range {
UpdateLikeClassMethodCall() {
exists(this.asExpr().getExpr().(ActiveRecordModelClassMethodCall).getReceiverClass()) and
this.getMethodName() = ["update", "update!", "upsert"]
this = activeRecordBaseClass().getAMethodCall(["update", "update!", "upsert"])
}
override DataFlow::Node getValue() {
@@ -448,10 +471,7 @@ private module Persistence {
* ```
*/
private class TouchAllCall extends DataFlow::CallNode, PersistentWriteAccess::Range {
TouchAllCall() {
exists(this.asExpr().getExpr().(ActiveRecordModelClassMethodCall).getReceiverClass()) and
this.getMethodName() = "touch_all"
}
TouchAllCall() { this = activeRecordQueryBuilderCall("touch_all") }
override DataFlow::Node getValue() { result = this.getKeywordArgument("time") }
}
@@ -461,8 +481,7 @@ private module Persistence {
private ExprNodes::ArrayLiteralCfgNode arr;
InsertAllLikeCall() {
exists(this.asExpr().getExpr().(ActiveRecordModelClassMethodCall).getReceiverClass()) and
this.getMethodName() = ["insert_all", "insert_all!", "upsert_all"] and
this = activeRecordBaseClass().getAMethodCall(["insert_all", "insert_all!", "upsert_all"]) and
arr = this.getArgument(0).asExpr()
}

View File

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

View File

@@ -7,7 +7,9 @@
*/
module Rack {
import rack.internal.App
import rack.internal.Request
import rack.internal.Response::Public as Response
import rack.internal.Utils
/** DEPRECATED: Alias for App::AppCandidate */
deprecated class AppCandidate = App::AppCandidate;

View File

@@ -15,21 +15,20 @@ private import codeql.ruby.Concepts
* https://github.com/sparklemotion/sqlite3-ruby
*/
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. */
private DataFlow::CallNode getADatabaseMethodCall(string methodName) {
exists(API::Node dbInstance |
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())
)
)
)
result = dbInstance().getAMethodCall(methodName)
}
/** A prepared but unexecuted SQL statement. */

View File

@@ -16,50 +16,28 @@ module Twirp {
/**
* A Twirp service instantiation
*/
class ServiceInstantiation extends DataFlow::CallNode {
deprecated class ServiceInstantiation extends DataFlow::CallNode {
ServiceInstantiation() {
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.
*/
Ast::Method getAHandlerMethod() {
result = this.getAHandlerClassAstNode().getAnInstanceMethod()
DataFlow::MethodNode getAHandlerMethodNode() {
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
*/
class ClientInstantiation extends DataFlow::CallNode {
deprecated class ClientInstantiation extends DataFlow::CallNode {
ClientInstantiation() {
this = API::getTopLevelMember("Twirp").getMember("Client").getAnInstantiation()
}
@@ -67,7 +45,10 @@ module Twirp {
/** The URL of a Twirp service, considered as a 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. */
@@ -75,7 +56,14 @@ module Twirp {
DataFlow::ParameterNode
{
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" }

View File

@@ -45,6 +45,17 @@ private class NokogiriXmlParserCall extends XmlParserCall::Range, DataFlow::Call
}
}
/** Execution of a XPath statement. */
private class NokogiriXPathExecution extends XPathExecution::Range, DataFlow::CallNode {
NokogiriXPathExecution() {
exists(NokogiriXmlParserCall parserCall |
this = parserCall.getAMethodCall(["xpath", "at_xpath", "search", "at"])
)
}
override DataFlow::Node getXPath() { result = this.getArgument(0) }
}
/**
* Holds if `assign` enables the `default_substitute_entities` option in
* libxml-ruby.
@@ -123,6 +134,40 @@ private predicate xmlMiniEntitySubstitutionEnabled() {
enablesLibXmlDefaultEntitySubstitution(_)
}
/** Execution of a XPath statement. */
private class LibXmlXPathExecution extends XPathExecution::Range, DataFlow::CallNode {
LibXmlXPathExecution() {
exists(LibXmlRubyXmlParserCall parserCall |
this = parserCall.getAMethodCall(["find", "find_first"])
)
}
override DataFlow::Node getXPath() { result = this.getArgument(0) }
}
/** A call to `REXML::Document.new`, considered as a XML parsing. */
private class RexmlParserCall extends XmlParserCall::Range, DataFlow::CallNode {
RexmlParserCall() {
this = API::getTopLevelMember("REXML").getMember("Document").getAnInstantiation()
}
override DataFlow::Node getInput() { result = this.getArgument(0) }
/** No option for parsing */
override predicate externalEntitiesEnabled() { none() }
}
/** Execution of a XPath statement. */
private class RexmlXPathExecution extends XPathExecution::Range, DataFlow::CallNode {
RexmlXPathExecution() {
this =
[API::getTopLevelMember("REXML").getMember("XPath"), API::getTopLevelMember("XPath")]
.getAMethodCall(["each", "first", "match"])
}
override DataFlow::Node getXPath() { result = this.getArgument(1) }
}
/**
* A call to `ActiveSupport::XmlMini.parse` considered as an `XmlParserCall`.
*/

View File

@@ -24,7 +24,7 @@ module Gem {
GemSpec() {
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
}
@@ -42,7 +42,7 @@ module Gem {
.getBlock()
.getParameter(0)
.getMethod(name + "=")
.getParameter(0)
.getArgument(0)
.asSink()
.asExpr()
.getExpr()

View File

@@ -19,7 +19,8 @@ module Kernel {
*/
class KernelMethodCall extends DataFlow::CallNode {
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
this.asExpr().getExpr() instanceof UnknownMethodCall and
(

View File

@@ -44,7 +44,7 @@ private class SummarizedCallableFromModel extends SummarizedCallable {
override Call getACall() {
exists(API::MethodAccessNode base |
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`.
type = "any" and
n = 1 and
exists(EntryPointFromAnyType entry |
methodMatchedByName(path, entry.getName()) and
result = entry.getANode()
exists(string methodName, DataFlow::CallNode call |
methodMatchedByName(path, methodName) and
call.getMethodName() = methodName and
result.(API::MethodAccessNode).asCall() = call
)
}
@@ -112,20 +113,10 @@ API::Node getExtraNodeFromType(string type) {
constRef = getConstantFromConstPath(consts)
|
suffix = "!" and
(
result.(API::Node::Internal).asSourceInternal() = constRef
or
result.(API::Node::Internal).asSourceInternal() =
constRef.getADescendentModule().getAnOwnModuleSelf()
)
result = constRef.track()
or
suffix = "" and
(
result.(API::Node::Internal).asSourceInternal() = constRef.getAMethodCall("new")
or
result.(API::Node::Internal).asSourceInternal() =
constRef.getADescendentModule().getAnInstanceSelf()
)
result = constRef.track().getInstance()
)
or
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`.
*/
@@ -175,9 +151,11 @@ API::Node getExtraSuccessorFromNode(API::Node node, AccessPathToken token) {
result = node.getInstance()
or
token.getName() = "Parameter" and
result =
node.getASuccessor(API::Label::getLabelFromParameterPosition(FlowSummaryImplSpecific::parseArgBody(token
.getAnArgument())))
exists(DataFlowDispatch::ArgumentPosition argPos, DataFlowDispatch::ParameterPosition paramPos |
argPos = FlowSummaryImplSpecific::parseParamBody(token.getAnArgument()) and
DataFlowDispatch::parameterMatch(paramPos, argPos) and
result = node.getParameterAtPosition(paramPos)
)
or
exists(DataFlow::ContentSet contents |
SummaryComponent::content(contents) = FlowSummaryImplSpecific::interpretComponentSpecific(token) and
@@ -191,9 +169,11 @@ API::Node getExtraSuccessorFromNode(API::Node node, AccessPathToken token) {
bindingset[token]
API::Node getExtraSuccessorFromInvoke(InvokeNode node, AccessPathToken token) {
token.getName() = "Argument" and
result =
node.getASuccessor(API::Label::getLabelFromArgumentPosition(FlowSummaryImplSpecific::parseParamBody(token
.getAnArgument())))
exists(DataFlowDispatch::ArgumentPosition argPos, DataFlowDispatch::ParameterPosition paramPos |
paramPos = FlowSummaryImplSpecific::parseArgBody(token.getAnArgument()) and
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. */
class InvokeNode extends API::MethodAccessNode {
/** 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`. */

View File

@@ -19,16 +19,7 @@ private class PotentialRequestHandler extends DataFlow::CallableNode {
(
this.(DataFlow::MethodNode).getMethodName() = "call"
or
not this instanceof DataFlow::MethodNode and
exists(DataFlow::CallNode cn | cn.getMethodName() = "run" |
this.(DataFlow::LocalSourceNode).flowsTo(cn.getArgument(0))
or
// TODO: `Proc.new` should automatically propagate flow from its block argument
any(DataFlow::CallNode proc |
proc = API::getTopLevelMember("Proc").getAnInstantiation() and
proc.getBlock() = this
).(DataFlow::LocalSourceNode).flowsTo(cn.getArgument(0))
)
this = API::getTopLevelCall("run").getArgument(0).asCallable()
)
}
}

View File

@@ -0,0 +1,39 @@
/**
* Provides modeling for the `Request` component of the `Rack` library.
*/
private import codeql.ruby.AST
private import codeql.ruby.ApiGraphs
private import codeql.ruby.Concepts
private import codeql.ruby.DataFlow
/**
* Provides modeling for the `Request` component of the `Rack` library.
*/
module Request {
private class RackRequest extends API::Node {
RackRequest() { this = API::getTopLevelMember("Rack").getMember("Request").getInstance() }
}
/** An access to the parameters of a request to a rack application via a `Rack::Request` instance. */
private class RackRequestParamsAccess extends Http::Server::RequestInputAccess::Range {
RackRequestParamsAccess() {
this = any(RackRequest req).getAMethodCall(["params", "query_string", "[]", "fullpath"])
}
override string getSourceType() { result = "Rack::Request#params" }
override Http::Server::RequestInputKind getKind() {
result = Http::Server::parameterInputKind()
}
}
/** An access to the cookies of a request to a rack application via a `Rack::Request` instance. */
private class RackRequestCookiesAccess extends Http::Server::RequestInputAccess::Range {
RackRequestCookiesAccess() { this = any(RackRequest req).getAMethodCall("cookies") }
override string getSourceType() { result = "Rack::Request#cookies" }
override Http::Server::RequestInputKind getKind() { result = Http::Server::cookieInputKind() }
}
}

View File

@@ -0,0 +1,29 @@
/**
* Provides modeling for the `Utils` component of the `Rack` library.
*/
private import codeql.ruby.ApiGraphs
private import codeql.ruby.dataflow.FlowSummary
/**
* Provides modeling for the `Utils` component of the `Rack` library.
*/
module Utils {
/** Flow summary for `Rack::Utils.parse_query`, which parses a query string. */
private class ParseQuerySummary extends SummarizedCallable {
ParseQuerySummary() { this = "Rack::Utils.parse_query" }
override MethodCall getACall() {
result =
API::getTopLevelMember("Rack")
.getMember("Utils")
.getAMethodCall("parse_query")
.asExpr()
.getExpr()
}
override predicate propagatesFlowExt(string input, string output, boolean preservesValue) {
input = "Argument[0]" and output = "ReturnValue" and preservesValue = false
}
}
}

View File

@@ -15,6 +15,14 @@ private class DangerousPrefix extends string {
this = "<!--" or
this = "<" + ["iframe", "script", "cript", "scrip", "style"]
}
/**
* Gets a character that is important to the dangerous prefix.
* That is, a char that should be mentioned in a regular expression that explicitly sanitizes the dangerous prefix.
*/
string getAnImportantChar() {
if this = ["/..", "../"] then result = ["/", "."] else result = "<"
}
}
/**
@@ -62,7 +70,11 @@ private DangerousPrefixSubstring getADangerousMatchedChar(EmptyReplaceRegExpTerm
*/
private DangerousPrefix getADangerousMatchedPrefix(EmptyReplaceRegExpTerm t) {
result = getADangerousMatchedPrefixSubstring(t) and
not exists(EmptyReplaceRegExpTerm pred | pred = t.getPredecessor+() and not pred.isNullable())
not exists(EmptyReplaceRegExpTerm pred | pred = t.getPredecessor+() and not pred.isNullable()) and
// the regex must explicitly mention a char important to the prefix.
forex(string char | char = result.getAnImportantChar() |
t.getRootTerm().getAChild*().(RegExpConstant).getValue().matches("%" + char + "%")
)
}
/**

View File

@@ -45,14 +45,6 @@ module Config implements DataFlow::StateConfigSig {
}
predicate isBarrier(DataFlow::Node node) { node instanceof Sanitizer }
predicate isBarrier(DataFlow::Node node, FlowState state) { none() }
predicate isAdditionalFlowStep(
DataFlow::Node node1, FlowState state1, DataFlow::Node node2, FlowState state2
) {
none()
}
}
module Flow = DataFlow::GlobalWithState<Config>;

View File

@@ -55,7 +55,8 @@ class AmbiguousPathCall extends DataFlow::CallNode {
}
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)
}

View File

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

View File

@@ -0,0 +1,55 @@
/**
* Provides class and predicates to track external data that
* may represent malicious xpath query objects.
*
* This module is intended to be imported into a taint-tracking query.
*/
private import codeql.ruby.Concepts
private import codeql.ruby.DataFlow
private import codeql.ruby.dataflow.BarrierGuards
private import codeql.ruby.dataflow.RemoteFlowSources
/** Models Xpath Injection related classes and functions */
module XpathInjection {
/** A data flow source for "XPath injection" vulnerabilities. */
abstract class Source extends DataFlow::Node { }
/** A data flow sink for "XPath injection" vulnerabilities */
abstract class Sink extends DataFlow::Node { }
/** A sanitizer for "XPath injection" vulnerabilities. */
abstract class Sanitizer extends DataFlow::Node { }
/**
* A source of remote user input, considered as a flow source.
*/
private class RemoteFlowSourceAsSource extends Source, RemoteFlowSource { }
/**
* An execution of an XPath expression, considered as a sink.
*/
private class XPathExecutionAsSink extends Sink {
XPathExecutionAsSink() { this = any(XPathExecution e).getXPath() }
}
/**
* A construction of an XPath expression, considered as a sink.
*/
private class XPathConstructionAsSink extends Sink {
XPathConstructionAsSink() { this = any(XPathConstruction c).getXPath() }
}
/**
* A comparison with a constant string, considered as a sanitizer-guard.
*/
private class StringConstCompareAsSanitizerGuard extends Sanitizer, StringConstCompareBarrier { }
/**
* An inclusion check against an array of constant strings, considered as a
* sanitizer-guard.
*/
private class StringConstArrayInclusionCallAsSanitizer extends Sanitizer,
StringConstArrayInclusionCallBarrier
{ }
}

View File

@@ -0,0 +1,27 @@
/**
* Provides a taint-tracking configuration for detecting "Xpath Injection" vulnerabilities.
*
* Note, for performance reasons: only import this file if
* `XpathInjection::Configuration` is needed, otherwise
* `XpathInjectionCustomizations` should be imported instead.
*/
private import codeql.ruby.DataFlow
private import codeql.ruby.TaintTracking
import XpathInjectionCustomizations::XpathInjection
/** Provides a taint-tracking configuration for detecting "Xpath Injection" vulnerabilities. */
module XpathInjection {
/**
* A taint-tracking configuration for detecting "Xpath Injection" vulnerabilities.
*/
private module Config implements DataFlow::ConfigSig {
predicate isSource(DataFlow::Node source) { source instanceof Source }
predicate isSink(DataFlow::Node sink) { sink instanceof Sink }
predicate isBarrier(DataFlow::Node node) { node instanceof Sanitizer }
}
import TaintTracking::Global<Config>
}

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]
private TypeTracker noContentTypeTracker(boolean hasCall) {
result = MkTypeTracker(hasCall, noContent())
}
/** Gets a type tracker with no content and the call bit set to the given value. */
cached
TypeTracker noContentTypeTracker(boolean hasCall) { result = MkTypeTracker(hasCall, noContent()) }
/** Gets the summary resulting from appending `step` to type-tracking summary `tt`. */
cached
@@ -318,6 +317,8 @@ class StepSummary extends TStepSummary {
/** Provides predicates for updating step summaries (`StepSummary`s). */
module StepSummary {
predicate append = Cached::append/2;
/**
* Gets the summary that corresponds to having taken a forwards
* inter-procedural step from `nodeFrom` to `nodeTo`.
@@ -378,6 +379,35 @@ module StepSummary {
}
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.
*/
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]

View File

@@ -1,5 +1,5 @@
name: codeql/ruby-all
version: 0.6.4-dev
version: 0.7.1-dev
groups: ruby
extractor: ruby
dbscheme: ruby.dbscheme

View File

@@ -1,3 +1,19 @@
## 0.7.0
### Minor Analysis Improvements
* Fixed a bug in how `map_filter` calls are analyzed. Previously, such calls would
appear to the return the receiver of the call, but now the return value of the callback
is properly taken into account.
### Bug Fixes
* The experimental query "Arbitrary file write during zipfile/tarfile extraction" (`ruby/zipslip`) has been renamed to "Arbitrary file access during archive extraction ("Zip Slip")."
## 0.6.4
No user-facing changes.
## 0.6.3
### Minor Analysis Improvements

View File

@@ -0,0 +1,4 @@
---
category: newQuery
---
* Added a new experimental query, `rb/xpath-injection`, to detect cases where XPath statements are constructed from user input in an unsafe manner.

View File

@@ -1,6 +0,0 @@
---
category: minorAnalysis
---
* Fixed a bug in how `map_filter` calls are analyzed. Previously, such calls would
appear to the return the receiver of the call, but now the return value of the callback
is properly taken into account.

View File

@@ -1,4 +0,0 @@
---
category: fix
---
* The experimental query "Arbitrary file write during zipfile/tarfile extraction" (`ruby/zipslip`) has been renamed to "Arbitrary file access during archive extraction ("Zip Slip")."

View File

@@ -0,0 +1,4 @@
---
category: minorAnalysis
---
* Improved resolution of calls performed on an object created with `Proc.new`.

View File

@@ -0,0 +1,3 @@
## 0.6.4
No user-facing changes.

View File

@@ -0,0 +1,11 @@
## 0.7.0
### Minor Analysis Improvements
* Fixed a bug in how `map_filter` calls are analyzed. Previously, such calls would
appear to the return the receiver of the call, but now the return value of the callback
is properly taken into account.
### Bug Fixes
* The experimental query "Arbitrary file write during zipfile/tarfile extraction" (`ruby/zipslip`) has been renamed to "Arbitrary file access during archive extraction ("Zip Slip")."

View File

@@ -1,2 +1,2 @@
---
lastReleaseVersion: 0.6.3
lastReleaseVersion: 0.7.0

View File

@@ -0,0 +1,37 @@
<!DOCTYPE qhelp PUBLIC "-//Semmle//qhelp//EN" "qhelp.dtd">
<qhelp>
<overview>
<p>
If an XPath expression is built using string concatenation, and the components of the concatenation
include user input, it makes it very easy for a user to create a malicious XPath expression.
</p>
</overview>
<recommendation>
<p>
If user input must be included in an XPath expression, either sanitize the data or use variable
references to safely embed it without altering the structure of the expression.
</p>
</recommendation>
<example>
<p>
The following example uses the <code>nokogiri</code>, <code>rexml</code> and <code>libxml</code> XML parsers to parse a string <code>xml</code>.
Then the xpath query is controlled by the user and hence leads to a vulnerability.
</p>
<sample src="examples/XPathBad.rb"/>
<p>
To guard against XPath Injection attacks, the user input should be sanitized.
</p>
<sample src="examples/XPathGood.rb"/>
</example>
<references>
<li>
OWASP:
<a href="https://owasp.org/www-community/attacks/XPATH_Injection">XPath injection</a>.
</li>
</references>
</qhelp>

View File

@@ -0,0 +1,21 @@
/**
* @name XPath query built from user-controlled sources
* @description Building a XPath query from user-controlled sources is vulnerable to insertion of
* malicious XPath code by the user.
* @kind path-problem
* @problem.severity error
* @security-severity 9.8
* @precision high
* @id rb/xpath-injection
* @tags security
* external/cwe/cwe-643
*/
import codeql.ruby.DataFlow
import codeql.ruby.security.XpathInjectionQuery
import XpathInjection::PathGraph
from XpathInjection::PathNode source, XpathInjection::PathNode sink
where XpathInjection::flowPath(source, sink)
select sink.getNode(), source, sink, "XPath expression depends on a $@.", source.getNode(),
"user-provided value"

View File

@@ -0,0 +1,45 @@
require 'nokogiri'
require 'rexml'
require 'libxml'
class BadNokogiriController < ActionController::Base
def some_request_handler
name = params["name"]
xml = <<-XML
<root>
<foo>bar</foo>
<password>THIS IS SECRET</password>
</root>
XML
doc = Nokogiri::XML.parse(xml)
results = doc.xpath("//#{name}")
end
end
class BadRexmlController < ActionController::Base
def some_request_handler
name = params["name"]
xml = <<-XML
<root>
<foo>bar</foo>
<password>THIS IS SECRET</password>
</root>
XML
doc = REXML::Document.new(xml)
results = REXML::XPath.first(doc, "//#{name}")
end
end
class BadLibxmlController < ActionController::Base
def some_request_handler
name = params["name"]
xml = <<-XML
<root>
<foo>bar</foo>
<password>THIS IS SECRET</password>
</root>
XML
doc = LibXML::XML::Document.string(xml)
results = doc.find_first("//#{name}")
end
end

View File

@@ -0,0 +1,60 @@
require 'nokogiri'
require 'rexml'
require 'libxml'
class BadNokogiriController < ActionController::Base
def some_request_handler
name = params["name"]
xml = <<-XML
<root>
<foo>bar</foo>
<password>THIS IS SECRET</password>
</root>
XML
doc = Nokogiri::XML.parse(xml)
name = if ["foo", "foo2"].include? name
name
else
name = "foo"
end
results = doc.xpath("//#{name}")
end
end
class BadRexmlController < ActionController::Base
def some_request_handler
name = params["name"]
xml = <<-XML
<root>
<foo>bar</foo>
<password>THIS IS SECRET</password>
</root>
XML
doc = REXML::Document.new(xml)
name = if ["foo", "foo2"].include? name
name
else
name = "foo"
end
results = REXML::XPath.first(doc, "//#{name}")
end
end
class BadLibxmlController < ActionController::Base
def some_request_handler
name = params["name"]
xml = <<-XML
<root>
<foo>bar</foo>
<password>THIS IS SECRET</password>
</root>
XML
doc = LibXML::XML::Document.string(xml)
name = if ["foo", "foo2"].include? name
name
else
name = "foo"
end
results = doc.find_first("//#{name}")
end
end

View File

@@ -1,5 +1,5 @@
name: codeql/ruby-queries
version: 0.6.4-dev
version: 0.7.1-dev
groups:
- ruby
- queries

View File

@@ -0,0 +1,117 @@
/**
* @id rb/performance-diagnostics
* @kind table
*/
/*
* This query outputs some numbers that can be used to help narrow down the cause of performance
* issues in a codebase, without leaking any actual code or identifiers from the codebase.
*/
import ruby
import codeql.ruby.ApiGraphs
query int numberOfModuleBases() { result = count(Ast::ModuleBase cls) }
query int numberOfClasses() { result = count(Ast::ClassDeclaration cls) }
query int numberOfMethods() { result = count(Ast::MethodBase method) }
query int numberOfCallables() { result = count(Ast::Callable c) }
query int numberOfMethodCalls() { result = count(Ast::MethodCall call) }
query int numberOfCalls() { result = count(Ast::Call call) }
signature module HistogramSig {
bindingset[this]
class Bucket;
int getCounts(Bucket bucket);
}
module MakeHistogram<HistogramSig H> {
predicate histogram(int bucketSize, int frequency) {
frequency = strictcount(H::Bucket bucket | H::getCounts(bucket) = bucketSize)
}
}
module MethodNames implements HistogramSig {
class Bucket = string;
int getCounts(string name) {
result = strictcount(Ast::MethodBase method | method.getName() = name) and
name != "initialize"
}
}
query predicate numberOfMethodsWithNameHistogram = MakeHistogram<MethodNames>::histogram/2;
module CallTargets implements HistogramSig {
class Bucket = Ast::Call;
int getCounts(Ast::Call call) { result = count(call.getATarget()) }
}
query predicate numberOfCallTargetsHistogram = MakeHistogram<CallTargets>::histogram/2;
module Callers implements HistogramSig {
class Bucket = Ast::Callable;
int getCounts(Ast::Callable callable) {
result = count(Ast::Call call | call.getATarget() = callable)
}
}
query predicate numberOfCallersHistogram = MakeHistogram<Callers>::histogram/2;
private DataFlow::MethodNode getAnOverriddenMethod(DataFlow::MethodNode method) {
exists(DataFlow::ModuleNode cls, string name |
method = cls.getInstanceMethod(name) and
result = cls.getAnAncestor().getInstanceMethod(name) and
result != method
)
}
module MethodOverrides implements HistogramSig {
class Bucket = DataFlow::MethodNode;
int getCounts(DataFlow::MethodNode method) { result = count(getAnOverriddenMethod(method)) }
}
query predicate numberOfOverriddenMethodsHistogram = MakeHistogram<MethodOverrides>::histogram/2;
module MethodOverriddenBy implements HistogramSig {
class Bucket = DataFlow::MethodNode;
int getCounts(DataFlow::MethodNode method) {
result = count(DataFlow::MethodNode overrider | method = getAnOverriddenMethod(overrider))
}
}
query predicate numberOfOverridingMethodsHistogram = MakeHistogram<MethodOverriddenBy>::histogram/2;
module Ancestors implements HistogramSig {
class Bucket = DataFlow::ModuleNode;
int getCounts(DataFlow::ModuleNode mod) {
result =
count(DataFlow::ModuleNode ancestor | ancestor = mod.getAnAncestor() and ancestor != mod)
}
}
query predicate numberOfAncestorsHistogram = MakeHistogram<Ancestors>::histogram/2;
module Descendents implements HistogramSig {
class Bucket = DataFlow::ModuleNode;
int getCounts(DataFlow::ModuleNode mod) {
not mod.getQualifiedName() = ["Object", "Kernel", "BasicObject", "Class", "Module"] and
result =
count(DataFlow::ModuleNode descendent |
descendent = mod.getADescendent() and descendent != mod
)
}
}
query predicate numberOfDescendentsHistogram = MakeHistogram<Descendents>::histogram/2;

View File

@@ -1,8 +1,8 @@
classMethodCalls
| test1.rb:58:1:58:8 | Use getMember("M1").getMember("C1").getMethod("m").getReturn() |
| test1.rb:59:1:59:8 | Use getMember("M2").getMember("C3").getMethod("m").getReturn() |
| test1.rb:58:1:58:8 | ForwardNode(call to m) |
| test1.rb:59:1:59:8 | ForwardNode(call to m) |
instanceMethodCalls
| test1.rb:61:1:61:12 | Use getMember("M1").getMember("C1").getMethod("new").getReturn().getMethod("m").getReturn() |
| test1.rb:62:1:62:12 | Use getMember("M2").getMember("C3").getMethod("new").getReturn().getMethod("m").getReturn() |
| test1.rb:61:1:61:12 | ForwardNode(call to m) |
| test1.rb:62:1:62:12 | ForwardNode(call to m) |
flowThroughArray
| 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()
a.something #$ use=getMember("Something").getMethod("foo").getReturn().getMethod("withCallback").getBlock().getParameter(0).getMethod("something").getReturn()
b.somethingElse #$ use=getMember("Something").getMethod("foo").getReturn().getMethod("withCallback").getBlock().getParameter(1).getMethod("somethingElse").getReturn()
end #$ use=getMember("Something").getMethod("foo").getReturn().getMethod("withCallback").getReturn()
Something.foo.withCallback do |a, b| #$ source=Member[Something].Method[foo].ReturnValue
a.something #$ source=Member[Something].Method[foo].ReturnValue.Method[withCallback].Argument[block].Argument[0].Method[something].ReturnValue
b.somethingElse #$ source=Member[Something].Method[foo].ReturnValue.Method[withCallback].Argument[block].Argument[1].Method[somethingElse].ReturnValue
end #$ source=Member[Something].Method[foo].ReturnValue.Method[withCallback].ReturnValue
Something.withNamedArg do |a:, b: nil| #$ use=getMember("Something")
a.something #$ use=getMember("Something").getMethod("withNamedArg").getBlock().getKeywordParameter("a").getMethod("something").getReturn()
b.somethingElse #$ use=getMember("Something").getMethod("withNamedArg").getBlock().getKeywordParameter("b").getMethod("somethingElse").getReturn()
end #$ use=getMember("Something").getMethod("withNamedArg").getReturn()
Something.withNamedArg do |a:, b: nil| #$ source=Member[Something]
a.something #$ source=Member[Something].Method[withNamedArg].Argument[block].Parameter[a:].Method[something].ReturnValue
b.somethingElse #$ source=Member[Something].Method[withNamedArg].Argument[block].Parameter[b:].Method[somethingElse].ReturnValue
end #$ source=Member[Something].Method[withNamedArg].ReturnValue
Something.withLambda ->(a, b) { #$ use=getMember("Something")
a.something #$ use=getMember("Something").getMethod("withLambda").getParameter(0).getParameter(0).getMethod("something").getReturn()
b.something #$ use=getMember("Something").getMethod("withLambda").getParameter(0).getParameter(1).getMethod("something").getReturn()
} #$ use=getMember("Something").getMethod("withLambda").getReturn()
Something.withLambda ->(a, b) { #$ source=Member[Something]
a.something #$ source=Member[Something].Method[withLambda].Argument[0].Parameter[0].Method[something].ReturnValue
b.something #$ source=Member[Something].Method[withLambda].Argument[0].Parameter[1].Method[something].ReturnValue
} #$ source=Member[Something].Method[withLambda].ReturnValue
Something.namedCallback( #$ use=getMember("Something")
Something.namedCallback( #$ source=Member[Something]
onEvent: ->(a, b) {
a.something #$ use=getMember("Something").getMethod("namedCallback").getKeywordParameter("onEvent").getParameter(0).getMethod("something").getReturn()
b.something #$ use=getMember("Something").getMethod("namedCallback").getKeywordParameter("onEvent").getParameter(1).getMethod("something").getReturn()
a.something #$ source=Member[Something].Method[namedCallback].Argument[onEvent:].Parameter[0].Method[something].ReturnValue
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")
a.nestedCall2 do |b:| #$ use=getMember("Something").getMethod("nestedCall1").getBlock().getParameter(0)
b.something #$ use=getMember("Something").getMethod("nestedCall1").getBlock().getParameter(0).getMethod("nestedCall2").getBlock().getKeywordParameter("b").getMethod("something").getReturn()
end #$ use=getMember("Something").getMethod("nestedCall1").getBlock().getParameter(0).getMethod("nestedCall2").getReturn()
end #$ use=getMember("Something").getMethod("nestedCall1").getReturn()
Something.nestedCall1 do |a| #$ source=Member[Something]
a.nestedCall2 do |b:| #$ reachableFromSource=Member[Something].Method[nestedCall1].Argument[block].Parameter[0]
b.something #$ source=Member[Something].Method[nestedCall1].Argument[block].Parameter[0].Method[nestedCall2].Argument[block].Parameter[b:].Method[something].ReturnValue
end #$ source=Member[Something].Method[nestedCall1].Argument[block].Parameter[0].Method[nestedCall2].ReturnValue
end #$ source=Member[Something].Method[nestedCall1].ReturnValue
def getCallback()
->(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
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")
a.something #$ use=getMember("Something").getMethod("withMixed").getBlock().getParameter(0).getMethod("something").getReturn()
Something.withMixed do |a, *args, b| #$ source=Member[Something]
a.something #$ source=Member[Something].Method[withMixed].Argument[block].Parameter[0].Method[something].ReturnValue
# 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,11 @@
Foo.bar proc { |x|
x # $ reachableFromSource=Member[Foo].Method[bar].Argument[0].Parameter[0]
}
Foo.bar lambda { |x|
x # $ reachableFromSource=Member[Foo].Method[bar].Argument[0].Parameter[0]
}
Foo.bar Proc.new { |x|
x # $ reachableFromSource=Member[Foo].Method[bar].Argument[0].Parameter[0]
}

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

@@ -2816,6 +2816,7 @@
| file://:0:0:0:0 | [summary param] position 0 in Mysql2::Client.escape() | file://:0:0:0:0 | [summary] to write: ReturnValue in Mysql2::Client.escape() |
| file://:0:0:0:0 | [summary param] position 0 in Mysql2::Client.new() | file://:0:0:0:0 | [summary] to write: ReturnValue in Mysql2::Client.new() |
| file://:0:0:0:0 | [summary param] position 0 in PG.new() | file://:0:0:0:0 | [summary] to write: ReturnValue in PG.new() |
| file://:0:0:0:0 | [summary param] position 0 in Rack::Utils.parse_query | file://:0:0:0:0 | [summary] to write: ReturnValue in Rack::Utils.parse_query |
| file://:0:0:0:0 | [summary param] position 0 in SQLite3::Database.quote() | file://:0:0:0:0 | [summary] to write: ReturnValue in SQLite3::Database.quote() |
| file://:0:0:0:0 | [summary param] position 0 in Sequel.connect | file://:0:0:0:0 | [summary] to write: ReturnValue in Sequel.connect |
| file://:0:0:0:0 | [summary param] position 0 in String.try_convert | file://:0:0:0:0 | [summary] to write: ReturnValue in String.try_convert |

View File

@@ -1,6 +1,8 @@
sourceTest
| hello_world_server.rb:8:13:8:15 | req |
| hello_world_server.rb:32:18:32:20 | req |
ssrfSinkTest
| hello_world_client.rb:6:47:6:75 | "http://localhost:8080/twirp" |
serviceInstantiationTest
| 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 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
# test: request
def hello(req, env)
def hello(req, env)
puts ">> Hello #{req.name}"
{message: "Hello #{req.name}"}
end
@@ -13,7 +13,7 @@ end
class FakeHelloWorldHandler
# test: !request
def hello(req, env)
def hello(req, env)
puts ">> Hello #{req.name}"
{message: "Hello #{req.name}"}
end
@@ -21,9 +21,18 @@ end
handler = HelloWorldHandler.new()
# test: serviceInstantiation
service = Example::HelloWorld::HelloWorldService.new(handler)
service = Example::HelloWorld::HelloWorldService.new(handler)
path_prefix = "/twirp/" + service.full_name
server = WEBrick::HTTPServer.new(Port: 8080)
server.mount path_prefix, Rack::Handler::WEBrick, service
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 |
| invalid | invalid |
mimeTypeInstances
| mime_type.rb:2:6:2:28 | Use getMember("Mime").getContent(element_text/html) |
| mime_type.rb:3:6:3:32 | Use getMember("Mime").getMember("Type").getMethod("new").getReturn() |
| mime_type.rb:4:6:4:35 | Use getMember("Mime").getMember("Type").getMethod("lookup").getReturn() |
| mime_type.rb:5:6:5:43 | Use getMember("Mime").getMember("Type").getMethod("lookup_by_extension").getReturn() |
| mime_type.rb:6:6:6:47 | Use getMember("Mime").getMember("Type").getMethod("register").getReturn() |
| mime_type.rb:7:6:7:64 | Use getMember("Mime").getMember("Type").getMethod("register_alias").getReturn() |
| mime_type.rb:2:6:2:28 | ForwardNode(call to fetch) |
| mime_type.rb:3:6:3:32 | ForwardNode(call to new) |
| mime_type.rb:4:6:4:35 | ForwardNode(call to lookup) |
| mime_type.rb:5:6:5:43 | ForwardNode(call to lookup_by_extension) |
| mime_type.rb:6:6:6:47 | ForwardNode(call to register) |
| mime_type.rb:7:6:7:64 | ForwardNode(call to register_alias) |
mimeTypeMatchRegExpInterpretations
| mime_type.rb:11:11:11:19 | "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:13:5:13:40 | call to find_by |
| 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:55:5:57:7 | if ... |
| ActiveRecord.rb:55:43:56:40 | then ... |
@@ -107,12 +108,14 @@ activeRecordSqlExecutionRanges
| ActiveRecord.rb:19:16:19:24 | condition |
| ActiveRecord.rb:28:30:28:44 | ...[...] |
| ActiveRecord.rb:29:20:29:42 | "id = '#{...}'" |
| ActiveRecord.rb:30:21:30:45 | call to [] |
| ActiveRecord.rb:30:22:30:44 | "id = '#{...}'" |
| ActiveRecord.rb:31:16:31:21 | <<-SQL |
| ActiveRecord.rb:34:20:34:47 | "user.id = '#{...}'" |
| ActiveRecord.rb:46:20:46:32 | ... + ... |
| ActiveRecord.rb:52:16:52:28 | "name #{...}" |
| ActiveRecord.rb:56:20:56:39 | "username = #{...}" |
| ActiveRecord.rb:68:21:68:44 | ...[...] |
| ActiveRecord.rb:106:27:106:76 | "this is an unsafe annotation:..." |
activeRecordModelClassMethodCalls
| 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:34:5:34:14 | call to where |
| 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:37:5:37:36 | call to not_a_find_by_method |
| 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:60:5:60:33 | call to find_by |
| 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:76:5:76:66 | 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:16:3:16:18 | call to belongs_to |
| 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
| ActiveRecord.rb:9:5:9:68 | call to find |
| 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 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()
}

View File

@@ -33,6 +33,13 @@ modelInstances
| active_resource.rb:26:9:26:14 | people |
| active_resource.rb:26:9:26:20 | call to first |
| 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
| active_resource.rb:6:1:6:10 | call to save |
| 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:10:24:26 | call to find |
| 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
query predicate modelClasses(
ActiveResource::ModelClass c, DataFlow::Node siteAssignCall, boolean disablesCertificateValidation
ActiveResource::ModelClassNode c, DataFlow::Node siteAssignCall,
boolean disablesCertificateValidation
) {
c.getASiteAssignment() = siteAssignCall and
if c.disablesCertificateValidation(siteAssignCall)
@@ -13,8 +14,16 @@ query predicate modelClasses(
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 collections(ActiveResource::Collection c) { any() }
deprecated query predicate collections(ActiveResource::Collection c) { any() }
query predicate collectionSources(ActiveResource::CollectionSource c) { any() }

View File

@@ -6,7 +6,9 @@ rackRequestHandlers
| rack.rb:60:3:62:5 | call | rack.rb:60:12:60:14 | env | rack.rb:66:7:66:24 | call to [] |
| rack.rb:60:3:62:5 | call | rack.rb:60:12:60:14 | env | rack.rb:73:5:73:23 | call to [] |
| rack.rb:79:3:81:5 | call | rack.rb:79:17:79:19 | env | rack.rb:93:5:93:78 | call to finish |
| rack.rb:98:3:102:5 | call | rack.rb:98:12:98:14 | env | rack.rb:101:5:101:42 | call to [] |
| rack.rb:98:3:107:5 | call | rack.rb:98:12:98:14 | env | rack.rb:110:5:110:28 | call to [] |
| rack.rb:98:3:107:5 | call | rack.rb:98:12:98:14 | env | rack.rb:114:5:114:30 | call to [] |
| rack.rb:119:3:123:5 | call | rack.rb:119:12:119:14 | env | rack.rb:122:5:122:42 | call to [] |
| rack_apps.rb:6:3:12:5 | call | rack_apps.rb:6:12:6:14 | env | rack_apps.rb:10:12:10:34 | call to [] |
| rack_apps.rb:16:3:18:5 | call | rack_apps.rb:16:17:16:19 | env | rack_apps.rb:17:5:17:28 | call to [] |
| rack_apps.rb:21:14:21:50 | -> { ... } | rack_apps.rb:21:17:21:19 | env | rack_apps.rb:21:24:21:48 | call to [] |
@@ -18,4 +20,7 @@ redirectResponses
| rack.rb:43:5:43:45 | call to [] | rack.rb:42:30:42:40 | "/foo.html" |
| rack.rb:93:5:93:78 | call to finish | rack.rb:93:60:93:70 | redirect_to |
requestInputAccesses
| rack.rb:99:14:99:32 | ...[...] |
| rack.rb:100:18:100:28 | call to cookies |
| rack.rb:103:14:103:23 | call to params |
| rack.rb:104:18:104:32 | ...[...] |
| rack.rb:120:14:120:32 | ...[...] |

View File

@@ -94,6 +94,27 @@ class Qux
end
end
class UsesRequest
def call(env)
req = Rack::Request.new(env)
if session = req.cookies['session']
reuse_session(session)
else
name = req.params['name']
password = req['password']
login(name, password)
end
end
def login(name, password)
[200, {}, "new session"]
end
def reuse_session(name, password)
[200, {}, "reuse session"]
end
end
class UsesEnvQueryParams
def call(env)
params = env['QUERY_STRING']

View File

@@ -0,0 +1,59 @@
require 'libxml'
class FooController < ActionController::Base
def libxml_handler(event:, context:)
name = params[:user_name]
xml = <<-XML
<root>
<foo>bar</foo>
<password>THIS IS SECRET</password>
</root>
XML
# Parse the XML
doc = LibXML::XML::Document.string(xml)
# GOOD: XPath query is not constructed from user input
results1 = doc.find_first('//foo')
# BAD: XPath query is constructed from user input
results2 = doc.find_first("//#{name}")
# GOOD: XPath query is not constructed from user input
results3 = doc.find('//foo')
# BAD: XPath query is constructed from user input
results4 = doc.find("//#{name}")
end
end
class BarController < ActionController::Base
def libxml_safe_handler(event:, context:)
safe_name = params[:user_name]
xml = <<-XML
<root>
<foo>bar</foo>
<password>THIS IS SECRET</password>
</root>
XML
# Parse the XML
doc = REXML::Document.new(xml)
# GOOD: barrier guard prevents taint flow
safe_name = if ["foo", "foo2"].include? safe_name
safe_name
else
safe_name = "foo"
end
# GOOD: XPath query is not constructed from unsanitized user input
results5 = doc.find_first("//#{safe_name}")
# GOOD: XPath query is not constructed from unsanitized user input
results6 = doc.find("//#{safe_name}")
end
end

View File

@@ -0,0 +1,88 @@
require 'nokogiri'
class FooController < ActionController::Base
def nokogiri_handler(event:, context:)
name = params[:user_name]
xml = <<-XML
<root>
<foo>bar</foo>
<password>THIS IS SECRET</password>
</root>
XML
# Parse the XML
doc = Nokogiri::XML.parse(xml)
# GOOD: XPath query is not constructed from user input
results1 = doc.at('//foo')
# BAD: XPath query is constructed from user input
results2 = doc.at("//#{name}")
# GOOD: XPath query is not constructed from user input
results3 = doc.xpath('//foo')
# BAD: XPath query is constructed from user input
results4 = doc.xpath("//#{name}")
# GOOD: XPath query is not constructed from user input
results5 = doc.at_xpath('//foo')
# BAD: XPath query is constructed from user input
results6 = doc.at_xpath("//#{name}")
# GOOD: XPath query is not constructed from user input
doc.xpath('//foo').each do |element|
puts element.text
end
# BAD: XPath query constructed from user input
doc.xpath("//#{name}").each do |element|
puts element.text
end
# GOOD: XPath query is not constructed from user input
doc.search('//foo').each do |element|
puts element.text
end
# BAD: XPath query constructed from user input
doc.search("//#{name}").each do |element|
puts element.text
end
end
end
class BarController < ActionController::Base
def nokogiri_safe_handler(event:, context:)
safe_name = params[:user_name]
xml = <<-XML
<root>
<foo>bar</foo>
<password>THIS IS SECRET</password>
</root>
XML
# Parse the XML
doc = Nokogiri::XML.parse(xml)
# GOOD: barrier guard prevents taint flow
safe_name = if ["foo", "foo2"].include? safe_name
safe_name
else
safe_name = "foo"
end
# GOOD: XPath query is not constructed from unsanitized user input
results7 = doc.at("//#{safe_name}")
# GOOD: XPath query is not constructed from unsanitized user input
results8 = doc.xpath("//#{safe_name}")
# GOOD: XPath query is not constructed from unsanitized user input
results9 = doc.at_xpath("//#{safe_name}")
end
end

View File

@@ -0,0 +1,69 @@
require 'rexml'
class FooController < ActionController::Base
def rexml_handler(event:, context:)
name = params[:user_name]
xml = <<-XML
<root>
<foo>bar</foo>
<password>THIS IS SECRET</password>
</root>
XML
# Parse the XML
doc = REXML::Document.new(xml)
# GOOD: XPath query is not constructed from user input
results1 = REXML::XPath.first(doc, "//foo")
# BAD: XPath query is constructed from user input
results2 = REXML::XPath.first(doc, "//#{name}")
# GOOD: XPath query is not constructed from user input
results3 = REXML::XPath.match(doc, "//foo", nil)
# BAD: XPath query is constructed from user input
results4 = REXML::XPath.match(doc, "//#{name}", nil)
# GOOD: XPath query is not constructed from user input
REXML::XPath.each(doc, "//foo") do |element|
puts element.text
end
# BAD: XPath query constructed from user input
REXML::XPath.each(doc, "//#{name}") do |element|
puts element.text
end
end
end
class BarController < ActionController::Base
def rexml_safe_handler(event:, context:)
safe_name = params[:user_name]
xml = <<-XML
<root>
<foo>bar</foo>
<password>THIS IS SECRET</password>
</root>
XML
# Parse the XML
doc = REXML::Document.new(xml)
# GOOD: barrier guard prevents taint flow
safe_name = if ["foo", "foo2"].include? safe_name
safe_name
else
safe_name = "foo"
end
# GOOD: XPath query is not constructed from unsanitized user input
results5 = REXML::XPath.first(doc, "//#{safe_name}")
# GOOD: XPath query is not constructed from unsanitized user input
results6 = REXML::XPath.match(doc, "//#{safe_name}", nil)
end
end

View File

@@ -0,0 +1,49 @@
edges
| LibxmlInjection.rb:5:5:5:8 | name | LibxmlInjection.rb:21:31:21:41 | "//#{...}" |
| LibxmlInjection.rb:5:5:5:8 | name | LibxmlInjection.rb:27:25:27:35 | "//#{...}" |
| LibxmlInjection.rb:5:12:5:17 | call to params | LibxmlInjection.rb:5:12:5:29 | ...[...] |
| LibxmlInjection.rb:5:12:5:29 | ...[...] | LibxmlInjection.rb:5:5:5:8 | name |
| NokogiriInjection.rb:5:5:5:8 | name | NokogiriInjection.rb:21:23:21:33 | "//#{...}" |
| NokogiriInjection.rb:5:5:5:8 | name | NokogiriInjection.rb:27:26:27:36 | "//#{...}" |
| NokogiriInjection.rb:5:5:5:8 | name | NokogiriInjection.rb:33:29:33:39 | "//#{...}" |
| NokogiriInjection.rb:5:5:5:8 | name | NokogiriInjection.rb:41:15:41:25 | "//#{...}" |
| NokogiriInjection.rb:5:5:5:8 | name | NokogiriInjection.rb:51:16:51:26 | "//#{...}" |
| NokogiriInjection.rb:5:12:5:17 | call to params | NokogiriInjection.rb:5:12:5:29 | ...[...] |
| NokogiriInjection.rb:5:12:5:29 | ...[...] | NokogiriInjection.rb:5:5:5:8 | name |
| RexmlInjection.rb:5:5:5:8 | name | RexmlInjection.rb:21:40:21:50 | "//#{...}" |
| RexmlInjection.rb:5:5:5:8 | name | RexmlInjection.rb:27:40:27:50 | "//#{...}" |
| RexmlInjection.rb:5:5:5:8 | name | RexmlInjection.rb:35:28:35:38 | "//#{...}" |
| RexmlInjection.rb:5:12:5:17 | call to params | RexmlInjection.rb:5:12:5:29 | ...[...] |
| RexmlInjection.rb:5:12:5:29 | ...[...] | RexmlInjection.rb:5:5:5:8 | name |
nodes
| LibxmlInjection.rb:5:5:5:8 | name | semmle.label | name |
| LibxmlInjection.rb:5:12:5:17 | call to params | semmle.label | call to params |
| LibxmlInjection.rb:5:12:5:29 | ...[...] | semmle.label | ...[...] |
| LibxmlInjection.rb:21:31:21:41 | "//#{...}" | semmle.label | "//#{...}" |
| LibxmlInjection.rb:27:25:27:35 | "//#{...}" | semmle.label | "//#{...}" |
| NokogiriInjection.rb:5:5:5:8 | name | semmle.label | name |
| NokogiriInjection.rb:5:12:5:17 | call to params | semmle.label | call to params |
| NokogiriInjection.rb:5:12:5:29 | ...[...] | semmle.label | ...[...] |
| NokogiriInjection.rb:21:23:21:33 | "//#{...}" | semmle.label | "//#{...}" |
| NokogiriInjection.rb:27:26:27:36 | "//#{...}" | semmle.label | "//#{...}" |
| NokogiriInjection.rb:33:29:33:39 | "//#{...}" | semmle.label | "//#{...}" |
| NokogiriInjection.rb:41:15:41:25 | "//#{...}" | semmle.label | "//#{...}" |
| NokogiriInjection.rb:51:16:51:26 | "//#{...}" | semmle.label | "//#{...}" |
| RexmlInjection.rb:5:5:5:8 | name | semmle.label | name |
| RexmlInjection.rb:5:12:5:17 | call to params | semmle.label | call to params |
| RexmlInjection.rb:5:12:5:29 | ...[...] | semmle.label | ...[...] |
| RexmlInjection.rb:21:40:21:50 | "//#{...}" | semmle.label | "//#{...}" |
| RexmlInjection.rb:27:40:27:50 | "//#{...}" | semmle.label | "//#{...}" |
| RexmlInjection.rb:35:28:35:38 | "//#{...}" | semmle.label | "//#{...}" |
subpaths
#select
| LibxmlInjection.rb:21:31:21:41 | "//#{...}" | LibxmlInjection.rb:5:12:5:17 | call to params | LibxmlInjection.rb:21:31:21:41 | "//#{...}" | XPath expression depends on a $@. | LibxmlInjection.rb:5:12:5:17 | call to params | user-provided value |
| LibxmlInjection.rb:27:25:27:35 | "//#{...}" | LibxmlInjection.rb:5:12:5:17 | call to params | LibxmlInjection.rb:27:25:27:35 | "//#{...}" | XPath expression depends on a $@. | LibxmlInjection.rb:5:12:5:17 | call to params | user-provided value |
| NokogiriInjection.rb:21:23:21:33 | "//#{...}" | NokogiriInjection.rb:5:12:5:17 | call to params | NokogiriInjection.rb:21:23:21:33 | "//#{...}" | XPath expression depends on a $@. | NokogiriInjection.rb:5:12:5:17 | call to params | user-provided value |
| NokogiriInjection.rb:27:26:27:36 | "//#{...}" | NokogiriInjection.rb:5:12:5:17 | call to params | NokogiriInjection.rb:27:26:27:36 | "//#{...}" | XPath expression depends on a $@. | NokogiriInjection.rb:5:12:5:17 | call to params | user-provided value |
| NokogiriInjection.rb:33:29:33:39 | "//#{...}" | NokogiriInjection.rb:5:12:5:17 | call to params | NokogiriInjection.rb:33:29:33:39 | "//#{...}" | XPath expression depends on a $@. | NokogiriInjection.rb:5:12:5:17 | call to params | user-provided value |
| NokogiriInjection.rb:41:15:41:25 | "//#{...}" | NokogiriInjection.rb:5:12:5:17 | call to params | NokogiriInjection.rb:41:15:41:25 | "//#{...}" | XPath expression depends on a $@. | NokogiriInjection.rb:5:12:5:17 | call to params | user-provided value |
| NokogiriInjection.rb:51:16:51:26 | "//#{...}" | NokogiriInjection.rb:5:12:5:17 | call to params | NokogiriInjection.rb:51:16:51:26 | "//#{...}" | XPath expression depends on a $@. | NokogiriInjection.rb:5:12:5:17 | call to params | user-provided value |
| RexmlInjection.rb:21:40:21:50 | "//#{...}" | RexmlInjection.rb:5:12:5:17 | call to params | RexmlInjection.rb:21:40:21:50 | "//#{...}" | XPath expression depends on a $@. | RexmlInjection.rb:5:12:5:17 | call to params | user-provided value |
| RexmlInjection.rb:27:40:27:50 | "//#{...}" | RexmlInjection.rb:5:12:5:17 | call to params | RexmlInjection.rb:27:40:27:50 | "//#{...}" | XPath expression depends on a $@. | RexmlInjection.rb:5:12:5:17 | call to params | user-provided value |
| RexmlInjection.rb:35:28:35:38 | "//#{...}" | RexmlInjection.rb:5:12:5:17 | call to params | RexmlInjection.rb:35:28:35:38 | "//#{...}" | XPath expression depends on a $@. | RexmlInjection.rb:5:12:5:17 | call to params | user-provided value |

View File

@@ -0,0 +1 @@
experimental/xpath-injection/XpathInjection.ql

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: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: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: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] |
@@ -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: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: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/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] |
@@ -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: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: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: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:17:86:28 | call to handle | semmle.label | call to handle |
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: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: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: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 |

View File

@@ -63,7 +63,7 @@
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.raw_name.html_safe
@@ -75,10 +75,10 @@
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 %>
<%# 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 %>
<%# 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: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: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: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 | ...[...] |
@@ -35,6 +36,7 @@ edges
| 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: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: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:..." |
@@ -102,6 +104,8 @@ nodes
| 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: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:31 | ...[...] | semmle.label | ...[...] |
| 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: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: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: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: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: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: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 |
@@ -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: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: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: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 |
@@ -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: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: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 |