mirror of
https://github.com/github/codeql.git
synced 2026-04-25 08:45:14 +02:00
Merge remote-tracking branch 'origin/main' into rb/rack-env-query-string
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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.
|
||||
@@ -1,4 +0,0 @@
|
||||
---
|
||||
category: minorAnalysis
|
||||
---
|
||||
* HTTP redirect responses from Rack applications are now recognized as a potential sink for open redirect alerts.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
5
ruby/ql/lib/change-notes/2023-07-05-rack-response.md
Normal file
5
ruby/ql/lib/change-notes/2023-07-05-rack-response.md
Normal 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.
|
||||
@@ -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.
|
||||
|
||||
3
ruby/ql/lib/change-notes/released/0.6.4.md
Normal file
3
ruby/ql/lib/change-notes/released/0.6.4.md
Normal file
@@ -0,0 +1,3 @@
|
||||
## 0.6.4
|
||||
|
||||
No user-facing changes.
|
||||
12
ruby/ql/lib/change-notes/released/0.7.0.md
Normal file
12
ruby/ql/lib/change-notes/released/0.7.0.md
Normal 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.
|
||||
@@ -1,2 +1,2 @@
|
||||
---
|
||||
lastReleaseVersion: 0.6.3
|
||||
lastReleaseVersion: 0.7.0
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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) }
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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`.
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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() }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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" }
|
||||
|
||||
@@ -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`.
|
||||
*/
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
(
|
||||
|
||||
@@ -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()
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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`. */
|
||||
|
||||
@@ -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()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
39
ruby/ql/lib/codeql/ruby/frameworks/rack/internal/Request.qll
Normal file
39
ruby/ql/lib/codeql/ruby/frameworks/rack/internal/Request.qll
Normal 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() }
|
||||
}
|
||||
}
|
||||
29
ruby/ql/lib/codeql/ruby/frameworks/rack/internal/Utils.qll
Normal file
29
ruby/ql/lib/codeql/ruby/frameworks/rack/internal/Utils.qll
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 + "%")
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
{ }
|
||||
}
|
||||
27
ruby/ql/lib/codeql/ruby/security/XpathInjectionQuery.qll
Normal file
27
ruby/ql/lib/codeql/ruby/security/XpathInjectionQuery.qll
Normal 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>
|
||||
}
|
||||
328
ruby/ql/lib/codeql/ruby/typetracking/ApiGraphShared.qll
Normal file
328
ruby/ql/lib/codeql/ruby/typetracking/ApiGraphShared.qll
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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")."
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
category: minorAnalysis
|
||||
---
|
||||
* Improved resolution of calls performed on an object created with `Proc.new`.
|
||||
3
ruby/ql/src/change-notes/released/0.6.4.md
Normal file
3
ruby/ql/src/change-notes/released/0.6.4.md
Normal file
@@ -0,0 +1,3 @@
|
||||
## 0.6.4
|
||||
|
||||
No user-facing changes.
|
||||
11
ruby/ql/src/change-notes/released/0.7.0.md
Normal file
11
ruby/ql/src/change-notes/released/0.7.0.md
Normal 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")."
|
||||
@@ -1,2 +1,2 @@
|
||||
---
|
||||
lastReleaseVersion: 0.6.3
|
||||
lastReleaseVersion: 0.7.0
|
||||
|
||||
@@ -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>
|
||||
21
ruby/ql/src/experimental/xpath-injection/XpathInjection.ql
Normal file
21
ruby/ql/src/experimental/xpath-injection/XpathInjection.ql
Normal 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"
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -1,5 +1,5 @@
|
||||
name: codeql/ruby-queries
|
||||
version: 0.6.4-dev
|
||||
version: 0.7.1-dev
|
||||
groups:
|
||||
- ruby
|
||||
- queries
|
||||
|
||||
117
ruby/ql/src/queries/diagnostics/PerformanceDiagnostics.ql
Normal file
117
ruby/ql/src/queries/diagnostics/PerformanceDiagnostics.ql
Normal 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;
|
||||
@@ -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 |
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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]
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -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 |
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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() }
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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" |
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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() }
|
||||
|
||||
@@ -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 | ...[...] |
|
||||
|
||||
@@ -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']
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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 |
|
||||
@@ -0,0 +1 @@
|
||||
experimental/xpath-injection/XpathInjection.ql
|
||||
@@ -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 |
|
||||
|
||||
@@ -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 %>
|
||||
|
||||
@@ -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 |
|
||||
|
||||
Reference in New Issue
Block a user