Merge branch 'main' into maikypedia/sqli-sink-2

This commit is contained in:
Alex Ford
2023-06-01 13:04:54 +01:00
committed by GitHub
1397 changed files with 119809 additions and 83388 deletions

View File

@@ -1,3 +1,9 @@
## 0.6.2
### Minor Analysis Improvements
* Support for the `sqlite3` gem has been added. Method calls that execute queries against an SQLite3 database that may be vulnerable to injection attacks will now be recognized.
## 0.6.1
No user-facing changes.

View File

@@ -0,0 +1,4 @@
---
category: minorAnalysis
---
* Support for the `pg` gem has been added. Method calls that execute queries against a PostgreSQL database that may be vulnerable to injection attacks will now be recognized.

View File

@@ -1,4 +1,5 @@
---
category: minorAnalysis
---
## 0.6.2
### Minor Analysis Improvements
* Support for the `sqlite3` gem has been added. Method calls that execute queries against an SQLite3 database that may be vulnerable to injection attacks will now be recognized.

View File

@@ -1,2 +1,2 @@
---
lastReleaseVersion: 0.6.1
lastReleaseVersion: 0.6.2

View File

@@ -17,7 +17,7 @@ private string locationToString(Location loc) {
*
* For more information about locations see [Locations](https://codeql.github.com/docs/writing-codeql-queries/providing-locations-in-codeql-queries/).
*/
class Location extends @location {
class Location extends @location_default {
/** Gets the file for this location. */
File getFile() { locations_default(this, result, _, _, _, _) }

View File

@@ -121,13 +121,15 @@ private Ruby::AstNode getSuperParent(Ruby::Super sup) {
result = sup
or
result = getSuperParent(sup).getParent() and
not result instanceof Ruby::Method
not result instanceof Ruby::Method and
not result instanceof Ruby::SingletonMethod
}
private string getSuperMethodName(Ruby::Super sup) {
exists(Ruby::Method meth |
meth = getSuperParent(sup).getParent() and
exists(Ruby::AstNode meth | meth = getSuperParent(sup).getParent() |
result = any(Method c | toGenerated(c) = meth).getName()
or
result = any(SingletonMethod c | toGenerated(c) = meth).getName()
)
}

View File

@@ -936,10 +936,10 @@ module ExprNodes {
}
/** A control-flow node that wraps a `StringLiteral` AST expression. */
class StringLiteralCfgNode extends ExprCfgNode {
override string getAPrimaryQlClass() { result = "StringLiteralCfgNode" }
class StringLiteralCfgNode extends StringlikeLiteralCfgNode {
StringLiteralCfgNode() { e instanceof StringLiteral }
override StringLiteral e;
override string getAPrimaryQlClass() { result = "StringLiteralCfgNode" }
final override StringLiteral getExpr() { result = super.getExpr() }
}

View File

@@ -9,6 +9,23 @@ private import FlowSummaryImplSpecific as FlowSummaryImplSpecific
private import codeql.ruby.dataflow.FlowSummary
private import codeql.ruby.dataflow.SSA
/**
* A `LocalSourceNode` for a `self` variable. This is either an implicit `self`
* parameter or an implicit SSA entry definition.
*/
private class SelfLocalSourceNode extends DataFlow::LocalSourceNode {
private SelfVariable self;
SelfLocalSourceNode() {
self = this.(SelfParameterNode).getSelfVariable()
or
self = this.(SsaSelfDefinitionNode).getVariable()
}
/** Gets the `self` variable. */
SelfVariable getVariable() { result = self }
}
newtype TReturnKind =
TNormalReturnKind() or
TBreakReturnKind() or
@@ -316,7 +333,7 @@ private predicate extendCall(DataFlow::ExprNode receiver, Module m) {
exists(DataFlow::CallNode extendCall |
extendCall.getMethodName() = "extend" and
exists(DataFlow::LocalSourceNode sourceNode | sourceNode.flowsTo(extendCall.getArgument(_)) |
selfInModule(sourceNode.(SsaSelfDefinitionNode).getVariable(), m) or
selfInModule(sourceNode.(SelfLocalSourceNode).getVariable(), m) or
m = resolveConstantReadAccess(sourceNode.asExpr().getExpr())
) and
receiver = extendCall.getReceiver()
@@ -329,7 +346,7 @@ private predicate extendCallModule(Module m, Module n) {
exists(DataFlow::LocalSourceNode receiver, DataFlow::ExprNode e |
receiver.flowsTo(e) and extendCall(e, n)
|
selfInModule(receiver.(SsaSelfDefinitionNode).getVariable(), m) or
selfInModule(receiver.(SelfLocalSourceNode).getVariable(), m) or
m = resolveConstantReadAccess(receiver.asExpr().getExpr())
)
}
@@ -502,12 +519,12 @@ private predicate isStandardNewCall(RelevantCall new, Module m, boolean exact) {
exact = true
or
// `self.new` inside a module
selfInModule(sourceNode.(SsaSelfDefinitionNode).getVariable(), m) and
selfInModule(sourceNode.(SelfLocalSourceNode).getVariable(), m) and
exact = true
or
// `self.new` inside a singleton method
exists(MethodBase caller |
selfInMethod(sourceNode.(SsaSelfDefinitionNode).getVariable(), caller, m) and
selfInMethod(sourceNode.(SelfLocalSourceNode).getVariable(), caller, m) and
singletonMethod(caller, _, _) and
exact = false
)
@@ -573,7 +590,7 @@ private predicate isInstance(DataFlow::Node n, Module tp, boolean exact) {
// `self` reference in method or top-level (but not in module or singleton method,
// where instance methods cannot be called; only singleton methods)
n =
any(SsaSelfDefinitionNode self |
any(SelfLocalSourceNode self |
exists(MethodBase m |
selfInMethod(self.getVariable(), m, tp) and
not m instanceof SingletonMethod and
@@ -607,10 +624,10 @@ private DataFlow::Node trackInstance(Module tp, boolean exact, TypeTracker t) {
m = resolveConstantReadAccess(result.asExpr().getExpr())
or
// needed for e.g. `self.include`
selfInModule(result.(SsaSelfDefinitionNode).getVariable(), m)
selfInModule(result.(SelfLocalSourceNode).getVariable(), m)
or
// needed for e.g. `self.puts`
selfInMethod(result.(SsaSelfDefinitionNode).getVariable(), any(SingletonMethod sm), m)
selfInMethod(result.(SelfLocalSourceNode).getVariable(), any(SingletonMethod sm), m)
)
)
or
@@ -970,7 +987,7 @@ private DataFlow::Node trackSingletonMethodOnInstance(MethodBase method, string
/** Holds if a `self` access may be the receiver of `call` directly inside module `m`. */
pragma[nomagic]
private predicate selfInModuleFlowsToMethodCallReceiver(RelevantCall call, Module m, string method) {
exists(SsaSelfDefinitionNode self |
exists(SelfLocalSourceNode self |
flowsToMethodCallReceiver(call, self, method) and
selfInModule(self.getVariable(), m)
)
@@ -984,7 +1001,7 @@ pragma[nomagic]
private predicate selfInSingletonMethodFlowsToMethodCallReceiver(
RelevantCall call, Module m, string method
) {
exists(SsaSelfDefinitionNode self, MethodBase caller |
exists(SelfLocalSourceNode self, MethodBase caller |
flowsToMethodCallReceiver(call, self, method) and
selfInMethod(self.getVariable(), caller, m) and
singletonMethod(caller, _, _)
@@ -1062,10 +1079,13 @@ private CfgScope getTargetSingleton(RelevantCall call, string method) {
*/
pragma[nomagic]
private predicate argMustFlowToReceiver(
RelevantCall ctx, DataFlow::LocalSourceNode source, DataFlow::Node arg,
SsaDefinitionExtNode paramDef, RelevantCall call, Callable encl, string name
RelevantCall ctx, DataFlow::LocalSourceNode source, DataFlow::Node arg, RelevantCall call,
Callable encl, string name
) {
exists(ParameterNodeImpl p, ParameterPosition ppos, ArgumentPosition apos |
exists(
ParameterNodeImpl p, SsaDefinitionExtNode paramDef, ParameterPosition ppos,
ArgumentPosition apos
|
// the receiver of `call` references `p`
exists(DataFlow::Node receiver |
LocalFlow::localFlowSsaParamInput(p, paramDef) and
@@ -1106,7 +1126,7 @@ private predicate mayBenefitFromCallContextInitialize(
RelevantCall ctx, RelevantCall new, DataFlow::Node arg, Callable encl, Module tp, string name
) {
exists(DataFlow::LocalSourceNode source |
argMustFlowToReceiver(ctx, pragma[only_bind_into](source), arg, _, new, encl, "new") and
argMustFlowToReceiver(ctx, pragma[only_bind_into](source), arg, new, encl, "new") and
source = trackModuleAccess(tp) and
name = "initialize" and
exists(lookupMethod(tp, name))
@@ -1127,7 +1147,7 @@ private predicate mayBenefitFromCallContextInstance(
string name
) {
exists(DataFlow::LocalSourceNode source |
argMustFlowToReceiver(ctx, pragma[only_bind_into](source), arg, _, call, encl,
argMustFlowToReceiver(ctx, pragma[only_bind_into](source), arg, call, encl,
pragma[only_bind_into](name)) and
source = trackInstance(tp, exact) and
exists(lookupMethod(tp, pragma[only_bind_into](name)))
@@ -1148,7 +1168,7 @@ private predicate mayBenefitFromCallContextSingleton(
string name
) {
exists(DataFlow::LocalSourceNode source |
argMustFlowToReceiver(ctx, pragma[only_bind_into](source), pragma[only_bind_into](arg), _, call,
argMustFlowToReceiver(ctx, pragma[only_bind_into](source), pragma[only_bind_into](arg), call,
encl, pragma[only_bind_into](name)) and
exists(lookupSingletonMethod(tp, pragma[only_bind_into](name), exact))
|
@@ -1216,13 +1236,10 @@ DataFlowCallable viableImplInCallContext(DataFlowCall call, DataFlowCall ctx) {
or
// `ctx` cannot provide a type bound, and the receiver of the call is `self`;
// in this case, still apply an open-world assumption
exists(
RelevantCall call0, RelevantCall ctx0, DataFlow::Node arg, SsaSelfDefinitionNode self,
string name
|
exists(RelevantCall call0, RelevantCall ctx0, DataFlow::Node arg, string name |
call0 = call.asCall() and
ctx0 = ctx.asCall() and
argMustFlowToReceiver(ctx0, _, arg, self, call0, _, name) and
argMustFlowToReceiver(ctx0, _, arg, call0, _, name) and
not mayBenefitFromCallContextInitialize(ctx0, call0, arg, _, _, _) and
not mayBenefitFromCallContextInstance(ctx0, call0, arg, _, _, _, name) and
not mayBenefitFromCallContextSingleton(ctx0, call0, arg, _, _, _, name) and
@@ -1230,7 +1247,7 @@ DataFlowCallable viableImplInCallContext(DataFlowCall call, DataFlowCall ctx) {
)
or
// library calls should always be able to resolve
argMustFlowToReceiver(ctx.asCall(), _, _, _, call.asCall(), _, _) and
argMustFlowToReceiver(ctx.asCall(), _, _, call.asCall(), _, _) and
result = viableLibraryCallable(call)
)
}

View File

@@ -96,7 +96,8 @@ module LocalFlow {
exists(BasicBlock bb, int i |
lastRefBeforeRedefExt(def, bb, i, next.getDefinitionExt()) and
def = nodeFrom.getDefinitionExt() and
def.definesAt(_, bb, i, _)
def.definesAt(_, bb, i, _) and
nodeFrom != next
)
}
@@ -143,12 +144,13 @@ module LocalFlow {
* This is intended to recover from flow not currently recognised by ordinary capture flow.
*/
predicate localFlowSsaParamCaptureInput(Node nodeFrom, Node nodeTo) {
exists(Ssa::CapturedEntryDefinition def |
nodeFrom.asParameter().(NamedParameter).getVariable() = def.getSourceVariable()
or
nodeFrom.(SelfParameterNode).getSelfVariable() = def.getSourceVariable()
|
exists(Ssa::CapturedEntryDefinition def, ParameterNodeImpl p |
(nodeFrom = p or LocalFlow::localFlowSsaParamInput(p, nodeFrom)) and
nodeTo.(SsaDefinitionExtNode).getDefinitionExt() = def
|
p.getParameter().(NamedParameter).getVariable() = def.getSourceVariable()
or
p.(SelfParameterNode).getSelfVariable() = def.getSourceVariable()
)
}
@@ -939,6 +941,12 @@ private class NewCall extends DataFlowCall {
abstract class ReturningNode extends Node {
/** Gets the kind of this return node. */
abstract ReturnKind getKind();
pragma[nomagic]
predicate hasKind(ReturnKind kind, CfgScope scope) {
kind = this.getKind() and
scope = this.(NodeImpl).getCfgScope()
}
}
/** A data-flow node that represents a value returned by a callable. */
@@ -1059,10 +1067,8 @@ private module ReturnNodes {
SynthReturnNode() { this = TSynthReturnNode(scope, kind) }
/** Gets a syntactic return node that flows into this synthetic node. */
ReturningNode getAnInput() {
result.(NodeImpl).getCfgScope() = scope and
result.getKind() = kind
}
pragma[nomagic]
ReturningNode getAnInput() { result.hasKind(kind, scope) }
override ReturnKind getKind() { result = kind }

View File

@@ -166,28 +166,21 @@ module Public {
SummaryComponentStack return(ReturnKind rk) { result = singleton(SummaryComponent::return(rk)) }
}
private predicate noComponentSpecific(SummaryComponent sc) {
not exists(getComponentSpecific(sc))
}
/** Gets a textual representation of this component used for flow summaries. */
private string getComponent(SummaryComponent sc) {
result = getComponentSpecific(sc)
or
noComponentSpecific(sc) and
(
exists(ArgumentPosition pos |
sc = TParameterSummaryComponent(pos) and
result = "Parameter[" + getArgumentPosition(pos) + "]"
)
or
exists(ParameterPosition pos |
sc = TArgumentSummaryComponent(pos) and
result = "Argument[" + getParameterPosition(pos) + "]"
)
or
sc = TReturnSummaryComponent(getReturnValueKind()) and result = "ReturnValue"
exists(ArgumentPosition pos |
sc = TParameterSummaryComponent(pos) and
result = "Parameter[" + getArgumentPosition(pos) + "]"
)
or
exists(ParameterPosition pos |
sc = TArgumentSummaryComponent(pos) and
result = "Argument[" + getParameterPosition(pos) + "]"
)
or
sc = TReturnSummaryComponent(getReturnValueKind()) and result = "ReturnValue"
}
/** Gets a textual representation of this stack used for flow summaries. */
@@ -335,7 +328,7 @@ module Public {
class NeutralCallable extends SummarizedCallableBase {
private Provenance provenance;
NeutralCallable() { neutralElement(this, provenance) }
NeutralCallable() { neutralSummaryElement(this, provenance) }
/**
* Holds if the neutral is auto generated.

View File

@@ -62,10 +62,11 @@ predicate summaryElement(
}
/**
* Holds if a neutral model exists for `c` with provenance `provenance`,
* Holds if a neutral summary model exists for `c` with provenance `provenance`,
* which means that there is no flow through `c`.
* Note. Neutral models have not been implemented for Ruby.
*/
predicate neutralElement(FlowSummary::SummarizedCallable c, string provenance) { none() }
predicate neutralSummaryElement(FlowSummary::SummarizedCallable c, string provenance) { none() }
bindingset[arg]
private SummaryComponent interpretElementArg(string arg) {

View File

@@ -112,6 +112,13 @@ private module Cached {
)
}
cached
predicate summaryThroughStepTaint(
DataFlow::Node arg, DataFlow::Node out, FlowSummaryImpl::Public::SummarizedCallable sc
) {
FlowSummaryImpl::Private::Steps::summaryThroughStepTaint(arg, out, sc)
}
/**
* Holds if taint propagates from `nodeFrom` to `nodeTo` in exactly one local
* (intra-procedural) step.
@@ -122,7 +129,7 @@ private module Cached {
defaultAdditionalTaintStep(nodeFrom, nodeTo) or
// Simple flow through library code is included in the exposed local
// step relation, even though flow is technically inter-procedural
FlowSummaryImpl::Private::Steps::summaryThroughStepTaint(nodeFrom, nodeTo, _)
summaryThroughStepTaint(nodeFrom, nodeTo, _)
}
}

View File

@@ -0,0 +1,29 @@
/**
* Provides default sources, sinks and sanitizers for detecting
* "Unicode transformation"
* vulnerabilities, as well as extension points for adding your own.
*/
private import ruby
/**
* Provides default sources, sinks and sanitizers for detecting
* "Unicode transformation"
* vulnerabilities, as well as extension points for adding your own.
*/
module UnicodeBypassValidation {
/**
* A data flow source for "Unicode transformation" vulnerabilities.
*/
abstract class Source extends DataFlow::Node { }
/**
* A data flow sink for "Unicode transformation" vulnerabilities.
*/
abstract class Sink extends DataFlow::Node { }
/**
* A sanitizer for "Unicode transformation" vulnerabilities.
*/
abstract class Sanitizer extends DataFlow::Node { }
}

View File

@@ -0,0 +1,123 @@
/**
* Provides a taint-tracking configuration for detecting "Unicode transformation mishandling" vulnerabilities.
*/
private import ruby
private import codeql.ruby.dataflow.RemoteFlowSources
private import codeql.ruby.Concepts
private import codeql.ruby.TaintTracking
private import codeql.ruby.ApiGraphs
import UnicodeBypassValidationCustomizations::UnicodeBypassValidation
/** A state signifying that a logical validation has not been performed. */
class PreValidation extends DataFlow::FlowState {
PreValidation() { this = "PreValidation" }
}
/** A state signifying that a logical validation has been performed. */
class PostValidation extends DataFlow::FlowState {
PostValidation() { this = "PostValidation" }
}
/**
* A taint-tracking configuration for detecting "Unicode transformation mishandling" vulnerabilities.
*
* This configuration uses two flow states, `PreValidation` and `PostValidation`,
* to track the requirement that a logical validation has been performed before the Unicode Transformation.
*/
class Configuration extends TaintTracking::Configuration {
Configuration() { this = "UnicodeBypassValidation" }
override predicate isSource(DataFlow::Node source, DataFlow::FlowState state) {
source instanceof RemoteFlowSource and state instanceof PreValidation
}
override predicate isAdditionalTaintStep(
DataFlow::Node nodeFrom, DataFlow::FlowState stateFrom, DataFlow::Node nodeTo,
DataFlow::FlowState stateTo
) {
(
exists(Escaping escaping | nodeFrom = escaping.getAnInput() and nodeTo = escaping.getOutput())
or
exists(RegexExecution re | nodeFrom = re.getString() and nodeTo = re)
or
// String Manipulation Method Calls
// https://ruby-doc.org/core-2.7.0/String.html
exists(DataFlow::CallNode cn |
cn.getMethodName() =
[
[
"ljust", "lstrip", "succ", "next", "rjust", "capitalize", "chomp", "gsub", "chop",
"downcase", "swapcase", "uprcase", "scrub", "slice", "squeeze", "strip", "sub",
"tr", "tr_s", "reverse"
] + ["", "!"], "concat", "dump", "each_line", "replace", "insert", "inspect", "lines",
"partition", "prepend", "replace", "rpartition", "scan", "split", "undump",
"unpack" + ["", "1"]
] and
nodeFrom = cn.getReceiver() and
nodeTo = cn
)
or
exists(DataFlow::CallNode cn |
cn.getMethodName() =
[
"casecmp" + ["", "?"], "center", "count", "each_char", "index", "rindex", "sum",
["delete", "delete_prefix", "delete_suffix"] + ["", "!"],
["start_with", "end_with" + "eql", "include"] + ["?", "!"], "match" + ["", "?"],
] and
nodeFrom = cn.getReceiver() and
nodeTo = nodeFrom
)
or
exists(DataFlow::CallNode cn |
cn = API::getTopLevelMember("CGI").getAMethodCall("escapeHTML") and
nodeFrom = cn.getArgument(0) and
nodeTo = cn
)
) and
stateFrom instanceof PreValidation and
stateTo instanceof PostValidation
}
/* A Unicode Tranformation (Unicode tranformation) is considered a sink when the algorithm used is either NFC or NFKC. */
override predicate isSink(DataFlow::Node sink, DataFlow::FlowState state) {
(
exists(DataFlow::CallNode cn |
cn.getMethodName() = "unicode_normalize" and
cn.getArgument(0).getConstantValue().getSymbol() = ["nfkc", "nfc", "nfkd", "nfd"] and
sink = cn.getReceiver()
)
or
// unicode_utils
exists(API::MethodAccessNode mac |
mac = API::getTopLevelMember("UnicodeUtils").getMethod(["nfkd", "nfc", "nfd", "nfkc"]) and
sink = mac.getParameter(0).asSink()
)
or
// eprun
exists(API::MethodAccessNode mac |
mac = API::getTopLevelMember("Eprun").getMethod("normalize") and
sink = mac.getParameter(0).asSink()
)
or
// unf
exists(API::MethodAccessNode mac |
mac = API::getTopLevelMember("UNF").getMember("Normalizer").getMethod("normalize") and
sink = mac.getParameter(0).asSink()
)
or
// ActiveSupport::Multibyte::Chars
exists(DataFlow::CallNode cn, DataFlow::CallNode n |
cn =
API::getTopLevelMember("ActiveSupport")
.getMember("Multibyte")
.getMember("Chars")
.getMethod("new")
.getCallNode() and
n = cn.getAMethodCall("normalize") and
sink = cn.getArgument(0)
)
) and
state instanceof PostValidation
}
}

View File

@@ -0,0 +1,77 @@
/**
* Provides modeling for Pg, a Ruby library (gem) for interacting with PostgreSQL databases.
*/
private import codeql.ruby.ApiGraphs
private import codeql.ruby.dataflow.FlowSummary
private import codeql.ruby.Concepts
/**
* Provides modeling for Pg, a Ruby library (gem) for interacting with PostgreSQL databases.
*/
module Pg {
/**
* Flow summary for `PG.new()`. This method initializes a database connection.
*/
private class SqlSummary extends SummarizedCallable {
SqlSummary() { this = "PG.new()" }
override MethodCall getACall() { result = any(PgConnection c).asExpr().getExpr() }
override predicate propagatesFlowExt(string input, string output, boolean preservesValue) {
input = "Argument[0]" and output = "ReturnValue" and preservesValue = false
}
}
/** A call to PG::Connection.open() is used to establish a connection to a PostgreSQL database. */
private class PgConnection extends DataFlow::CallNode {
PgConnection() {
this =
API::getTopLevelMember("PG")
.getMember("Connection")
.getAMethodCall(["open", "new", "connect_start"])
or
this = API::getTopLevelMember("PG").getAnInstantiation()
}
}
/** A call that prepares an SQL statement to be executed later. */
private class PgPrepareCall extends SqlConstruction::Range, DataFlow::CallNode {
private DataFlow::Node query;
private PgConnection pgConnection;
private string queryName;
PgPrepareCall() {
this = pgConnection.getAMethodCall("prepare") and
queryName = this.getArgument(0).getConstantValue().getStringlikeValue() and
query = this.getArgument(1)
}
PgConnection getConnection() { result = pgConnection }
string getQueryName() { result = queryName }
override DataFlow::Node getSql() { result = query }
}
/** A call that executes SQL statements against a PostgreSQL database. */
private class PgExecution extends SqlExecution::Range, DataFlow::CallNode {
private DataFlow::Node query;
PgExecution() {
exists(PgConnection pgConnection |
this =
pgConnection.getAMethodCall(["exec", "async_exec", "exec_params", "async_exec_params"]) and
query = this.getArgument(0)
or
exists(PgPrepareCall prepareCall |
pgConnection = prepareCall.getConnection() and
this.getArgument(0).getConstantValue().isStringlikeValue(prepareCall.getQueryName()) and
query = prepareCall.getSql()
)
)
}
override DataFlow::Node getSql() { result = query }
}
}

View File

@@ -583,7 +583,8 @@ module Array {
private class DeleteUnknownSummary extends DeleteSummary {
DeleteUnknownSummary() {
this = "delete" and
// Note: take care to avoid a name clash with the "delete" summary from String.qll
this = "delete-unknown-key" and
not exists(DataFlow::Content::getKnownElementIndex(mc.getArgument(0)))
}

View File

@@ -57,36 +57,40 @@ module Gem {
}
/** Gets the name of the gem */
string getName() { result = getSpecProperty("name").getConstantValue().getString() }
string getName() { result = this.getSpecProperty("name").getConstantValue().getString() }
/** Gets a path that is loaded when the gem is required */
private string getARequirePath() {
result = getSpecProperty(["require_paths", "require_path"]).getConstantValue().getString()
result =
this.getSpecProperty(["require_paths", "require_path"]).getConstantValue().getString()
or
not exists(getSpecProperty(["require_paths", "require_path"]).getConstantValue().getString()) and
not exists(
this.getSpecProperty(["require_paths", "require_path"]).getConstantValue().getString()
) and
result = "lib" // the default is "lib"
}
/** Gets a file that could be loaded when the gem is required. */
private File getAPossiblyRequiredFile() {
result = File.super.getParentContainer().getFolder(getARequirePath()).getAChildContainer*()
result =
File.super.getParentContainer().getFolder(this.getARequirePath()).getAChildContainer*()
}
/** Gets a class/module that is exported by this gem. */
private ModuleBase getAPublicModule() {
result.(Toplevel).getLocation().getFile() = getAPossiblyRequiredFile()
result.(Toplevel).getLocation().getFile() = this.getAPossiblyRequiredFile()
or
result = getAPublicModule().getAModule()
result = this.getAPublicModule().getAModule()
or
result = getAPublicModule().getAClass()
result = this.getAPublicModule().getAClass()
or
result = getAPublicModule().getStmt(_).(SingletonClass)
result = this.getAPublicModule().getStmt(_).(SingletonClass)
}
/** Gets a parameter from an exported method, which is an input to this gem. */
DataFlow::ParameterNode getAnInputParameter() {
exists(MethodBase method |
method = getAPublicModule().getAMethod() and
method = this.getAPublicModule().getAMethod() and
result.getParameter() = method.getAParameter()
|
method.isPublic()

View File

@@ -199,11 +199,13 @@ module Hash {
}
}
private class AssocUnknownSummary extends AssocSummary {
AssocUnknownSummary() {
this = "assoc" and
mc.getNumberOfArguments() = 1 and
not exists(DataFlow::Content::getKnownElementIndex(mc.getArgument(0)))
private class AssocUnknownSummary extends SummarizedCallable {
AssocUnknownSummary() { this = "assoc-unknown-arg" }
override MethodCall getACallSimple() {
result.getMethodName() = "assoc" and
result.getNumberOfArguments() = 1 and
not exists(DataFlow::Content::getKnownElementIndex(result.getArgument(0)))
}
override predicate propagatesFlowExt(string input, string output, boolean preservesValue) {

View File

@@ -21,6 +21,7 @@ private import codeql.ruby.typetracking.TypeTracker
private import codeql.ruby.ApiGraphs
private import codeql.ruby.Concepts
private import codeql.ruby.dataflow.internal.DataFlowPrivate as DataFlowPrivate
private import codeql.ruby.dataflow.internal.TaintTrackingPrivate as TaintTrackingPrivate
private import codeql.ruby.TaintTracking
private import codeql.ruby.frameworks.core.String
@@ -37,43 +38,6 @@ DataFlow::LocalSourceNode strStart() {
/** Gets a dataflow node for a regular expression literal. */
DataFlow::LocalSourceNode regStart() { result.asExpr().getExpr() instanceof Ast::RegExpLiteral }
/**
* Holds if the analysis should track flow from `nodeFrom` to `nodeTo` on top of the ordinary type-tracking steps.
* `nodeFrom` and `nodeTo` has type `fromType` and `toType` respectively.
* The types are either "string" or "regexp".
*/
predicate step(
DataFlow::Node nodeFrom, DataFlow::LocalSourceNode nodeTo, string fromType, string toType
) {
fromType = toType and
fromType = "string" and
(
// include taint flow through `String` summaries
TaintTracking::localTaintStep(nodeFrom, nodeTo) and
nodeFrom.(DataFlowPrivate::SummaryNode).getSummarizedCallable() instanceof
String::SummarizedCallable
or
// string concatenations, and
exists(CfgNodes::ExprNodes::OperationCfgNode op |
op = nodeTo.asExpr() and
op.getAnOperand() = nodeFrom.asExpr() and
op.getExpr().(Ast::BinaryOperation).getOperator() = "+"
)
or
// string interpolations
nodeFrom.asExpr() =
nodeTo.asExpr().(CfgNodes::ExprNodes::StringlikeLiteralCfgNode).getAComponent()
)
or
fromType = "string" and
toType = "reg" and
exists(DataFlow::CallNode call |
call = API::getTopLevelMember("Regexp").getAMethodCall(["compile", "new"]) and
nodeFrom = call.getArgument(0) and
nodeTo = call
)
}
/** Gets a node where string values that flow to the node are interpreted as regular expressions. */
DataFlow::Node stringSink() {
result instanceof RE::RegExpInterpretation::Range and
@@ -91,28 +55,139 @@ DataFlow::Node stringSink() {
/** Gets a node where regular expressions that flow to the node are used. */
DataFlow::Node regSink() { result = any(RegexExecution exec).getRegex() }
/** Gets a node that is reachable by type-tracking from any string or regular expression. */
DataFlow::LocalSourceNode forward(TypeTracker t) {
t.start() and
result = [strStart(), regStart()]
or
exists(TypeTracker t2 | result = forward(t2).track(t2, t))
or
exists(TypeTracker t2 | t2 = t.continue() | step(forward(t2).getALocalUse(), result, _, _))
private signature module TypeTrackInputSig {
DataFlow::LocalSourceNode start(TypeTracker t, DataFlow::Node start);
predicate end(DataFlow::Node n);
predicate additionalStep(DataFlow::Node nodeFrom, DataFlow::LocalSourceNode nodeTo);
}
/**
* Gets a node that is backwards reachable from any regular expression use,
* where that use is reachable by type-tracking from any string or regular expression.
* Provides a version of type tracking where we first prune for reachable nodes,
* before doing the type tracking computation.
*/
DataFlow::LocalSourceNode backwards(TypeBackTracker t) {
t.start() and
result.flowsTo([stringSink(), regSink()]) and
result = forward(TypeTracker::end())
or
exists(TypeBackTracker t2 | result = backwards(t2).backtrack(t2, t))
or
exists(TypeBackTracker t2 | t2 = t.continue() | step(result.getALocalUse(), backwards(t2), _, _))
private module TypeTrack<TypeTrackInputSig Input> {
private predicate additionalStep(
DataFlow::LocalSourceNode nodeFrom, DataFlow::LocalSourceNode nodeTo
) {
Input::additionalStep(nodeFrom.getALocalUse(), nodeTo)
}
/** Gets a node that is forwards reachable by type-tracking. */
pragma[nomagic]
private DataFlow::LocalSourceNode forward(TypeTracker t) {
result = Input::start(t, _)
or
exists(TypeTracker t2 | result = forward(t2).track(t2, t))
or
exists(TypeTracker t2 | t2 = t.continue() | additionalStep(forward(t2), result))
}
bindingset[result, tbt]
pragma[inline_late]
pragma[noopt]
private DataFlow::LocalSourceNode forwardLateInline(TypeBackTracker tbt) {
exists(TypeTracker tt |
result = forward(tt) and
tt = tbt.getACompatibleTypeTracker()
)
}
/** Gets a node that is backwards reachable by type-tracking. */
pragma[nomagic]
private DataFlow::LocalSourceNode backwards(TypeBackTracker t) {
result = forwardLateInline(t) and
(
t.start() and
Input::end(result.getALocalUse())
or
exists(TypeBackTracker t2 | result = backwards(t2).backtrack(t2, t))
or
exists(TypeBackTracker t2 | t2 = t.continue() | additionalStep(result, backwards(t2)))
)
}
bindingset[result, tt]
pragma[inline_late]
pragma[noopt]
private DataFlow::LocalSourceNode backwardsInlineLate(TypeTracker tt) {
exists(TypeBackTracker tbt |
result = backwards(tbt) and
tt = tbt.getACompatibleTypeTracker()
)
}
/** Holds if `n` is forwards and backwards reachable with type tracker `t`. */
pragma[nomagic]
private predicate reached(DataFlow::LocalSourceNode n, TypeTracker t) {
n = forward(t) and
n = backwardsInlineLate(t)
}
pragma[nomagic]
private TypeTracker stepReached(
TypeTracker t, DataFlow::LocalSourceNode nodeFrom, DataFlow::LocalSourceNode nodeTo
) {
exists(StepSummary summary |
StepSummary::step(nodeFrom, nodeTo, summary) and
reached(nodeFrom, t) and
reached(nodeTo, result) and
result = t.append(summary)
)
or
additionalStep(nodeFrom, nodeTo) and
reached(nodeFrom, pragma[only_bind_into](t)) and
reached(nodeTo, pragma[only_bind_into](t)) and
result = t.continue()
}
/** Gets a node that has been tracked from the start node `start`. */
DataFlow::LocalSourceNode track(DataFlow::Node start, TypeTracker t) {
t.start() and
result = Input::start(t, start) and
reached(result, t)
or
exists(TypeTracker t2 | t = stepReached(t2, track(start, t2), result))
}
}
/** Holds if `inputStr` is compiled to a regular expression that is returned at `call`. */
pragma[nomagic]
private predicate regFromString(DataFlow::LocalSourceNode inputStr, DataFlow::CallNode call) {
exists(DataFlow::Node mid |
inputStr.flowsTo(mid) and
call = API::getTopLevelMember("Regexp").getAMethodCall(["compile", "new"]) and
mid = call.getArgument(0)
)
}
private module StringTypeTrackInput implements TypeTrackInputSig {
DataFlow::LocalSourceNode start(TypeTracker t, DataFlow::Node start) {
start = strStart() and t.start() and result = start
}
predicate end(DataFlow::Node n) {
n = stringSink() or
regFromString(n, _)
}
predicate additionalStep(DataFlow::Node nodeFrom, DataFlow::LocalSourceNode nodeTo) {
// include taint flow through `String` summaries
TaintTrackingPrivate::summaryThroughStepTaint(nodeFrom, nodeTo,
any(String::SummarizedCallable c))
or
// string concatenations, and
exists(CfgNodes::ExprNodes::OperationCfgNode op |
op = nodeTo.asExpr() and
op.getAnOperand() = nodeFrom.asExpr() and
op.getExpr().(Ast::BinaryOperation).getOperator() = "+"
)
or
// string interpolations
nodeFrom.asExpr() =
nodeTo.asExpr().(CfgNodes::ExprNodes::StringlikeLiteralCfgNode).getAComponent()
}
}
/**
@@ -120,41 +195,34 @@ DataFlow::LocalSourceNode backwards(TypeBackTracker t) {
* This is used to figure out where `start` is evaluated as a regular expression against an input string,
* or where `start` is compiled into a regular expression.
*/
private DataFlow::LocalSourceNode trackStrings(DataFlow::Node start, TypeTracker t) {
result = backwards(_) and
(
private predicate trackStrings = TypeTrack<StringTypeTrackInput>::track/2;
/** Holds if `strConst` flows to a regex compilation (tracked by `t`), where the resulting regular expression is stored in `reg`. */
pragma[nomagic]
private predicate regFromStringStart(DataFlow::Node strConst, TypeTracker t, DataFlow::CallNode reg) {
regFromString(trackStrings(strConst, t), reg) and
exists(t.continue())
}
private module RegTypeTrackInput implements TypeTrackInputSig {
DataFlow::LocalSourceNode start(TypeTracker t, DataFlow::Node start) {
start = regStart() and
t.start() and
start = result and
result = strStart()
result = start
or
exists(TypeTracker t2 | result = trackStrings(start, t2).track(t2, t))
or
// an additional step from string to string
exists(TypeTracker t2 | t2 = t.continue() |
step(trackStrings(start, t2).getALocalUse(), result, "string", "string")
)
)
regFromStringStart(start, t, result)
}
predicate end(DataFlow::Node n) { n = regSink() }
predicate additionalStep(DataFlow::Node nodeFrom, DataFlow::LocalSourceNode nodeTo) { none() }
}
/**
* Gets a node that has been tracked from the regular expression `start` to some node.
* This is used to figure out where `start` is executed against an input string.
*/
private DataFlow::LocalSourceNode trackRegs(DataFlow::Node start, TypeTracker t) {
result = backwards(_) and
(
t.start() and
start = result and
result = regStart()
or
exists(TypeTracker t2 | result = trackRegs(start, t2).track(t2, t))
or
// an additional step where a string is converted to a regular expression
exists(TypeTracker t2 | t2 = t.continue() |
step(trackStrings(start, t2).getALocalUse(), result, "string", "reg")
)
)
}
private predicate trackRegs = TypeTrack<RegTypeTrackInput>::track/2;
/** Gets a node that references a regular expression. */
private DataFlow::LocalSourceNode trackRegexpType(TypeTracker t) {

View File

@@ -613,8 +613,17 @@ class TypeBackTracker extends TTypeBackTracker {
* also flow to `sink`.
*/
TypeTracker getACompatibleTypeTracker() {
exists(boolean hasCall | result = MkTypeTracker(hasCall, content) |
hasCall = false or this.hasReturn() = false
exists(boolean hasCall, OptionalTypeTrackerContent c |
result = MkTypeTracker(hasCall, c) and
(
compatibleContents(c, content)
or
content = noContent() and c = content
)
|
hasCall = false
or
this.hasReturn() = false
)
}
}

View File

@@ -80,10 +80,8 @@ predicate jumpStep = DataFlowPrivate::jumpStep/2;
pragma[nomagic]
private predicate flowThrough(DataFlowPublic::ParameterNode param) {
exists(DataFlowPrivate::ReturningNode returnNode, DataFlowDispatch::ReturnKind rk |
DataFlowPrivate::LocalFlow::getParameterDefNode(param.getParameter())
.(TypeTrackingNode)
.flowsTo(returnNode) and
rk = returnNode.getKind()
param.flowsTo(returnNode) and
returnNode.hasKind(rk, param.(DataFlowPrivate::NodeImpl).getCfgScope())
|
rk instanceof DataFlowDispatch::NormalReturnKind
or
@@ -91,12 +89,23 @@ private predicate flowThrough(DataFlowPublic::ParameterNode param) {
)
}
/** Holds if there is flow from `arg` to `p` via the call `call`, not counting `new -> initialize` call steps. */
pragma[nomagic]
predicate callStepNoInitialize(
ExprNodes::CallCfgNode call, Node arg, DataFlowPrivate::ParameterNodeImpl p
) {
exists(DataFlowDispatch::ParameterPosition pos |
argumentPositionMatch(call, arg, pos) and
p.isSourceParameterOf(DataFlowDispatch::getTarget(call), pos)
)
}
/** Holds if there is a level step from `nodeFrom` to `nodeTo`, which may depend on the call graph. */
pragma[nomagic]
predicate levelStepCall(Node nodeFrom, Node nodeTo) {
exists(DataFlowPublic::ParameterNode param |
flowThrough(param) and
callStep(nodeTo.asExpr(), nodeFrom, param)
callStepNoInitialize(nodeTo.asExpr(), nodeFrom, param)
)
}
@@ -600,14 +609,26 @@ private DataFlow::Node evaluateSummaryComponentStackLocal(
pragma[only_bind_out](tail)) and
stack = SCS::push(pragma[only_bind_out](head), pragma[only_bind_out](tail))
|
exists(DataFlowDispatch::ArgumentPosition apos, DataFlowDispatch::ParameterPosition ppos |
exists(
DataFlowDispatch::ArgumentPosition apos, DataFlowDispatch::ParameterPosition ppos,
DataFlowPrivate::ParameterNodeImpl p
|
head = SummaryComponent::parameter(apos) and
DataFlowDispatch::parameterMatch(ppos, apos) and
result.(DataFlowPrivate::ParameterNodeImpl).isSourceParameterOf(prev.asExpr().getExpr(), ppos)
p.isSourceParameterOf(prev.asExpr().getExpr(), ppos) and
// We need to include both `p` and the SSA definition for `p`, since in type-tracking
// the step from `p` to the SSA definition is considered a call step.
result =
[p.(DataFlow::Node), DataFlowPrivate::LocalFlow::getParameterDefNode(p.getParameter())]
)
or
head = SummaryComponent::return() and
result.(DataFlowPrivate::SynthReturnNode).getCfgScope() = prev.asExpr().getExpr()
exists(DataFlowPrivate::SynthReturnNode ret |
head = SummaryComponent::return() and
ret.getCfgScope() = prev.asExpr().getExpr() and
// We need to include both `ret` and `ret.getAnInput()`, since in type-tracking
// the step from `ret.getAnInput()` to `ret` is considered a return step.
result = [ret.(DataFlow::Node), ret.getAnInput()]
)
or
exists(DataFlow::ContentSet content |
head = SummaryComponent::withoutContent(content) and

View File

@@ -0,0 +1,22 @@
/**
* @name Print CFG
* @description Produces a representation of a file's Control Flow Graph.
* This query is used by the VS Code extension.
* @id rb/print-cfg
* @kind graph
* @tags ide-contextual-queries/print-cfg
*/
private import codeql.ruby.controlflow.internal.ControlFlowGraphImplShared::TestOutput
private import codeql.IDEContextual
/**
* Gets the source file to generate a CFG from.
*/
external string selectedSourceFile();
class MyRelevantNode extends RelevantNode {
MyRelevantNode() {
this.getScope().getLocation().getFile() = getFileBySourceArchiveName(selectedSourceFile())
}
}

View File

@@ -1,5 +1,5 @@
name: codeql/ruby-all
version: 0.6.2-dev
version: 0.6.3-dev
groups: ruby
extractor: ruby
dbscheme: ruby.dbscheme
@@ -12,3 +12,4 @@ dependencies:
codeql/util: ${workspace}
dataExtensions:
- codeql/ruby/frameworks/**/model.yml
warnOnImplicitThis: true

View File

@@ -1,15 +1,22 @@
// CodeQL database schema for Ruby
// Automatically generated from the tree-sitter grammar; do not edit
@location = @location_default
/*- Files and folders -*/
/**
* The location of an element.
* The location spans column `startcolumn` of line `startline` to
* column `endcolumn` of line `endline` in file `file`.
* For more information, see
* [Locations](https://codeql.github.com/docs/writing-codeql-queries/providing-locations-in-codeql-queries/).
*/
locations_default(
unique int id: @location_default,
int file: @file ref,
int start_line: int ref,
int start_column: int ref,
int end_line: int ref,
int end_column: int ref
int beginLine: int ref,
int beginColumn: int ref,
int endLine: int ref,
int endColumn: int ref
);
files(
@@ -29,9 +36,14 @@ containerparent(
unique int child: @container ref
);
sourceLocationPrefix(
string prefix: string ref
);
/*- Source location prefix -*/
/**
* The source location of the snapshot.
*/
sourceLocationPrefix(string prefix : string ref);
/*- Diagnostic messages -*/
diagnostics(
unique int id: @diagnostic,
@@ -42,6 +54,8 @@ diagnostics(
int location: @location_default ref
);
/*- Diagnostic messages: severity -*/
case @diagnostic.severity of
10 = @diagnostic_debug
| 20 = @diagnostic_info
@@ -49,7 +63,46 @@ case @diagnostic.severity of
| 40 = @diagnostic_error
;
/*- YAML -*/
#keyset[parent, idx]
yaml (unique int id: @yaml_node,
int kind: int ref,
int parent: @yaml_node_parent ref,
int idx: int ref,
string tag: string ref,
string tostring: string ref);
case @yaml_node.kind of
0 = @yaml_scalar_node
| 1 = @yaml_mapping_node
| 2 = @yaml_sequence_node
| 3 = @yaml_alias_node
;
@yaml_collection_node = @yaml_mapping_node | @yaml_sequence_node;
@yaml_node_parent = @yaml_collection_node | @file;
yaml_anchors (unique int node: @yaml_node ref,
string anchor: string ref);
yaml_aliases (unique int alias: @yaml_alias_node ref,
string target: string ref);
yaml_scalars (unique int scalar: @yaml_scalar_node ref,
int style: int ref,
string value: string ref);
yaml_errors (unique int id: @yaml_error,
string message: string ref);
yaml_locations(unique int locatable: @yaml_locatable ref,
int location: @location_default ref);
@yaml_locatable = @yaml_node | @yaml_error;
/*- Ruby dbscheme -*/
@ruby_underscore_arg = @ruby_assignment | @ruby_binary | @ruby_conditional | @ruby_operator_assignment | @ruby_range | @ruby_unary | @ruby_underscore_primary
@ruby_underscore_call_operator = @ruby_reserved_word
@@ -1375,9 +1428,10 @@ ruby_ast_node_info(
unique int node: @ruby_ast_node ref,
int parent: @ruby_ast_node_parent ref,
int parent_index: int ref,
int loc: @location ref
int loc: @location_default ref
);
/*- Erb dbscheme -*/
erb_comment_directive_child(
unique int erb_comment_directive: @erb_comment_directive ref,
unique int child: @erb_token_comment ref
@@ -1450,6 +1504,6 @@ erb_ast_node_info(
unique int node: @erb_ast_node ref,
int parent: @erb_ast_node_parent ref,
int parent_index: int ref,
int loc: @location ref
int loc: @location_default ref
);

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,2 @@
description: Sync dbscheme fragments
compatibility: full