Merge remote-tracking branch 'origin/main' into ruby/weak-cryptographic-algorithm

This commit is contained in:
Alex Ford
2022-03-31 17:17:46 +01:00
1329 changed files with 48886 additions and 11803 deletions

View File

@@ -1,4 +1,4 @@
import codeql.ruby.security.performance.RegExpTreeView
import codeql.ruby.Regexp
query predicate nonUniqueChild(RegExpParent parent, int i, RegExpTerm child) {
child = parent.getChild(i) and

View File

@@ -1,3 +1,10 @@
## 0.0.11
### Minor Analysis Improvements
* The `Regex` class is now an abstract class that extends `StringlikeLiteral` with implementations for `RegExpLiteral` and string literals that 'flow' into functions that are known to interpret string arguments as regular expressions such as `Regex.new` and `String.match`.
* The regular expression parser now groups sequences of normal characters. This reduces the number of instances of `RegExpNormalChar`.
## 0.0.10
### Minor Analysis Improvements

View File

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

View File

@@ -1,4 +0,0 @@
---
category: minorAnalysis
---
* The regular expression parser now groups sequences of normal characters. This reduces the number of instances of `RegExpNormalChar`.

View File

@@ -0,0 +1,4 @@
---
category: breaking
---
* The `getURL` member-predicates of the `HTTP::Client::Request` and `HTTP::Client::Request::Range` classes from `Concepts.qll` have been renamed to `getAUrlPart`.

View File

@@ -0,0 +1,4 @@
---
category: feature
---
* The data flow and taint tracking libraries have been extended with versions of `isBarrierIn`, `isBarrierOut`, and `isBarrierGuard`, respectively `isSanitizerIn`, `isSanitizerOut`, and `isSanitizerGuard`, that support flow states.

View File

@@ -0,0 +1,4 @@
---
category: breaking
---
* The flow state variants of `isBarrier` and `isAdditionalFlowStep` are no longer exposed in the taint tracking library. The `isSanitizer` and `isAdditionalTaintStep` predicates should be used instead.

View File

@@ -0,0 +1,5 @@
---
category: minorAnalysis
---
* `getConstantValue()` now returns the contents of strings and symbols after escape sequences have been interpreted. For example, for the Ruby string literal `"\n"`, `getConstantValue().getString()` previously returned a QL string with two characters, a backslash followed by `n`; now it returns the single-character string "\n" (U+000A, known as newline).
* `getConstantValue().getInt()` previously returned incorrect values for integers larger than 2<sup>31</sup>-1 (the largest value that can be represented by the QL `int` type). It now returns no result in those cases.

View File

@@ -0,0 +1,4 @@
---
category: minorAnalysis
---
* The `ParseRegExp` and `RegExpTreeView` modules are now "internal" modules. Users should use `codeql.ruby.Regexp` instead.

View File

@@ -0,0 +1,5 @@
---
category: minorAnalysis
---
* Whereas `ConstantValue::getString()` previously returned both string and regular-expression values, it now returns only string values. The same applies to `ConstantValue::isString(value)`.
* Regular-expression values can now be accessed with the new predicates `ConstantValue::getRegExp()`, `ConstantValue::isRegExp(value)`, and `ConstantValue::isRegExpWithFlags(value, flags)`.

View File

@@ -0,0 +1,4 @@
---
category: deprecated
---
* `ConstantValue::getStringOrSymbol` and `ConstantValue::isStringOrSymbol`, which return/hold for all string-like values (strings, symbols, and regular expressions), have been renamed to `ConstantValue::getStringlikeValue` and `ConstantValue::isStringlikeValue`, respectively. The old names have been marked as `deprecated`.

View File

@@ -1,4 +1,6 @@
---
category: minorAnalysis
---
## 0.0.11
### Minor Analysis Improvements
* The `Regex` class is now an abstract class that extends `StringlikeLiteral` with implementations for `RegExpLiteral` and string literals that 'flow' into functions that are known to interpret string arguments as regular expressions such as `Regex.new` and `String.match`.
* The regular expression parser now groups sequences of normal characters. This reduces the number of instances of `RegExpNormalChar`.

View File

@@ -1,2 +1,2 @@
---
lastReleaseVersion: 0.0.10
lastReleaseVersion: 0.0.11

View File

@@ -0,0 +1,129 @@
/**
* Provides predicates for working with numeric values and their string
* representations.
*/
/**
* Gets the integer value of `binary` when interpreted as binary. `binary` must
* contain only the digits 0 and 1. For values greater than
* 01111111111111111111111111111111 (2^31-1, the maximum value that `int` can
* represent), there is no result.
*
* ```
* "0" => 0
* "01" => 1
* "1010101" => 85
* ```
*/
bindingset[binary]
int parseBinaryInt(string binary) {
exists(string stripped | stripped = stripLeadingZeros(binary) |
stripped.length() <= 31 and
result >= 0 and
result =
sum(int index, string c, int digit |
c = stripped.charAt(index) and
digit = "01".indexOf(c)
|
twoToThe(stripped.length() - 1 - index) * digit
)
)
}
/**
* Gets the integer value of `hex` when interpreted as hex. `hex` must be a
* valid hexadecimal string. For values greater than 7FFFFFFF (2^31-1, the
* maximum value that `int` can represent), there is no result.
*
* ```
* "0" => 0
* "FF" => 255
* "f00d" => 61453
* ```
*/
bindingset[hex]
int parseHexInt(string hex) {
exists(string stripped | stripped = stripLeadingZeros(hex) |
stripped.length() <= 8 and
result >= 0 and
result =
sum(int index, string c |
c = stripped.charAt(index)
|
sixteenToThe(stripped.length() - 1 - index) * toHex(c)
)
)
}
/**
* Gets the integer value of `octal` when interpreted as octal. `octal` must be
* a valid octal string containing only the digits 0-7. For values greater than
* 17777777777 (2^31-1, the maximum value that `int` can represent), there is no
* result.
*
* ```
* "0" => 0
* "77" => 63
* "76543210" => 16434824
* ```
*/
bindingset[octal]
int parseOctalInt(string octal) {
exists(string stripped | stripped = stripLeadingZeros(octal) |
stripped.length() <= 11 and
result >= 0 and
result =
sum(int index, string c, int digit |
c = stripped.charAt(index) and
digit = "01234567".indexOf(c)
|
eightToThe(stripped.length() - 1 - index) * digit
)
)
}
/** Gets the integer value of the `hex` char. */
private int toHex(string hex) {
hex = [0 .. 9].toString() and
result = hex.toInt()
or
result = 10 and hex = ["a", "A"]
or
result = 11 and hex = ["b", "B"]
or
result = 12 and hex = ["c", "C"]
or
result = 13 and hex = ["d", "D"]
or
result = 14 and hex = ["e", "E"]
or
result = 15 and hex = ["f", "F"]
}
/**
* Gets the value of 16 to the power of `n`. Holds only for `n` in the range
* 0..7 (inclusive).
*/
int sixteenToThe(int n) {
// 16**7 is the largest power of 16 that fits in an int.
n in [0 .. 7] and result = 1.bitShiftLeft(4 * n)
}
/**
* Gets the value of 8 to the power of `n`. Holds only for `n` in the range
* 0..10 (inclusive).
*/
int eightToThe(int n) {
// 8**10 is the largest power of 8 that fits in an int.
n in [0 .. 10] and result = 1.bitShiftLeft(3 * n)
}
/**
* Gets the value of 2 to the power of `n`. Holds only for `n` in the range
* 0..30 (inclusive).
*/
int twoToThe(int n) { n in [0 .. 30] and result = 1.bitShiftLeft(n) }
/** Gets `s` with any leading "0" characters removed. */
bindingset[s]
private string stripLeadingZeros(string s) { result = s.regexpCapture("0*(.*)", 1) }

View File

@@ -266,6 +266,41 @@ module API {
/** A node corresponding to the method being invoked at a method call. */
class MethodAccessNode extends Node, Impl::MkMethodAccessNode {
override string toString() { result = "MethodAccessNode " + tryGetPath(this) }
/** Gets the call node corresponding to this method access. */
DataFlow::CallNode getCallNode() { this = Impl::MkMethodAccessNode(result) }
}
/**
* An API entry point.
*
* By default, API graph nodes are only created for nodes that come from an external
* library or escape into an external library. The points where values are cross the boundary
* between codebases are called "entry points".
*
* Anything in the global scope is considered to be an entry point, but
* additional entry points may be added by extending this class.
*/
abstract class EntryPoint extends string {
bindingset[this]
EntryPoint() { any() }
/** Gets a data-flow node corresponding to a use-node for this entry point. */
DataFlow::LocalSourceNode getAUse() { none() }
/** Gets a data-flow node corresponding to a def-node for this entry point. */
DataFlow::Node getARhs() { none() }
/** Gets a call corresponding to a method access node for this entry point. */
DataFlow::CallNode getACall() { none() }
/** Gets an API-node for this entry point. */
API::Node getANode() { result = root().getASuccessor(Label::entryPoint(this)) }
}
// Ensure all entry points are imported from ApiGraphs.qll
private module ImportEntryPoints {
private import codeql.ruby.frameworks.data.ModelsAsData
}
/** Gets the root node. */
@@ -324,7 +359,7 @@ module API {
/**
* Holds if `ref` is a use of a node that should have an incoming edge from the root
* node labeled `lbl` in the API graph.
* node labeled `lbl` in the API graph (not including those from API::EntryPoint).
*/
pragma[nomagic]
private predicate useRoot(Label::ApiLabel lbl, DataFlow::Node ref) {
@@ -371,6 +406,10 @@ module API {
useCandFwd().flowsTo(nd.(DataFlow::CallNode).getReceiver())
or
parameterStep(_, defCand(), nd)
or
nd = any(EntryPoint entry).getAUse()
or
nd = any(EntryPoint entry).getACall()
}
/**
@@ -416,6 +455,8 @@ module API {
private predicate isDef(DataFlow::Node rhs) {
// If a call node is relevant as a use-node, treat its arguments as def-nodes
argumentStep(_, useCandFwd(), rhs)
or
rhs = any(EntryPoint entry).getARhs()
}
/** Gets a data flow node that flows to the RHS of a def-node. */
@@ -432,36 +473,6 @@ module API {
/** Gets a data flow node that flows to the RHS of a def-node. */
private DataFlow::LocalSourceNode defCand() { result = defCand(TypeBackTracker::end()) }
private Label::ApiLabel getLabelFromArgumentPosition(DataFlowDispatch::ArgumentPosition pos) {
exists(int n |
pos.isPositional(n) and
result = Label::parameter(n)
)
or
exists(string name |
pos.isKeyword(name) and
result = Label::keywordParameter(name)
)
or
pos.isBlock() and
result = Label::blockParameter()
}
private Label::ApiLabel getLabelFromParameterPosition(DataFlowDispatch::ParameterPosition pos) {
exists(int n |
pos.isPositional(n) and
result = Label::parameter(n)
)
or
exists(string name |
pos.isKeyword(name) and
result = Label::keywordParameter(name)
)
or
pos.isBlock() and
result = Label::blockParameter()
}
/**
* Holds if there should be a `lbl`-edge from the given call to an argument.
*/
@@ -471,7 +482,7 @@ module API {
) {
exists(DataFlowDispatch::ArgumentPosition argPos |
argument.sourceArgumentOf(call.asExpr(), argPos) and
lbl = getLabelFromArgumentPosition(argPos)
lbl = Label::getLabelFromArgumentPosition(argPos)
)
}
@@ -484,7 +495,7 @@ module API {
) {
exists(DataFlowDispatch::ParameterPosition paramPos |
paramNode.isSourceParameterOf(callable.asExpr().getExpr(), paramPos) and
lbl = getLabelFromParameterPosition(paramPos)
lbl = Label::getLabelFromParameterPosition(paramPos)
)
}
@@ -590,6 +601,17 @@ module API {
)
)
)
or
exists(EntryPoint entry |
pred = root() and
lbl = Label::entryPoint(entry)
|
succ = MkDef(entry.getARhs())
or
succ = MkUse(entry.getAUse())
or
succ = MkMethodAccessNode(entry.getACall())
)
}
/**
@@ -619,7 +641,8 @@ module API {
or
any(DataFlowDispatch::ParameterPosition c).isPositional(n)
} or
MkLabelBlockParameter()
MkLabelBlockParameter() or
MkLabelEntryPoint(EntryPoint name)
}
/** Provides classes modeling the various edges (labels) in the API graph. */
@@ -710,6 +733,18 @@ module API {
override string toString() { result = "getBlock()" }
}
/** A label from the root node to a custom entry point. */
class LabelEntryPoint extends ApiLabel {
private API::EntryPoint name;
LabelEntryPoint() { this = MkLabelEntryPoint(name) }
override string toString() { result = name }
/** Gets the name of the entry point. */
API::EntryPoint getName() { result = name }
}
}
/** Gets the `member` edge label for member `m`. */
@@ -735,5 +770,40 @@ module API {
/** Gets the label representing the block argument/parameter. */
LabelBlockParameter blockParameter() { any() }
/** Gets the label for the edge from the root node to a custom entry point of the given name. */
LabelEntryPoint entryPoint(API::EntryPoint name) { result.getName() = name }
/** Gets the API graph label corresponding to the given argument position. */
Label::ApiLabel getLabelFromArgumentPosition(DataFlowDispatch::ArgumentPosition pos) {
exists(int n |
pos.isPositional(n) and
result = Label::parameter(n)
)
or
exists(string name |
pos.isKeyword(name) and
result = Label::keywordParameter(name)
)
or
pos.isBlock() and
result = Label::blockParameter()
}
/** Gets the API graph label corresponding to the given parameter position. */
Label::ApiLabel getLabelFromParameterPosition(DataFlowDispatch::ParameterPosition pos) {
exists(int n |
pos.isPositional(n) and
result = Label::parameter(n)
)
or
exists(string name |
pos.isKeyword(name) and
result = Label::keywordParameter(name)
)
or
pos.isBlock() and
result = Label::blockParameter()
}
}
}

View File

@@ -268,7 +268,7 @@ module HTTP {
string getUrlPattern() {
exists(CfgNodes::ExprNodes::StringlikeLiteralCfgNode strNode |
this.getUrlPatternArg().getALocalSource() = DataFlow::exprNode(strNode) and
result = strNode.getExpr().getConstantValue().getStringOrSymbol()
result = strNode.getExpr().getConstantValue().getStringlikeValue()
)
}
@@ -290,6 +290,44 @@ module HTTP {
}
}
/**
* An access to a user-controlled HTTP request input. For example, the URL or body of a request.
* Instances of this class automatically become `RemoteFlowSource`s.
*
* Extend this class to refine existing API models. If you want to model new APIs,
* extend `RequestInputAccess::Range` instead.
*/
class RequestInputAccess extends DataFlow::Node instanceof RequestInputAccess::Range {
/**
* Gets a string that describes the type of this input.
*
* This is typically the name of the method that gives rise to this input.
*/
string getSourceType() { result = super.getSourceType() }
}
/** Provides a class for modeling new HTTP request inputs. */
module RequestInputAccess {
/**
* An access to a user-controlled HTTP request input.
*
* Extend this class to model new APIs. If you want to refine existing API models,
* extend `RequestInputAccess` instead.
*/
abstract class Range extends DataFlow::Node {
/**
* Gets a string that describes the type of this input.
*
* This is typically the name of the method that gives rise to this input.
*/
abstract string getSourceType();
}
}
private class RequestInputAccessAsRemoteFlowSource extends RemoteFlowSource::Range instanceof RequestInputAccess {
override string getSourceType() { result = this.(RequestInputAccess).getSourceType() }
}
/**
* A function that will handle incoming HTTP requests.
*
@@ -343,7 +381,7 @@ module HTTP {
}
/** A parameter that will receive parts of the url when handling an incoming request. */
private class RoutedParameter extends RemoteFlowSource::Range, DataFlow::ParameterNode {
private class RoutedParameter extends RequestInputAccess::Range, DataFlow::ParameterNode {
RequestHandler handler;
RoutedParameter() { this.getParameter() = handler.getARoutedParameter() }
@@ -393,7 +431,7 @@ module HTTP {
string getMimetype() {
exists(CfgNodes::ExprNodes::StringlikeLiteralCfgNode strNode |
this.getMimetypeOrContentTypeArg().getALocalSource() = DataFlow::exprNode(strNode) and
result = strNode.getExpr().getConstantValue().getStringOrSymbol().splitAt(";", 0)
result = strNode.getExpr().getConstantValue().getStringlikeValue().splitAt(";", 0)
)
or
not exists(this.getMimetypeOrContentTypeArg()) and
@@ -447,10 +485,18 @@ module HTTP {
DataFlow::Node getResponseBody() { result = super.getResponseBody() }
/**
* DEPRECATED: Use `getAUrlPart` instead.
*
* Gets a node that contributes to the URL of the request.
* Depending on the framework, a request may have multiple nodes which contribute to the URL.
*/
DataFlow::Node getURL() { result = super.getURL() }
deprecated DataFlow::Node getURL() { result = super.getURL() or result = super.getAUrlPart() }
/**
* Gets a data-flow node that contributes to the URL of the request.
* Depending on the framework, a request may have multiple nodes which contribute to the URL.
*/
DataFlow::Node getAUrlPart() { result = super.getAUrlPart() }
/** Gets a string that identifies the framework used for this request. */
string getFramework() { result = super.getFramework() }
@@ -478,10 +524,18 @@ module HTTP {
abstract DataFlow::Node getResponseBody();
/**
* DEPRECATED: overwrite `getAUrlPart` instead.
*
* Gets a node that contributes to the URL of the request.
* Depending on the framework, a request may have multiple nodes which contribute to the URL.
*/
abstract DataFlow::Node getURL();
deprecated DataFlow::Node getURL() { none() }
/**
* Gets a data-flow node that contributes to the URL of the request.
* Depending on the framework, a request may have multiple nodes which contribute to the URL.
*/
abstract DataFlow::Node getAUrlPart();
/** Gets a string that identifies the framework used for this request. */
abstract string getFramework();

View File

@@ -0,0 +1,134 @@
/**
* Contains classes for recognizing array and string inclusion tests.
*/
private import ruby
private import codeql.ruby.DataFlow
private import codeql.ruby.controlflow.CfgNodes
/**
* An expression that checks if an element is contained in an array
* or is a substring of another string.
*
* Examples:
* ```
* A.include?(B)
* A.index(B) != nil
* A.index(B) >= 0
* ```
*/
class InclusionTest extends DataFlow::Node instanceof InclusionTest::Range {
/** Gets the `A` in `A.include?(B)`. */
final DataFlow::Node getContainerNode() { result = super.getContainerNode() }
/** Gets the `B` in `A.include?(B)`. */
final DataFlow::Node getContainedNode() { result = super.getContainedNode() }
/**
* Gets the polarity of the check.
*
* If the polarity is `false` the check returns `true` if the container does not contain
* the given element.
*/
final boolean getPolarity() { result = super.getPolarity() }
}
/**
* Contains classes for recognizing array and string inclusion tests.
*/
module InclusionTest {
/**
* An expression that is equivalent to `A.include?(B)` or `!A.include?(B)`.
*
* Note that this also includes calls to the array method named `include?`.
*/
abstract class Range extends DataFlow::Node {
/** Gets the `A` in `A.include?(B)`. */
abstract DataFlow::Node getContainerNode();
/** Gets the `B` in `A.include?(B)`. */
abstract DataFlow::Node getContainedNode();
/**
* Gets the polarity of the check.
*
* If the polarity is `false` the check returns `true` if the container does not contain
* the given element.
*/
boolean getPolarity() { result = true }
}
/**
* A call to a method named `include?`, assumed to refer to `String.include?`
* or `Array.include?`.
*/
private class Includes_Native extends Range, DataFlow::CallNode {
Includes_Native() {
this.getMethodName() = "include?" and
strictcount(this.getArgument(_)) = 1
}
override DataFlow::Node getContainerNode() { result = this.getReceiver() }
override DataFlow::Node getContainedNode() { result = this.getArgument(0) }
}
/**
* A check of form `A.index(B) != nil`, `A.index(B) >= 0`, or similar.
*/
private class Includes_IndexOfComparison extends Range, DataFlow::Node {
private DataFlow::CallNode indexOf;
private boolean polarity;
Includes_IndexOfComparison() {
exists(ExprCfgNode index, ExprNodes::ComparisonOperationCfgNode comparison, int value |
indexOf.asExpr() = comparison.getAnOperand() and
index = comparison.getAnOperand() and
this.asExpr() = comparison and
// one operand is of the form `whitelist.index(x)`
indexOf.getMethodName() = "index" and
// and the other one is 0 or -1
(
value = index.getConstantValue().getInt() and value = 0
or
index.getConstantValue().isNil() and value = -1
)
|
value = -1 and polarity = false and comparison.getExpr() instanceof CaseEqExpr
or
value = -1 and polarity = false and comparison.getExpr() instanceof EqExpr
or
value = -1 and polarity = true and comparison.getExpr() instanceof NEExpr
or
exists(RelationalOperation op | op = comparison.getExpr() |
exists(Expr lesser, Expr greater |
op.getLesserOperand() = lesser and
op.getGreaterOperand() = greater
|
polarity = true and
greater = indexOf.asExpr().getExpr() and
(
value = 0 and op.isInclusive()
or
value = -1 and not op.isInclusive()
)
or
polarity = false and
lesser = indexOf.asExpr().getExpr() and
(
value = -1 and op.isInclusive()
or
value = 0 and not op.isInclusive()
)
)
)
)
}
override DataFlow::Node getContainerNode() { result = indexOf.getReceiver() }
override DataFlow::Node getContainedNode() { result = indexOf.getArgument(0) }
override boolean getPolarity() { result = polarity }
}
}

View File

@@ -0,0 +1,143 @@
/**
* Provides classes for working with regular expressions.
*
* Regular expression literals are represented as an abstract syntax tree of regular expression
* terms.
*/
import regexp.RegExpTreeView // re-export
private import regexp.internal.ParseRegExp
private import codeql.ruby.ast.Literal as AST
private import codeql.ruby.DataFlow
private import codeql.ruby.controlflow.CfgNodes
private import codeql.ruby.ApiGraphs
private import codeql.ruby.dataflow.internal.tainttrackingforlibraries.TaintTrackingImpl
/**
* Provides utility predicates related to regular expressions.
*/
module RegExpPatterns {
/**
* Gets a pattern that matches common top-level domain names in lower case.
*/
string getACommonTld() {
// according to ranking by http://google.com/search?q=site:.<<TLD>>
result = "(?:com|org|edu|gov|uk|net|io)(?![a-z0-9])"
}
}
/**
* A node whose value may flow to a position where it is interpreted
* as a part of a regular expression.
*/
abstract class RegExpPatternSource extends DataFlow::Node {
/**
* Gets a node where the pattern of this node is parsed as a part of
* a regular expression.
*/
abstract DataFlow::Node getAParse();
/**
* Gets the root term of the regular expression parsed from this pattern.
*/
abstract RegExpTerm getRegExpTerm();
}
/**
* A regular expression literal, viewed as the pattern source for itself.
*/
private class RegExpLiteralPatternSource extends RegExpPatternSource {
private AST::RegExpLiteral astNode;
RegExpLiteralPatternSource() { astNode = this.asExpr().getExpr() }
override DataFlow::Node getAParse() { result = this }
override RegExpTerm getRegExpTerm() { result = astNode.getParsed() }
}
/**
* A node whose string value may flow to a position where it is interpreted
* as a part of a regular expression.
*/
private class StringRegExpPatternSource extends RegExpPatternSource {
private DataFlow::Node parse;
StringRegExpPatternSource() { this = regExpSource(parse) }
override DataFlow::Node getAParse() { result = parse }
override RegExpTerm getRegExpTerm() { result.getRegExp() = this.asExpr().getExpr() }
}
private class RegExpLiteralRegExp extends RegExp, AST::RegExpLiteral {
override predicate isDotAll() { this.hasMultilineFlag() }
override predicate isIgnoreCase() { this.hasCaseInsensitiveFlag() }
override string getFlags() { result = this.getFlagString() }
}
private class ParsedStringRegExp extends RegExp {
private DataFlow::Node parse;
ParsedStringRegExp() { this = regExpSource(parse).asExpr().getExpr() }
DataFlow::Node getAParse() { result = parse }
override predicate isDotAll() { none() }
override predicate isIgnoreCase() { none() }
override string getFlags() { none() }
}
/**
* Holds if `source` may be interpreted as a regular expression.
*/
private predicate isInterpretedAsRegExp(DataFlow::Node source) {
// The first argument to an invocation of `Regexp.new` or `Regexp.compile`.
source = API::getTopLevelMember("Regexp").getAMethodCall(["compile", "new"]).getArgument(0)
or
// The argument of a call that coerces the argument to a regular expression.
exists(DataFlow::CallNode mce |
mce.getMethodName() = ["match", "match?"] and
source = mce.getArgument(0) and
// exclude https://ruby-doc.org/core-2.4.0/Regexp.html#method-i-match
not mce.getReceiver().asExpr().getExpr() instanceof AST::RegExpLiteral
)
}
private class RegExpConfiguration extends Configuration {
RegExpConfiguration() { this = "RegExpConfiguration" }
override predicate isSource(DataFlow::Node source) {
source.asExpr() =
any(ExprCfgNode e |
e.getConstantValue().isString(_) and
not e instanceof ExprNodes::VariableReadAccessCfgNode and
not e instanceof ExprNodes::ConstantReadAccessCfgNode
)
}
override predicate isSink(DataFlow::Node sink) { isInterpretedAsRegExp(sink) }
override predicate isSanitizer(DataFlow::Node node) {
// stop flow if `node` is receiver of
// https://ruby-doc.org/core-2.4.0/String.html#method-i-match
exists(DataFlow::CallNode mce |
mce.getMethodName() = ["match", "match?"] and
node = mce.getReceiver() and
mce.getArgument(0).asExpr().getExpr() instanceof AST::RegExpLiteral
)
}
}
/**
* Gets a node whose value may flow (inter-procedurally) to `re`, where it is interpreted
* as a part of a regular expression.
*/
cached
DataFlow::Node regExpSource(DataFlow::Node re) {
exists(RegExpConfiguration c | c.hasFlow(result, re))
}

View File

@@ -0,0 +1,180 @@
/**
* Provides classes and predicates for reasoning about string-manipulating expressions.
*/
private import ruby
private import codeql.ruby.DataFlow
private import codeql.ruby.controlflow.CfgNodes
private import InclusionTests
/**
* Provides classes for reasoning about string-manipulating expressions.
*/
module StringOps {
/**
* A expression that is equivalent to `A.start_with?(B)` or `!A.start_with?(B)`.
*/
class StartsWith extends DataFlow::Node instanceof StartsWith::Range {
/**
* Gets the `A` in `A.start_with?(B)`.
*/
final DataFlow::Node getBaseString() { result = super.getBaseString() }
/**
* Gets the `B` in `A.start_with?(B)`.
*/
final DataFlow::Node getSubstring() { result = super.getSubstring() }
/**
* Gets the polarity of the check.
*
* If the polarity is `false` the check returns `true` if the string does not start
* with the given substring.
*/
final boolean getPolarity() { result = super.getPolarity() }
}
/**
* Provides classes implementing prefix test expressions.
*/
module StartsWith {
/**
* A expression that is equivalent to `A.start_with?(B)` or `!A.start_with?(B)`.
*/
abstract class Range extends DataFlow::Node {
/**
* Gets the `A` in `A.start_with?(B)`.
*/
abstract DataFlow::Node getBaseString();
/**
* Gets the `B` in `A.start_with?(B)`.
*/
abstract DataFlow::Node getSubstring();
/**
* Gets the polarity of the check.
*
* If the polarity is `false` the check returns `true` if the string does not start
* with the given substring.
*/
boolean getPolarity() { result = true }
}
/**
* An expression of form `A.start_with?(B)`.
*/
private class StartsWith_Native extends Range, DataFlow::CallNode {
StartsWith_Native() { this.getMethodName() = "start_with?" }
override DataFlow::Node getBaseString() { result = this.getReceiver() }
override DataFlow::Node getSubstring() { result = this.getArgument(_) }
}
/**
* An expression of form `A.index(B) == 0` or `A.index(B) != 0`.
*/
private class StartsWith_IndexOfEquals extends Range {
private DataFlow::CallNode indexOf;
private boolean polarity;
StartsWith_IndexOfEquals() {
exists(ExprNodes::ComparisonOperationCfgNode comparison |
this.asExpr() = comparison and
indexOf.getMethodName() = "index" and
strictcount(indexOf.getArgument(_)) = 1 and
indexOf.flowsTo(any(DataFlow::Node n | n.asExpr() = comparison.getAnOperand())) and
comparison.getAnOperand().getConstantValue().getInt() = 0
|
polarity = true and comparison.getExpr() instanceof EqExpr
or
polarity = true and comparison.getExpr() instanceof CaseEqExpr
or
polarity = false and comparison.getExpr() instanceof NEExpr
)
}
override DataFlow::Node getBaseString() { result = indexOf.getReceiver() }
override DataFlow::Node getSubstring() { result = indexOf.getArgument(0) }
override boolean getPolarity() { result = polarity }
}
}
/**
* An expression that is equivalent to `A.include?(B)` or `!A.include?(B)`.
* Note that this class is equivalent to `InclusionTest`, which also matches
* inclusion tests on array objects.
*/
class Includes extends InclusionTest {
/** Gets the `A` in `A.include?(B)`. */
final DataFlow::Node getBaseString() { result = super.getContainerNode() }
/** Gets the `B` in `A.include?(B)`. */
final DataFlow::Node getSubstring() { result = super.getContainedNode() }
}
/**
* An expression that is equivalent to `A.end_with?(B)` or `!A.end_with?(B)`.
*/
class EndsWith extends DataFlow::Node instanceof EndsWith::Range {
/**
* Gets the `A` in `A.start_with?(B)`.
*/
final DataFlow::Node getBaseString() { result = super.getBaseString() }
/**
* Gets the `B` in `A.start_with?(B)`.
*/
final DataFlow::Node getSubstring() { result = super.getSubstring() }
/**
* Gets the polarity if the check.
*
* If the polarity is `false` the check returns `true` if the string does not end
* with the given substring.
*/
final boolean getPolarity() { result = super.getPolarity() }
}
/**
* Provides classes implementing suffix test expressions.
*/
module EndsWith {
/**
* An expression that is equivalent to `A.end_with?(B)` or `!A.end_with?(B)`.
*/
abstract class Range extends DataFlow::Node {
/**
* Gets the `A` in `A.end_with?(B)`.
*/
abstract DataFlow::Node getBaseString();
/**
* Gets the `B` in `A.end_with?(B)`.
*/
abstract DataFlow::Node getSubstring();
/**
* Gets the polarity if the check.
*
* If the polarity is `false` the check returns `true` if the string does not end
* with the given substring.
*/
boolean getPolarity() { result = true }
}
/**
* An expression of form `A.end_with?(B)`.
*/
private class EndsWith_Native extends Range, DataFlow::CallNode {
EndsWith_Native() { this.getMethodName() = "end_with?" }
override DataFlow::Node getBaseString() { result = this.getReceiver() }
override DataFlow::Node getSubstring() { result = this.getArgument(_) }
}
}
}

View File

@@ -8,28 +8,37 @@ private import internal.TreeSitter
/** A constant value. */
class ConstantValue extends TConstantValue {
/** Gets a textual representation of this constant value. */
final string toString() {
result = this.getInt().toString()
final string toString() { this.hasValueWithType(result, _) }
/** Gets a string describing the type of this constant value. */
string getValueType() { this.hasValueWithType(_, result) }
private predicate hasValueWithType(string value, string type) {
value = this.getInt().toString() and type = "int"
or
result = this.getFloat().toString()
value = this.getFloat().toString() and type = "float"
or
exists(int numerator, int denominator |
this.isRational(numerator, denominator) and
result = numerator + "/" + denominator
value = numerator + "/" + denominator and
type = "rational"
)
or
exists(float real, float imaginary |
this.isComplex(real, imaginary) and
result = real + "+" + imaginary + "i"
value = real + "+" + imaginary + "i" and
type = "complex"
)
or
result = this.getString()
value = this.getString() and type = "string"
or
result = ":" + this.getSymbol()
value = ":" + this.getSymbol() and type = "symbol"
or
result = this.getBoolean().toString()
value = this.getRegExp() and type = "regexp"
or
this.isNil() and result = "nil"
value = this.getBoolean().toString() and type = "boolean"
or
this.isNil() and value = "nil" and type = "nil"
}
/** Gets the integer value, if this is an integer. */
@@ -62,11 +71,31 @@ class ConstantValue extends TConstantValue {
/** Holds if this is the symbol value `:s`. */
predicate isSymbol(string s) { s = this.getSymbol() }
/** Gets the string or symbol value, if any. */
string getStringOrSymbol() { result = [this.getString(), this.getSymbol()] }
/** Gets the regexp value, if this is a regexp. */
string getRegExp() { this.isRegExpWithFlags(result, _) }
/** Holds if this is the string value `s` or the symbol value `:s`. */
predicate isStringOrSymbol(string s) { s = this.getStringOrSymbol() }
/** Holds if this is the regexp value `/s/`, ignoring any flags. */
predicate isRegExp(string s) { this.isRegExpWithFlags(s, _) }
/** Holds if this is the regexp value `/s/flags` . */
predicate isRegExpWithFlags(string s, string flags) { this = TRegExp(s, flags) }
/** DEPRECATED: Use `getStringlikeValue` instead. */
deprecated string getStringOrSymbol() { result = this.getStringlikeValue() }
/** DEPRECATED: Use `isStringlikeValue` instead. */
deprecated predicate isStringOrSymbol(string s) { s = this.getStringlikeValue() }
/** Gets the string/symbol/regexp value, if any. */
string getStringlikeValue() { result = [this.getString(), this.getSymbol(), this.getRegExp()] }
/**
* Holds if this is:
* - the string value `s`,
* - the symbol value `:s`, or
* - the regexp value `/s/`.
*/
predicate isStringlikeValue(string s) { s = this.getStringlikeValue() }
/** Gets the Boolean value, if this is a Boolean. */
boolean getBoolean() { this = TBoolean(result) }
@@ -92,11 +121,17 @@ module ConstantValue {
/** A constant complex value. */
class ConstantComplexValue extends ConstantValue, TComplex { }
/** A constant string-like value. */
class ConstantStringlikeValue extends ConstantValue, TStringlike { }
/** A constant string value. */
class ConstantStringValue extends ConstantValue, TString { }
class ConstantStringValue extends ConstantStringlikeValue, TString { }
/** A constant symbol value. */
class ConstantSymbolValue extends ConstantValue, TSymbol { }
class ConstantSymbolValue extends ConstantStringlikeValue, TSymbol { }
/** A constant regexp value. */
class ConstantRegExpValue extends ConstantStringlikeValue, TRegExp { }
/** A constant Boolean value. */
class ConstantBooleanValue extends ConstantValue, TBoolean { }
@@ -224,23 +259,7 @@ class ConstantReadAccess extends ConstantAccess {
*
* the value being read at `M::CONST` is `"const"`.
*/
Expr getValue() {
not exists(this.getScopeExpr()) and
result = lookupConst(this.getEnclosingModule+().getModule(), this.getName()) and
// For now, we restrict the scope of top-level declarations to their file.
// This may remove some plausible targets, but also removes a lot of
// implausible targets
if result.getEnclosingModule() instanceof Toplevel
then result.getFile() = this.getFile()
else any()
or
this.hasGlobalScope() and
result = lookupConst(TResolved("Object"), this.getName())
or
result = lookupConst(resolveConstantReadAccess(this.getScopeExpr()), this.getName())
}
final override ConstantValue getConstantValue() { result = this.getValue().getConstantValue() }
Expr getValue() { result = getConstantReadAccessValue(this) }
final override string getAPrimaryQlClass() { result = "ConstantReadAccess" }
}

View File

@@ -2,6 +2,7 @@ private import codeql.ruby.AST
private import codeql.ruby.CFG
private import codeql.ruby.ast.Constant
private import internal.AST
private import internal.Constant
private import internal.Expr
private import internal.TreeSitter
@@ -19,9 +20,7 @@ class Expr extends Stmt, TExpr {
deprecated string getValueText() { result = this.getConstantValue().toString() }
/** Gets the constant value of this expression, if any. */
ConstantValue getConstantValue() {
forex(CfgNodes::ExprCfgNode n | n = this.getAControlFlowNode() | result = n.getConstantValue())
}
ConstantValue getConstantValue() { result = getConstantValueExpr(this) }
}
/** DEPRECATED: Use `SelfVariableAccess` instead. */
@@ -455,11 +454,11 @@ class StringConcatenation extends Expr, TStringConcatenation {
*/
final string getConcatenatedValueText() {
forall(StringLiteral c | c = this.getString(_) |
exists(c.getConstantValue().getStringOrSymbol())
exists(c.getConstantValue().getStringlikeValue())
) and
result =
concat(string valueText, int i |
valueText = this.getString(i).getConstantValue().getStringOrSymbol()
valueText = this.getString(i).getConstantValue().getStringlikeValue()
|
valueText order by i
)

View File

@@ -1,5 +1,5 @@
private import codeql.ruby.AST
private import codeql.ruby.security.performance.RegExpTreeView as RETV
private import codeql.ruby.Regexp as RE
private import internal.AST
private import internal.Constant
private import internal.Literal
@@ -39,10 +39,10 @@ class NumericLiteral extends Literal, TNumericLiteral { }
*/
class IntegerLiteral extends NumericLiteral instanceof IntegerLiteralImpl {
/** Gets the numerical value of this integer literal. */
final int getValue() { result = super.getValueImpl() }
final int getValue() { result = super.getValue() }
final override ConstantValue::ConstantIntegerValue getConstantValue() {
result.isInt(this.getValue())
result = NumericLiteral.super.getConstantValue()
}
final override string getAPrimaryQlClass() { result = "IntegerLiteral" }
@@ -56,17 +56,11 @@ class IntegerLiteral extends NumericLiteral instanceof IntegerLiteralImpl {
* 2.7e+5
* ```
*/
class FloatLiteral extends NumericLiteral, TFloatLiteral {
private Ruby::Float g;
FloatLiteral() { this = TFloatLiteral(g) }
class FloatLiteral extends NumericLiteral instanceof FloatLiteralImpl {
final override ConstantValue::ConstantFloatValue getConstantValue() {
result.isFloat(parseFloat(g))
result = NumericLiteral.super.getConstantValue()
}
final override string toString() { result = g.getValue() }
final override string getAPrimaryQlClass() { result = "FloatLiteral" }
}
@@ -77,20 +71,11 @@ class FloatLiteral extends NumericLiteral, TFloatLiteral {
* 123r
* ```
*/
class RationalLiteral extends NumericLiteral, TRationalLiteral {
private Ruby::Rational g;
RationalLiteral() { this = TRationalLiteral(g) }
class RationalLiteral extends NumericLiteral instanceof RationalLiteralImpl {
final override ConstantValue::ConstantRationalValue getConstantValue() {
exists(int numerator, int denominator |
isRationalValue(g, numerator, denominator) and
result.isRational(numerator, denominator)
)
result = NumericLiteral.super.getConstantValue()
}
final override string toString() { result = g.getChild().(Ruby::Token).getValue() + "r" }
final override string getAPrimaryQlClass() { result = "RationalLiteral" }
}
@@ -101,29 +86,17 @@ class RationalLiteral extends NumericLiteral, TRationalLiteral {
* 1i
* ```
*/
class ComplexLiteral extends NumericLiteral, TComplexLiteral {
private Ruby::Complex g;
ComplexLiteral() { this = TComplexLiteral(g) }
class ComplexLiteral extends NumericLiteral instanceof ComplexLiteralImpl {
final override ConstantValue::ConstantComplexValue getConstantValue() {
result.isComplex(0, getComplexValue(g))
result = NumericLiteral.super.getConstantValue()
}
final override string toString() { result = g.getValue() }
final override string getAPrimaryQlClass() { result = "ComplexLiteral" }
}
/** A `nil` literal. */
class NilLiteral extends Literal, TNilLiteral {
private Ruby::Nil g;
NilLiteral() { this = TNilLiteral(g) }
final override ConstantValue::ConstantNilValue getConstantValue() { any() }
final override string toString() { result = g.getValue() }
class NilLiteral extends Literal instanceof NilLiteralImpl {
final override ConstantValue::ConstantNilValue getConstantValue() { result = TNil() }
final override string getAPrimaryQlClass() { result = "NilLiteral" }
}
@@ -137,62 +110,53 @@ class NilLiteral extends Literal, TNilLiteral {
* FALSE
* ```
*/
class BooleanLiteral extends Literal, TBooleanLiteral {
class BooleanLiteral extends Literal instanceof BooleanLiteralImpl {
final override string getAPrimaryQlClass() { result = "BooleanLiteral" }
/** Holds if the Boolean literal is `true` or `TRUE`. */
predicate isTrue() { none() }
final predicate isTrue() { this.getValue() = true }
/** Holds if the Boolean literal is `false` or `FALSE`. */
predicate isFalse() { none() }
final predicate isFalse() { this.getValue() = false }
/** Gets the value of this Boolean literal. */
boolean getValue() {
this.isTrue() and result = true
or
this.isFalse() and result = false
}
boolean getValue() { result = super.getValue() }
final override ConstantValue::ConstantBooleanValue getConstantValue() {
result.isBoolean(this.getValue())
result = Literal.super.getConstantValue()
}
}
/**
* An `__ENCODING__` literal.
*/
class EncodingLiteral extends Literal, TEncoding {
class EncodingLiteral extends Literal instanceof EncodingLiteralImpl {
final override string getAPrimaryQlClass() { result = "EncodingLiteral" }
final override string toString() { result = "__ENCODING__" }
// TODO: return the encoding defined by a magic encoding: comment, if any.
override ConstantValue::ConstantStringValue getConstantValue() { result.isString("UTF-8") }
final override ConstantValue::ConstantStringValue getConstantValue() {
result = Literal.super.getConstantValue()
}
}
/**
* A `__LINE__` literal.
*/
class LineLiteral extends Literal, TLine {
class LineLiteral extends Literal instanceof LineLiteralImpl {
final override string getAPrimaryQlClass() { result = "LineLiteral" }
final override string toString() { result = "__LINE__" }
final override ConstantValue::ConstantIntegerValue getConstantValue() {
result.isInt(this.getLocation().getStartLine())
result = Literal.super.getConstantValue()
}
}
/**
* A `__FILE__` literal.
*/
class FileLiteral extends Literal, TFile {
class FileLiteral extends Literal instanceof FileLiteralImpl {
final override string getAPrimaryQlClass() { result = "FileLiteral" }
final override string toString() { result = "__FILE__" }
final override ConstantValue::ConstantStringValue getConstantValue() {
result.isString(this.getLocation().getFile().getAbsolutePath())
result = Literal.super.getConstantValue()
}
}
@@ -200,7 +164,7 @@ class FileLiteral extends Literal, TFile {
* The base class for a component of a string: `StringTextComponent`,
* `StringEscapeSequenceComponent`, or `StringInterpolationComponent`.
*/
class StringComponent extends AstNode, TStringComponent {
class StringComponent extends AstNode instanceof StringComponentImpl {
/**
* DEPRECATED: Use `getConstantValue` instead.
*
@@ -210,7 +174,7 @@ class StringComponent extends AstNode, TStringComponent {
deprecated string getValueText() { result = this.getConstantValue().toString() }
/** Gets the constant value of this string component, if any. */
ConstantValue::ConstantStringValue getConstantValue() { none() }
ConstantValue::ConstantStringValue getConstantValue() { result = TString(super.getValue()) }
}
/**
@@ -225,53 +189,38 @@ class StringComponent extends AstNode, TStringComponent {
* "foo#{ bar() } baz"
* ```
*/
class StringTextComponent extends StringComponent, TStringTextComponentNonRegexp {
private Ruby::Token g;
StringTextComponent() { this = TStringTextComponentNonRegexp(g) }
final override string toString() { result = g.getValue() }
final override ConstantValue::ConstantStringValue getConstantValue() {
result.isString(g.getValue())
}
class StringTextComponent extends StringComponent instanceof StringTextComponentImpl {
final override string getAPrimaryQlClass() { result = "StringTextComponent" }
/** Gets the text of this component as it appears in the source code. */
final string getRawText() { result = super.getRawTextImpl() }
}
/**
* An escape sequence component of a string or string-like literal.
*/
class StringEscapeSequenceComponent extends StringComponent, TStringEscapeSequenceComponentNonRegexp {
private Ruby::EscapeSequence g;
StringEscapeSequenceComponent() { this = TStringEscapeSequenceComponentNonRegexp(g) }
final override string toString() { result = g.getValue() }
final override ConstantValue::ConstantStringValue getConstantValue() {
result.isString(g.getValue())
}
class StringEscapeSequenceComponent extends StringComponent instanceof StringEscapeSequenceComponentImpl {
final override string getAPrimaryQlClass() { result = "StringEscapeSequenceComponent" }
/** Gets the text of this component as it appears in the source code. */
final string getRawText() { result = super.getRawTextImpl() }
}
/**
* An interpolation expression component of a string or string-like literal.
*/
class StringInterpolationComponent extends StringComponent, StmtSequence,
TStringInterpolationComponentNonRegexp {
class StringInterpolationComponent extends StringComponent, StmtSequence instanceof StringInterpolationComponentImpl {
private Ruby::Interpolation g;
StringInterpolationComponent() { this = TStringInterpolationComponentNonRegexp(g) }
final override string toString() { result = "#{...}" }
final override Stmt getStmt(int n) { toGenerated(result) = g.getChild(n) }
deprecated final override string getValueText() { none() }
final override ConstantValue::ConstantStringValue getConstantValue() { none() }
final override ConstantValue::ConstantStringValue getConstantValue() {
result = StmtSequence.super.getConstantValue()
}
final override string getAPrimaryQlClass() { result = "StringInterpolationComponent" }
}
@@ -279,17 +228,7 @@ class StringInterpolationComponent extends StringComponent, StmtSequence,
/**
* The base class for a component of a regular expression literal.
*/
class RegExpComponent extends AstNode, TRegExpComponent {
/**
* DEPRECATED: Use `getConstantValue` instead.
*
* Gets the source text for this regex component, if any.
*/
deprecated string getValueText() { result = this.getConstantValue().toString() }
/** Gets the constant value of this regex component, if any. */
ConstantValue::ConstantStringValue getConstantValue() { none() }
}
class RegExpComponent extends StringComponent instanceof RegExpComponentImpl { }
/**
* A component of a regex literal that is simply text.
@@ -303,53 +242,32 @@ class RegExpComponent extends AstNode, TRegExpComponent {
* "foo#{ bar() } baz"
* ```
*/
class RegExpTextComponent extends RegExpComponent, TStringTextComponentRegexp {
private Ruby::Token g;
RegExpTextComponent() { this = TStringTextComponentRegexp(g) }
final override string toString() { result = g.getValue() }
final override ConstantValue::ConstantStringValue getConstantValue() {
result.isString(getRegExpTextComponentValue(this))
}
class RegExpTextComponent extends RegExpComponent instanceof RegExpTextComponentImpl {
final override string getAPrimaryQlClass() { result = "RegExpTextComponent" }
}
/**
* An escape sequence component of a regex literal.
*/
class RegExpEscapeSequenceComponent extends RegExpComponent, TStringEscapeSequenceComponentRegexp {
private Ruby::EscapeSequence g;
RegExpEscapeSequenceComponent() { this = TStringEscapeSequenceComponentRegexp(g) }
final override string toString() { result = g.getValue() }
final override ConstantValue::ConstantStringValue getConstantValue() {
result.isString(getRegExpEscapeSequenceComponentValue(this))
}
class RegExpEscapeSequenceComponent extends RegExpComponent instanceof RegExpEscapeSequenceComponentImpl {
final override string getAPrimaryQlClass() { result = "RegExpEscapeSequenceComponent" }
}
/**
* An interpolation expression component of a regex literal.
*/
class RegExpInterpolationComponent extends RegExpComponent, StmtSequence,
TStringInterpolationComponentRegexp {
class RegExpInterpolationComponent extends RegExpComponent, StmtSequence instanceof RegExpComponentImpl {
private Ruby::Interpolation g;
RegExpInterpolationComponent() { this = TStringInterpolationComponentRegexp(g) }
final override string toString() { result = "#{...}" }
final override Stmt getStmt(int n) { toGenerated(result) = g.getChild(n) }
deprecated final override string getValueText() { none() }
final override ConstantValue::ConstantStringValue getConstantValue() { none() }
final override ConstantValue::ConstantStringValue getConstantValue() {
result = StmtSequence.super.getConstantValue()
}
final override string getAPrimaryQlClass() { result = "RegExpInterpolationComponent" }
}
@@ -357,7 +275,7 @@ class RegExpInterpolationComponent extends RegExpComponent, StmtSequence,
/**
* A string, symbol, regexp, or subshell literal.
*/
class StringlikeLiteral extends Literal, TStringlikeLiteral {
class StringlikeLiteral extends Literal instanceof StringlikeLiteralImpl {
/**
* Gets the `n`th component of this string or string-like literal. The result
* will be one of `StringTextComponent`, `StringInterpolationComponent`, and
@@ -371,7 +289,7 @@ class StringlikeLiteral extends Literal, TStringlikeLiteral {
* "foo_#{ Time.now }"
* ```
*/
StringComponent getComponent(int n) { none() }
final StringComponent getComponent(int n) { result = super.getComponentImpl(n) }
/**
* Gets the number of components in this string or string-like literal.
@@ -392,78 +310,8 @@ class StringlikeLiteral extends Literal, TStringlikeLiteral {
*/
final int getNumberOfComponents() { result = count(this.getComponent(_)) }
private string getStartDelimiter() {
this instanceof TStringLiteral and
result = "\""
or
this instanceof TRegExpLiteral and
result = "/"
or
this instanceof TSimpleSymbolLiteral and
result = ":"
or
this instanceof TComplexSymbolLiteral and
result = ":\""
or
this instanceof THashKeySymbolLiteral and
result = ""
or
this instanceof TSubshellLiteral and
result = "`"
or
this instanceof THereDoc and
result = ""
}
private string getEndDelimiter() {
this instanceof TStringLiteral and
result = "\""
or
this instanceof TRegExpLiteral and
result = "/"
or
this instanceof TSimpleSymbolLiteral and
result = ""
or
this instanceof TComplexSymbolLiteral and
result = "\""
or
this instanceof THashKeySymbolLiteral and
result = ""
or
this instanceof TSubshellLiteral and
result = "`"
or
this instanceof THereDoc and
result = ""
}
override string toString() {
exists(string full, string summary |
full =
concat(StringComponent c, int i, string s |
c = this.getComponent(i) and
(
s = toGenerated(c).(Ruby::Token).getValue()
or
not toGenerated(c) instanceof Ruby::Token and
s = "#{...}"
)
|
s order by i
) and
(
// summary should be 32 chars max (incl. ellipsis)
full.length() > 32 and summary = full.substring(0, 29) + "..."
or
full.length() <= 32 and summary = full
) and
result = this.getStartDelimiter() + summary + this.getEndDelimiter()
)
}
final override AstNode getAChild(string pred) {
result = super.getAChild(pred)
result = Literal.super.getAChild(pred)
or
pred = "getComponent" and result = this.getComponent(_)
}
@@ -477,7 +325,7 @@ class StringlikeLiteral extends Literal, TStringlikeLiteral {
* "hello, #{name}"
* ```
*/
class StringLiteral extends StringlikeLiteral, TStringLiteral {
class StringLiteral extends StringlikeLiteral instanceof StringLiteralImpl {
final override string getAPrimaryQlClass() { result = "StringLiteral" }
}
@@ -488,15 +336,13 @@ class StringLiteral extends StringlikeLiteral, TStringLiteral {
* /[a-z]+/
* ```
*/
class RegExpLiteral extends StringlikeLiteral, TRegExpLiteral {
class RegExpLiteral extends StringlikeLiteral instanceof RegExpLiteralImpl {
private Ruby::Regex g;
RegExpLiteral() { this = TRegExpLiteral(g) }
final override string getAPrimaryQlClass() { result = "RegExpLiteral" }
final override StringComponent getComponent(int i) { toGenerated(result) = g.getChild(i) }
/**
* Gets the regexp flags as a string.
*
@@ -547,7 +393,7 @@ class RegExpLiteral extends StringlikeLiteral, TRegExpLiteral {
final predicate hasFreeSpacingFlag() { this.getFlagString().charAt(_) = "x" }
/** Returns the root node of the parse tree of this regular expression. */
final RETV::RegExpTerm getParsed() { result = RETV::getParsedRegExp(this) }
final RE::RegExpTerm getParsed() { result = RE::getParsedRegExp(this) }
}
/**
@@ -559,22 +405,14 @@ class RegExpLiteral extends StringlikeLiteral, TRegExpLiteral {
* :"foo bar #{baz}"
* ```
*/
class SymbolLiteral extends StringlikeLiteral, TSymbolLiteral {
class SymbolLiteral extends StringlikeLiteral instanceof SymbolLiteralImpl {
final override string getAPrimaryQlClass() {
not this instanceof MethodName and result = "SymbolLiteral"
}
}
private class SimpleSymbolLiteral extends SymbolLiteral, TSimpleSymbolLiteral {
private Ruby::SimpleSymbol g;
SimpleSymbolLiteral() { this = TSimpleSymbolLiteral(g) }
final override ConstantValue::ConstantSymbolValue getConstantValue() {
result.isSymbol(getSimpleSymbolValue(g))
result = StringlikeLiteral.super.getConstantValue()
}
final override string toString() { result = g.getValue() }
}
/**
@@ -585,18 +423,12 @@ private class SimpleSymbolLiteral extends SymbolLiteral, TSimpleSymbolLiteral {
* %x(/bin/sh foo.sh)
* ```
*/
class SubshellLiteral extends StringlikeLiteral, TSubshellLiteral {
class SubshellLiteral extends StringlikeLiteral instanceof SubshellLiteralImpl {
private Ruby::Subshell g;
SubshellLiteral() { this = TSubshellLiteral(g) }
final override string getAPrimaryQlClass() { result = "SubshellLiteral" }
final override StringComponent getComponent(int i) { toGenerated(result) = g.getChild(i) }
}
private class RequiredCharacterConstantValue extends RequiredConstantValue {
override predicate requiredString(string s) { s = any(Ruby::Character c).getValue() }
}
/**
@@ -607,18 +439,12 @@ private class RequiredCharacterConstantValue extends RequiredConstantValue {
* ?\u{61}
* ```
*/
class CharacterLiteral extends Literal, TCharacterLiteral {
private Ruby::Character g;
CharacterLiteral() { this = TCharacterLiteral(g) }
class CharacterLiteral extends Literal instanceof CharacterLiteralImpl {
final override string getAPrimaryQlClass() { result = "CharacterLiteral" }
final override ConstantValue::ConstantStringValue getConstantValue() {
result.isString(g.getValue())
result = Literal.super.getConstantValue()
}
final override string toString() { result = g.getValue() }
final override string getAPrimaryQlClass() { result = "CharacterLiteral" }
}
/**
@@ -630,7 +456,7 @@ class CharacterLiteral extends Literal, TCharacterLiteral {
* SQL
* ```
*/
class HereDoc extends StringlikeLiteral, THereDoc {
class HereDoc extends StringlikeLiteral instanceof HereDocImpl {
private Ruby::HeredocBeginning g;
HereDoc() { this = THereDoc(g) }
@@ -678,12 +504,6 @@ class HereDoc extends StringlikeLiteral, THereDoc {
result = ["-", "~"]
)
}
final override StringComponent getComponent(int n) {
toGenerated(result) = getHereDocBody(g).getChild(n)
}
final override string toString() { result = g.getValue() }
}
/**
@@ -749,8 +569,6 @@ class HashLiteral extends Literal instanceof HashLiteralImpl {
/** Gets the number of elements in this hash literal. */
final int getNumberOfElements() { result = super.getNumberOfElementsImpl() }
final override string toString() { result = "{...}" }
final override AstNode getAChild(string pred) {
result = Literal.super.getAChild(pred)
or
@@ -766,44 +584,34 @@ class HashLiteral extends Literal instanceof HashLiteralImpl {
* (1024...2048)
* ```
*/
class RangeLiteral extends Literal, TRangeLiteral {
class RangeLiteral extends Literal instanceof RangeLiteralImpl {
final override string getAPrimaryQlClass() { result = "RangeLiteral" }
/** Gets the begin expression of this range, if any. */
Expr getBegin() { none() }
final Expr getBegin() { result = super.getBeginImpl() }
/** Gets the end expression of this range, if any. */
Expr getEnd() { none() }
final Expr getEnd() { result = super.getEndImpl() }
/**
* Holds if the range is inclusive of the end value, i.e. uses the `..`
* operator.
*/
predicate isInclusive() { none() }
final predicate isInclusive() { super.isInclusiveImpl() }
/**
* Holds if the range is exclusive of the end value, i.e. uses the `...`
* operator.
*/
predicate isExclusive() { none() }
final predicate isExclusive() { super.isExclusiveImpl() }
final override AstNode getAChild(string pred) {
result = super.getAChild(pred)
result = Literal.super.getAChild(pred)
or
pred = "getBegin" and result = this.getBegin()
or
pred = "getEnd" and result = this.getEnd()
}
final override string toString() {
exists(string op |
this.isInclusive() and op = ".."
or
this.isExclusive() and op = "..."
|
result = "_ " + op + " _"
)
}
}
/**

View File

@@ -53,7 +53,7 @@ private class MethodModifier extends MethodCall {
predicate modifiesMethod(Namespace n, string name) {
this = n.getAStmt() and
[
this.getMethodArgument().getConstantValue().getStringOrSymbol(),
this.getMethodArgument().getConstantValue().getStringlikeValue(),
this.getMethodArgument().(MethodBase).getName()
] = name
}

View File

@@ -341,6 +341,11 @@ class RelationalOperation extends ComparisonOperation, TRelationalOperation {
/** Gets the lesser operand. */
Expr getLesserOperand() { none() }
/**
* Holds if this is a comparison with `<=` or `>=`.
*/
predicate isInclusive() { this instanceof LEExpr or this instanceof GEExpr }
final override AstNode getAChild(string pred) {
result = super.getAChild(pred)
or

View File

@@ -278,7 +278,7 @@ class HashPattern extends CasePattern, THashPattern {
/** Gets the value for a given key name. */
CasePattern getValueByKey(string key) {
exists(int i |
this.getKey(i).getConstantValue().isStringOrSymbol(key) and result = this.getValue(i)
this.getKey(i).getConstantValue().isStringlikeValue(key) and result = this.getValue(i)
)
}

View File

@@ -284,10 +284,12 @@ private module Cached {
TStringInterpolationComponentRegexp(Ruby::Interpolation g) {
g.getParent() instanceof Ruby::Regex
} or
TStringTextComponentNonRegexp(Ruby::Token g) {
TStringTextComponentNonRegexpStringOrHeredocContent(Ruby::Token g) {
(g instanceof Ruby::StringContent or g instanceof Ruby::HeredocContent) and
not g.getParent() instanceof Ruby::Regex
} or
TStringTextComponentNonRegexpSimpleSymbol(Ruby::SimpleSymbol g) or
TStringTextComponentNonRegexpHashKeySymbol(Ruby::HashKeySymbol g) or
TStringTextComponentRegexp(Ruby::Token g) {
(g instanceof Ruby::StringContent or g instanceof Ruby::HeredocContent) and
g.getParent() instanceof Ruby::Regex
@@ -511,7 +513,9 @@ private module Cached {
n = TStringEscapeSequenceComponentRegexp(result) or
n = TStringInterpolationComponentNonRegexp(result) or
n = TStringInterpolationComponentRegexp(result) or
n = TStringTextComponentNonRegexp(result) or
n = TStringTextComponentNonRegexpStringOrHeredocContent(result) or
n = TStringTextComponentNonRegexpSimpleSymbol(result) or
n = TStringTextComponentNonRegexpHashKeySymbol(result) or
n = TStringTextComponentRegexp(result) or
n = TSubExprReal(result) or
n = TSubshellLiteral(result) or
@@ -702,6 +706,10 @@ class TIntegerLiteral = TIntegerLiteralReal or TIntegerLiteralSynth;
class TBooleanLiteral = TTrueLiteral or TFalseLiteral;
class TStringTextComponentNonRegexp =
TStringTextComponentNonRegexpStringOrHeredocContent or
TStringTextComponentNonRegexpSimpleSymbol or TStringTextComponentNonRegexpHashKeySymbol;
class TStringTextComponent = TStringTextComponentNonRegexp or TStringTextComponentRegexp;
class TStringEscapeSequenceComponent =

View File

@@ -1,36 +1,511 @@
cached
newtype TConstantValue =
TInt(int i) { any(RequiredConstantValue x).requiredInt(i) } or
TFloat(float f) { any(RequiredConstantValue x).requiredFloat(f) } or
TRational(int numerator, int denominator) {
any(RequiredConstantValue x).requiredRational(numerator, denominator)
} or
TComplex(float real, float imaginary) {
any(RequiredConstantValue x).requiredComplex(real, imaginary)
} or
TString(string s) { any(RequiredConstantValue x).requiredString(s) } or
TSymbol(string s) { any(RequiredConstantValue x).requiredSymbol(s) } or
TBoolean(boolean b) { b in [false, true] } or
TNil()
private newtype TRequiredConstantValue = MkRequiredConstantValue()
private import codeql.ruby.AST
private import codeql.ruby.ast.internal.Literal
private import codeql.ruby.ast.internal.Module
private import codeql.ruby.controlflow.CfgNodes
private import codeql.ruby.dataflow.SSA
private import ExprNodes
/**
* A class that exists for QL technical reasons only (the IPA type used
* to represent constant values needs to be bounded).
* Provides an implementation of constant propagation for control-flow graph
* (CFG) nodes and expressions (AST nodes).
*
* The end result are two predicates
* ```ql
* ConstantValue getConstantValue(ExprCfgNode n);
* ConstantValue getConstantValueExpr(Expr e);
* ```
*
* It would be natural to define those predicates recursively. However, because
* of how `newtype`s work, this results in bad performance as a result of
* unnecessary recursion through the constructors of `TConstantValue`. Instead,
* we define a set of predicates for each possible `ConstantValue` type, and each
* set of predicates needs to replicate logic, e.g., how a constant may be propagated
* from an assignment to a variable read.
*
* For each `ConstantValue` type `T`, we define three predicates:
* ```ql
* predicate isT(ExprCfgNode n, T v);
* predicate isTExprNoCfg(Expr e, T v);
* predicate isTExpr(Expr e, T v);
* ```
*
* `isT` and `isTExpr` rely on the CFG to determine the constant value of a CFG
* node and expression, respectively, whereas `isTExprNoCfg` is able to determine
* the constant value of an expression without relying on the CFG. This means that
* even if the CFG is not available (dead code), we may still be able to infer a
* constant value in some cases.
*/
class RequiredConstantValue extends MkRequiredConstantValue {
string toString() { none() }
private module Propagation {
private ExprCfgNode getSource(VariableReadAccessCfgNode read) {
exists(Ssa::WriteDefinition def |
def.assigns(result) and
read = def.getARead()
)
}
predicate requiredInt(int i) { none() }
predicate isInt(ExprCfgNode e, int i) {
isIntExprNoCfg(e.getExpr(), i)
or
isIntExpr(e.getExpr().(ConstantReadAccess).getValue(), i)
or
isInt(getSource(e), i)
or
e =
any(UnaryOperationCfgNode unop |
unop.getExpr() instanceof UnaryMinusExpr and
isInt(unop.getOperand(), -i)
or
unop.getExpr() instanceof UnaryPlusExpr and
isInt(unop.getOperand(), i)
)
or
exists(BinaryOperationCfgNode binop, BinaryOperation b, int left, int right |
e = binop and
isInt(binop.getLeftOperand(), left) and
isInt(binop.getRightOperand(), right) and
b = binop.getExpr()
|
b instanceof AddExpr and
i = left + right
or
b instanceof SubExpr and
i = left - right
or
b instanceof MulExpr and
i = left * right
or
b instanceof DivExpr and
i = left / right
)
}
predicate requiredFloat(float f) { none() }
private predicate isIntExprNoCfg(Expr e, int i) {
i = e.(IntegerLiteralImpl).getValue()
or
i = e.(LineLiteralImpl).getValue()
or
isIntExprNoCfg(e.(ConstantReadAccess).getValue(), i)
}
predicate requiredRational(int numerator, int denominator) { none() }
predicate isIntExpr(Expr e, int i) {
isIntExprNoCfg(e, i)
or
isIntExpr(e.(ConstantReadAccess).getValue(), i)
or
forex(ExprCfgNode n | n = e.getAControlFlowNode() | isInt(n, i))
}
predicate requiredComplex(float real, float imaginary) { none() }
predicate isFloat(ExprCfgNode e, float f) {
isFloatExprNoCfg(e.getExpr(), f)
or
isFloatExpr(e.getExpr().(ConstantReadAccess).getValue(), f)
or
isFloat(getSource(e), f)
or
e =
any(UnaryOperationCfgNode unop |
unop.getExpr() instanceof UnaryMinusExpr and
isFloat(unop.getOperand(), -f)
or
unop.getExpr() instanceof UnaryPlusExpr and
isFloat(unop.getOperand(), f)
)
or
exists(BinaryOperationCfgNode binop, BinaryOperation b, float left, float right |
e = binop and
b = binop.getExpr() and
exists(ExprCfgNode l, ExprCfgNode r |
l = binop.getLeftOperand() and
r = binop.getRightOperand()
|
isFloat(l, left) and isFloat(r, right)
or
isInt(l, left) and isFloat(r, right)
or
isFloat(l, left) and isInt(r, right)
)
|
b instanceof AddExpr and
f = left + right
or
b instanceof SubExpr and
f = left - right
or
b instanceof MulExpr and
f = left * right
or
b instanceof DivExpr and
f = left / right
)
}
predicate requiredString(string s) { none() }
private predicate isFloatExprNoCfg(Expr e, float f) {
f = e.(FloatLiteralImpl).getValue()
or
isFloatExprNoCfg(e.(ConstantReadAccess).getValue(), f)
}
predicate requiredSymbol(string s) { none() }
predicate isFloatExpr(Expr e, float f) {
isFloatExprNoCfg(e, f)
or
isFloatExpr(e.(ConstantReadAccess).getValue(), f)
or
forex(ExprCfgNode n | n = e.getAControlFlowNode() | isFloat(n, f))
}
predicate isRational(ExprCfgNode e, int numerator, int denominator) {
isRationalExprNoCfg(e.getExpr(), numerator, denominator)
or
isRationalExpr(e.getExpr().(ConstantReadAccess).getValue(), numerator, denominator)
or
isRational(getSource(e), numerator, denominator)
}
private predicate isRationalExprNoCfg(Expr e, int numerator, int denominator) {
e.(RationalLiteralImpl).hasValue(numerator, denominator)
or
isRationalExprNoCfg(e.(ConstantReadAccess).getValue(), numerator, denominator)
}
predicate isRationalExpr(Expr e, int numerator, int denominator) {
isRationalExprNoCfg(e, numerator, denominator)
or
isRationalExpr(e.(ConstantReadAccess).getValue(), numerator, denominator)
or
forex(ExprCfgNode n | n = e.getAControlFlowNode() | isRational(n, numerator, denominator))
}
predicate isComplex(ExprCfgNode e, float real, float imaginary) {
isComplexExprNoCfg(e.getExpr(), real, imaginary)
or
isComplexExpr(e.getExpr().(ConstantReadAccess).getValue(), real, imaginary)
or
isComplex(getSource(e), real, imaginary)
}
private predicate isComplexExprNoCfg(Expr e, float real, float imaginary) {
e.(ComplexLiteralImpl).hasValue(real, imaginary)
or
isComplexExprNoCfg(e.(ConstantReadAccess).getValue(), real, imaginary)
}
predicate isComplexExpr(Expr e, float real, float imaginary) {
isComplexExprNoCfg(e, real, imaginary)
or
isComplexExpr(e.(ConstantReadAccess).getValue(), real, imaginary)
or
forex(ExprCfgNode n | n = e.getAControlFlowNode() | isComplex(n, real, imaginary))
}
private class StringlikeLiteralWithInterpolationCfgNode extends StringlikeLiteralCfgNode {
StringlikeLiteralWithInterpolationCfgNode() {
this.getAComponent() =
any(StringComponentCfgNode c |
c instanceof StringInterpolationComponentCfgNode or
c instanceof RegExpInterpolationComponentCfgNode
)
}
pragma[nomagic]
private string getComponentValue(int i) {
this.getComponent(i) =
any(StringComponentCfgNode c |
isString(c, result)
or
result = c.getNode().(StringComponentImpl).getValue()
)
}
language[monotonicAggregates]
private string getValue() {
result =
strictconcat(int i | exists(this.getComponent(i)) | this.getComponentValue(i) order by i)
}
pragma[nomagic]
string getSymbolValue() {
result = this.getValue() and
this.getExpr() instanceof SymbolLiteral
}
pragma[nomagic]
string getStringValue() {
result = this.getValue() and
not this.getExpr() instanceof SymbolLiteral and
not this.getExpr() instanceof RegExpLiteral
}
pragma[nomagic]
string getRegExpValue(string flags) {
result = this.getValue() and
flags = this.getExpr().(RegExpLiteral).getFlagString()
}
}
predicate isString(ExprCfgNode e, string s) {
isStringExprNoCfg(e.getExpr(), s)
or
isStringExpr(e.getExpr().(ConstantReadAccess).getValue(), s)
or
isString(getSource(e), s)
or
exists(BinaryOperationCfgNode binop, string left, string right |
e = binop and
isString(binop.getLeftOperand(), left) and
isString(binop.getRightOperand(), right) and
binop.getExpr() instanceof AddExpr and
left.length() + right.length() <= 1000 and
s = left + right
)
or
s = e.(StringlikeLiteralWithInterpolationCfgNode).getStringValue()
or
// If last statement in the interpolation is a constant or local variable read,
// we attempt to look up its string value.
// If there's a result, we return that as the string value of the interpolation.
exists(ExprCfgNode last | last = e.(StringInterpolationComponentCfgNode).getLastStmt() |
isInt(last, any(int i | s = i.toString())) or
isFloat(last, any(float f | s = f.toString())) or
isString(last, s)
)
or
// If last statement in the interpolation is a constant or local variable read,
// attempt to look up its definition and return the definition's `getConstantValue()`.
exists(ExprCfgNode last | last = e.(RegExpInterpolationComponentCfgNode).getLastStmt() |
isInt(last, any(int i | s = i.toString())) or
isFloat(last, any(float f | s = f.toString())) or
isString(last, s) or
isRegExp(last, s, _) // Note: we lose the flags for interpolated regexps here.
)
}
private predicate isStringExprNoCfg(Expr e, string s) {
s = e.(StringlikeLiteralImpl).getStringValue() and
not e instanceof SymbolLiteral and
not e instanceof RegExpLiteral
or
s = e.(EncodingLiteralImpl).getValue()
or
s = e.(FileLiteralImpl).getValue()
or
s = e.(TokenMethodName).getValue()
or
s = e.(CharacterLiteralImpl).getValue()
or
isStringExprNoCfg(e.(ConstantReadAccess).getValue(), s)
}
predicate isStringExpr(Expr e, string s) {
isStringExprNoCfg(e, s)
or
isStringExpr(e.(ConstantReadAccess).getValue(), s)
or
forex(ExprCfgNode n | n = e.getAControlFlowNode() | isString(n, s))
}
predicate isSymbol(ExprCfgNode e, string s) {
isSymbolExprNoCfg(e.getExpr(), s)
or
isSymbolExpr(e.getExpr().(ConstantReadAccess).getValue(), s)
or
isSymbol(getSource(e), s)
or
s = e.(StringlikeLiteralWithInterpolationCfgNode).getSymbolValue()
}
private predicate isSymbolExprNoCfg(Expr e, string s) {
s = e.(StringlikeLiteralImpl).getStringValue() and
e instanceof SymbolLiteral
or
isSymbolExprNoCfg(e.(ConstantReadAccess).getValue(), s)
}
predicate isSymbolExpr(Expr e, string s) {
isSymbolExprNoCfg(e, s)
or
isSymbolExpr(e.(ConstantReadAccess).getValue(), s)
or
forex(ExprCfgNode n | n = e.getAControlFlowNode() | isSymbol(n, s))
}
predicate isRegExp(ExprCfgNode e, string s, string flags) {
isRegExpExprNoCfg(e.getExpr(), s, flags)
or
isRegExpExpr(e.getExpr().(ConstantReadAccess).getValue(), s, flags)
or
isRegExp(getSource(e), s, flags)
or
s = e.(StringlikeLiteralWithInterpolationCfgNode).getRegExpValue(flags)
}
private predicate isRegExpExprNoCfg(Expr e, string s, string flags) {
s = e.(StringlikeLiteralImpl).getStringValue() and
e.(RegExpLiteral).getFlagString() = flags
or
isRegExpExprNoCfg(e.(ConstantReadAccess).getValue(), s, flags)
}
predicate isRegExpExpr(Expr e, string s, string flags) {
isRegExpExprNoCfg(e, s, flags)
or
isRegExpExpr(e.(ConstantReadAccess).getValue(), s, flags)
or
forex(ExprCfgNode n | n = e.getAControlFlowNode() | isRegExp(n, s, flags))
}
predicate isBoolean(ExprCfgNode e, boolean b) {
isBooleanExprNoCfg(e.getExpr(), b)
or
isBooleanExpr(e.getExpr().(ConstantReadAccess).getValue(), b)
or
isBoolean(getSource(e), b)
}
private predicate isBooleanExprNoCfg(Expr e, boolean b) {
b = e.(BooleanLiteralImpl).getValue()
or
isBooleanExprNoCfg(e.(ConstantReadAccess).getValue(), b)
}
predicate isBooleanExpr(Expr e, boolean b) {
isBooleanExprNoCfg(e, b)
or
isBooleanExpr(e.(ConstantReadAccess).getValue(), b)
or
forex(ExprCfgNode n | n = e.getAControlFlowNode() | isBoolean(n, b))
}
predicate isNil(ExprCfgNode e) {
isNilExprNoCfg(e.getExpr())
or
isNilExpr(e.getExpr().(ConstantReadAccess).getValue())
or
isNil(getSource(e))
}
private predicate isNilExprNoCfg(Expr e) {
e instanceof NilLiteralImpl
or
isNilExprNoCfg(e.(ConstantReadAccess).getValue())
}
predicate isNilExpr(Expr e) {
isNilExprNoCfg(e)
or
isNilExpr(e.(ConstantReadAccess).getValue())
or
forex(ExprCfgNode n | n = e.getAControlFlowNode() | isNil(n))
}
}
private import Propagation
cached
private module Cached {
cached
newtype TConstantValue =
TInt(int i) { isInt(_, i) or isIntExpr(_, i) } or
TFloat(float f) { isFloat(_, f) or isFloatExpr(_, f) } or
TRational(int numerator, int denominator) {
isRational(_, numerator, denominator) or
isRationalExpr(_, numerator, denominator)
} or
TComplex(float real, float imaginary) {
isComplex(_, real, imaginary) or
isComplexExpr(_, real, imaginary)
} or
TString(string s) {
isString(_, s)
or
isStringExpr(_, s)
or
s = any(StringComponentImpl c).getValue()
} or
TSymbol(string s) { isString(_, s) or isSymbolExpr(_, s) } or
TRegExp(string s, string flags) {
isRegExp(_, s, flags)
or
isRegExpExpr(_, s, flags)
or
s = any(StringComponentImpl c).getValue() and flags = ""
} or
TBoolean(boolean b) { b in [false, true] } or
TNil()
class TStringlike = TString or TSymbol or TRegExp;
cached
ConstantValue getConstantValue(ExprCfgNode n) {
result.isInt(any(int i | isInt(n, i)))
or
result.isFloat(any(float f | isFloat(n, f)))
or
exists(int numerator, int denominator |
isRational(n, numerator, denominator) and
result = TRational(numerator, denominator)
)
or
exists(float real, float imaginary |
isComplex(n, real, imaginary) and
result = TComplex(real, imaginary)
)
or
result.isString(any(string s | isString(n, s)))
or
result.isSymbol(any(string s | isSymbol(n, s)))
or
exists(string s, string flags | isRegExp(n, s, flags) and result = TRegExp(s, flags))
or
result.isBoolean(any(boolean b | isBoolean(n, b)))
or
result.isNil() and
isNil(n)
}
cached
ConstantValue getConstantValueExpr(Expr e) {
result.isInt(any(int i | isIntExpr(e, i)))
or
result.isFloat(any(float f | isFloatExpr(e, f)))
or
exists(int numerator, int denominator |
isRationalExpr(e, numerator, denominator) and
result = TRational(numerator, denominator)
)
or
exists(float real, float imaginary |
isComplexExpr(e, real, imaginary) and
result = TComplex(real, imaginary)
)
or
result.isString(any(string s | isStringExpr(e, s)))
or
result.isSymbol(any(string s | isSymbolExpr(e, s)))
or
exists(string s, string flags | isRegExpExpr(e, s, flags) and result = TRegExp(s, flags))
or
result.isBoolean(any(boolean b | isBooleanExpr(e, b)))
or
result.isNil() and
isNilExpr(e)
}
cached
Expr getConstantReadAccessValue(ConstantReadAccess read) {
not exists(read.getScopeExpr()) and
result = lookupConst(read.getEnclosingModule+().getModule(), read.getName()) and
// For now, we restrict the scope of top-level declarations to their file.
// This may remove some plausible targets, but also removes a lot of
// implausible targets
if result.getEnclosingModule() instanceof Toplevel
then result.getFile() = read.getFile()
else any()
or
read.hasGlobalScope() and
result = lookupConst(TResolved("Object"), read.getName())
or
result = lookupConst(resolveConstantReadAccess(read.getScopeExpr()), read.getName())
}
}
import Cached

View File

@@ -2,53 +2,30 @@ private import codeql.ruby.AST
private import AST
private import Constant
private import TreeSitter
private import codeql.ruby.ast.internal.Scope
private import codeql.ruby.controlflow.CfgNodes
private import codeql.NumberUtils
int parseInteger(Ruby::Integer i) {
exists(string s | s = i.getValue().toLowerCase().replaceAll("_", "") |
s.charAt(0) != "0" and
result = s.toInt()
or
exists(string str, string values, int shift |
s.matches("0b%") and
values = "01" and
str = s.suffix(2) and
shift = 1
or
s.matches("0x%") and
values = "0123456789abcdef" and
str = s.suffix(2) and
shift = 4
or
s.charAt(0) = "0" and
not s.charAt(1) = ["b", "x", "o"] and
values = "01234567" and
str = s.suffix(1) and
shift = 3
or
s.matches("0o%") and
values = "01234567" and
str = s.suffix(2) and
shift = 3
|
result =
sum(int index, string c, int v, int exp |
c = str.charAt(index) and
v = values.indexOf(c.toLowerCase()) and
exp = str.length() - index - 1
|
v.bitShiftLeft((str.length() - index - 1) * shift)
)
)
s.matches("0b%") and result = parseBinaryInt(s.suffix(2))
or
s.matches("0x%") and result = parseHexInt(s.suffix(2))
or
s.charAt(0) = "0" and
not s.charAt(1) = ["b", "x", "o"] and
result = parseOctalInt(s.suffix(1))
or
s.matches("0o%") and
result = parseOctalInt(s.suffix(2))
)
}
private class RequiredIntegerLiteralConstantValue extends RequiredConstantValue {
override predicate requiredInt(int i) { i = any(IntegerLiteral il).getValue() }
}
abstract class IntegerLiteralImpl extends Expr, TIntegerLiteral {
abstract int getValueImpl();
abstract int getValue();
}
class IntegerLiteralReal extends IntegerLiteralImpl, TIntegerLiteralReal {
@@ -56,7 +33,7 @@ class IntegerLiteralReal extends IntegerLiteralImpl, TIntegerLiteralReal {
IntegerLiteralReal() { this = TIntegerLiteralReal(g) }
final override int getValueImpl() { result = parseInteger(g) }
final override int getValue() { result = parseInteger(g) }
final override string toString() { result = g.getValue() }
}
@@ -66,7 +43,7 @@ class IntegerLiteralSynth extends IntegerLiteralImpl, TIntegerLiteralSynth {
IntegerLiteralSynth() { this = TIntegerLiteralSynth(_, _, value) }
final override int getValueImpl() { result = value }
final override int getValue() { result = value }
final override string toString() { result = value.toString() }
}
@@ -74,8 +51,14 @@ class IntegerLiteralSynth extends IntegerLiteralImpl, TIntegerLiteralSynth {
// TODO: implement properly
float parseFloat(Ruby::Float f) { result = f.getValue().toFloat() }
private class RequiredFloatConstantValue extends RequiredConstantValue {
override predicate requiredFloat(float f) { f = parseFloat(_) }
class FloatLiteralImpl extends Expr, TFloatLiteral {
private Ruby::Float g;
FloatLiteralImpl() { this = TFloatLiteral(g) }
final float getValue() { result = parseFloat(g) }
final override string toString() { result = g.getValue() }
}
predicate isRationalValue(Ruby::Rational r, int numerator, int denominator) {
@@ -92,10 +75,16 @@ predicate isRationalValue(Ruby::Rational r, int numerator, int denominator) {
)
}
private class RequiredRationalConstantValue extends RequiredConstantValue {
override predicate requiredRational(int numerator, int denominator) {
isRationalValue(_, numerator, denominator)
class RationalLiteralImpl extends Expr, TRationalLiteral {
private Ruby::Rational g;
RationalLiteralImpl() { this = TRationalLiteral(g) }
final predicate hasValue(int numerator, int denominator) {
isRationalValue(g, numerator, denominator)
}
final override string toString() { result = g.getChild().(Ruby::Token).getValue() + "r" }
}
float getComplexValue(Ruby::Complex c) {
@@ -105,162 +94,79 @@ float getComplexValue(Ruby::Complex c) {
)
}
private class RequiredComplexConstantValue extends RequiredConstantValue {
override predicate requiredComplex(float real, float imaginary) {
real = 0 and
imaginary = getComplexValue(_)
class ComplexLiteralImpl extends Expr, TComplexLiteral {
private Ruby::Complex g;
ComplexLiteralImpl() { this = TComplexLiteral(g) }
final predicate hasValue(float real, float imaginary) {
real = 0 and imaginary = getComplexValue(g)
}
final override string toString() { result = g.getValue() }
}
class TrueLiteral extends BooleanLiteral, TTrueLiteral {
class NilLiteralImpl extends Expr, TNilLiteral {
private Ruby::Nil g;
NilLiteralImpl() { this = TNilLiteral(g) }
final override string toString() { result = g.getValue() }
}
abstract class BooleanLiteralImpl extends Expr, TBooleanLiteral {
abstract boolean getValue();
}
class TrueLiteral extends BooleanLiteralImpl, TTrueLiteral {
private Ruby::True g;
TrueLiteral() { this = TTrueLiteral(g) }
final override string toString() { result = g.getValue() }
final override predicate isTrue() { any() }
final override boolean getValue() { result = true }
}
class FalseLiteral extends BooleanLiteral, TFalseLiteral {
class FalseLiteral extends BooleanLiteralImpl, TFalseLiteral {
private Ruby::False g;
FalseLiteral() { this = TFalseLiteral(g) }
final override string toString() { result = g.getValue() }
final override predicate isFalse() { any() }
final override boolean getValue() { result = false }
}
private class RequiredEncodingLiteralConstantValue extends RequiredConstantValue {
override predicate requiredString(string s) { s = "UTF-8" }
class EncodingLiteralImpl extends Expr, TEncoding {
private Ruby::Encoding g;
EncodingLiteralImpl() { this = TEncoding(g) }
// TODO: return the encoding defined by a magic encoding: comment, if any.
final string getValue() { result = "UTF-8" }
final override string toString() { result = "__ENCODING__" }
}
private class RequiredLineLiteralConstantValue extends RequiredConstantValue {
override predicate requiredInt(int i) { i = any(LineLiteral ll).getLocation().getStartLine() }
class LineLiteralImpl extends Expr, TLine {
private Ruby::Line g;
LineLiteralImpl() { this = TLine(g) }
final int getValue() { result = this.getLocation().getStartLine() }
final override string toString() { result = "__LINE__" }
}
private class RequiredFileLiteralConstantValue extends RequiredConstantValue {
override predicate requiredString(string s) {
s = any(FileLiteral fl).getLocation().getFile().getAbsolutePath()
}
}
class FileLiteralImpl extends Expr, TFile {
private Ruby::File g;
private class RequiredStringTextComponentConstantValue extends RequiredConstantValue {
override predicate requiredString(string s) {
s = any(Ruby::Token t | exists(TStringTextComponentNonRegexp(t))).getValue()
}
}
FileLiteralImpl() { this = TFile(g) }
private class RequiredStringEscapeSequenceComponentConstantValue extends RequiredConstantValue {
override predicate requiredString(string s) {
s = any(Ruby::Token t | exists(TStringEscapeSequenceComponentNonRegexp(t))).getValue()
}
}
final string getValue() { result = this.getLocation().getFile().getAbsolutePath() }
class TRegExpComponent =
TStringTextComponentRegexp or TStringEscapeSequenceComponentRegexp or
TStringInterpolationComponentRegexp;
// Exclude components that are children of a free-spacing regex.
// We do this because `ParseRegExp.qll` cannot handle free-spacing regexes.
string getRegExpTextComponentValue(RegExpTextComponent c) {
exists(Ruby::Token t |
c = TStringTextComponentRegexp(t) and
not c.getParent().(RegExpLiteral).hasFreeSpacingFlag() and
result = t.getValue()
)
}
private class RequiredRegExpTextComponentConstantValue extends RequiredConstantValue {
override predicate requiredString(string s) { s = getRegExpTextComponentValue(_) }
}
// Exclude components that are children of a free-spacing regex.
// We do this because `ParseRegExp.qll` cannot handle free-spacing regexes.
string getRegExpEscapeSequenceComponentValue(RegExpEscapeSequenceComponent c) {
exists(Ruby::EscapeSequence e |
c = TStringEscapeSequenceComponentRegexp(e) and
not c.getParent().(RegExpLiteral).hasFreeSpacingFlag() and
result = e.getValue()
)
}
private class RequiredRegExpEscapeSequenceComponentConstantValue extends RequiredConstantValue {
override predicate requiredString(string s) { s = getRegExpEscapeSequenceComponentValue(_) }
}
class RegularStringLiteral extends StringLiteral, TRegularStringLiteral {
private Ruby::String g;
RegularStringLiteral() { this = TRegularStringLiteral(g) }
final override StringComponent getComponent(int n) { toGenerated(result) = g.getChild(n) }
}
class BareStringLiteral extends StringLiteral, TBareStringLiteral {
private Ruby::BareString g;
BareStringLiteral() { this = TBareStringLiteral(g) }
final override StringComponent getComponent(int n) { toGenerated(result) = g.getChild(n) }
}
// Tree-sitter gives us value text including the colon, which we skip.
string getSimpleSymbolValue(Ruby::SimpleSymbol ss) { result = ss.getValue().suffix(1) }
private class RequiredSimpleSymbolConstantValue extends RequiredConstantValue {
override predicate requiredSymbol(string s) { s = getSimpleSymbolValue(_) }
}
private class SimpleSymbolLiteral extends SymbolLiteral, TSimpleSymbolLiteral {
private Ruby::SimpleSymbol g;
SimpleSymbolLiteral() { this = TSimpleSymbolLiteral(g) }
final override ConstantValue::ConstantSymbolValue getConstantValue() {
result.isSymbol(getSimpleSymbolValue(g))
}
final override string toString() { result = g.getValue() }
}
class ComplexSymbolLiteral extends SymbolLiteral, TComplexSymbolLiteral { }
class DelimitedSymbolLiteral extends ComplexSymbolLiteral, TDelimitedSymbolLiteral {
private Ruby::DelimitedSymbol g;
DelimitedSymbolLiteral() { this = TDelimitedSymbolLiteral(g) }
final override StringComponent getComponent(int i) { toGenerated(result) = g.getChild(i) }
}
class BareSymbolLiteral extends ComplexSymbolLiteral, TBareSymbolLiteral {
private Ruby::BareSymbol g;
BareSymbolLiteral() { this = TBareSymbolLiteral(g) }
final override StringComponent getComponent(int i) { toGenerated(result) = g.getChild(i) }
}
private class RequiredHashKeySymbolConstantValue extends RequiredConstantValue {
override predicate requiredSymbol(string s) { s = any(Ruby::HashKeySymbol h).getValue() }
}
private class HashKeySymbolLiteral extends SymbolLiteral, THashKeySymbolLiteral {
private Ruby::HashKeySymbol g;
HashKeySymbolLiteral() { this = THashKeySymbolLiteral(g) }
final override ConstantValue::ConstantSymbolValue getConstantValue() {
result.isSymbol(g.getValue())
}
final override string toString() { result = ":" + g.getValue() }
}
private class RequiredCharacterConstantValue extends RequiredConstantValue {
override predicate requiredString(string s) { s = any(Ruby::Character c).getValue() }
final override string toString() { result = "__FILE__" }
}
abstract class ArrayLiteralImpl extends Literal, TArrayLiteral {
@@ -311,34 +217,56 @@ class HashLiteralImpl extends Literal, THashLiteral {
HashLiteralImpl() { this = THashLiteral(g) }
final int getNumberOfElementsImpl() { result = count(g.getChild(_)) }
final override string toString() { result = "{...}" }
}
class RangeLiteralReal extends RangeLiteral, TRangeLiteralReal {
abstract class RangeLiteralImpl extends Literal, TRangeLiteral {
abstract Expr getBeginImpl();
abstract Expr getEndImpl();
abstract predicate isInclusiveImpl();
abstract predicate isExclusiveImpl();
final override string toString() {
exists(string op |
this.isInclusiveImpl() and op = ".."
or
this.isExclusiveImpl() and op = "..."
|
result = "_ " + op + " _"
)
}
}
class RangeLiteralReal extends RangeLiteralImpl, TRangeLiteralReal {
private Ruby::Range g;
RangeLiteralReal() { this = TRangeLiteralReal(g) }
final override Expr getBegin() { toGenerated(result) = g.getBegin() }
final override Expr getBeginImpl() { toGenerated(result) = g.getBegin() }
final override Expr getEnd() { toGenerated(result) = g.getEnd() }
final override Expr getEndImpl() { toGenerated(result) = g.getEnd() }
final override predicate isInclusive() { g instanceof @ruby_range_dotdot }
final override predicate isInclusiveImpl() { g instanceof @ruby_range_dotdot }
final override predicate isExclusive() { g instanceof @ruby_range_dotdotdot }
final override predicate isExclusiveImpl() { g instanceof @ruby_range_dotdotdot }
}
class RangeLiteralSynth extends RangeLiteral, TRangeLiteralSynth {
class RangeLiteralSynth extends RangeLiteralImpl, TRangeLiteralSynth {
private boolean inclusive;
RangeLiteralSynth() { this = TRangeLiteralSynth(_, _, inclusive) }
final override Expr getBegin() { result = TIntegerLiteralSynth(this, 0, _) }
final override Expr getBeginImpl() { result = TIntegerLiteralSynth(this, 0, _) }
final override Expr getEnd() { result = TIntegerLiteralSynth(this, 1, _) }
final override Expr getEndImpl() { result = TIntegerLiteralSynth(this, 1, _) }
final override predicate isInclusive() { inclusive = true }
final override predicate isInclusiveImpl() { inclusive = true }
final override predicate isExclusive() { inclusive = false }
final override predicate isExclusiveImpl() { inclusive = false }
}
private string getMethodName(MethodName::Token t) {
@@ -347,18 +275,387 @@ private string getMethodName(MethodName::Token t) {
result = t.(Ruby::Setter).getName().getValue() + "="
}
private class RequiredMethodNameConstantValue extends RequiredConstantValue {
override predicate requiredString(string s) { s = getMethodName(_) }
}
class TokenMethodName extends MethodName, TTokenMethodName {
class TokenMethodName extends Expr, TTokenMethodName {
private MethodName::Token g;
TokenMethodName() { this = TTokenMethodName(g) }
final override ConstantValue::ConstantStringValue getConstantValue() {
result.isString(getMethodName(g))
}
final string getValue() { result = getMethodName(g) }
final override string toString() { result = getMethodName(g) }
}
abstract class StringComponentImpl extends AstNode, TStringComponent {
abstract string getValue();
}
abstract class StringTextComponentImpl extends StringComponentImpl, TStringTextComponentNonRegexp {
abstract string getRawTextImpl();
final override string toString() { result = this.getRawTextImpl() }
}
/**
* Gets the result of unescaping a string text component by replacing `\\` and
* `\'` with `\` and `'`, respectively.
*
* ```rb
* 'foo\\bar \'baz\'' # foo\bar 'baz'
* ```
*/
bindingset[text]
private string unescapeTextComponent(string text) {
result = text.regexpReplaceAll("\\\\(['\\\\])", "$1")
}
class StringTextComponentStringOrHeredocContent extends StringTextComponentImpl,
TStringTextComponentNonRegexpStringOrHeredocContent {
private Ruby::Token g;
StringTextComponentStringOrHeredocContent() {
this = TStringTextComponentNonRegexpStringOrHeredocContent(g)
}
final override string getValue() { result = this.getUnescapedText() }
final override string getRawTextImpl() { result = g.getValue() }
final private string getUnescapedText() { result = unescapeTextComponent(g.getValue()) }
}
private class StringTextComponentSimpleSymbol extends StringTextComponentImpl,
TStringTextComponentNonRegexpSimpleSymbol {
private Ruby::SimpleSymbol g;
StringTextComponentSimpleSymbol() { this = TStringTextComponentNonRegexpSimpleSymbol(g) }
// Tree-sitter gives us value text including the colon, which we skip.
private string getSimpleSymbolValue() { result = g.getValue().suffix(1) }
final override string getValue() { result = this.getSimpleSymbolValue() }
final override string getRawTextImpl() { result = this.getSimpleSymbolValue() }
}
private class StringTextComponentHashKeySymbol extends StringTextComponentImpl,
TStringTextComponentNonRegexpHashKeySymbol {
private Ruby::HashKeySymbol g;
StringTextComponentHashKeySymbol() { this = TStringTextComponentNonRegexpHashKeySymbol(g) }
final override string getValue() { result = g.getValue() }
final override string getRawTextImpl() { result = g.getValue() }
}
bindingset[escaped]
private string unescapeKnownEscapeSequence(string escaped) {
escaped = "\\\\" and result = "\\"
or
escaped = "\\'" and result = "'"
or
escaped = "\\\"" and result = "\""
or
escaped = "\\a" and result = 7.toUnicode()
or
escaped = "\\b" and result = 8.toUnicode()
or
escaped = "\\t" and result = "\t"
or
escaped = "\\n" and result = "\n"
or
escaped = "\\v" and result = 11.toUnicode()
or
escaped = "\\f" and result = 12.toUnicode()
or
escaped = "\\r" and result = "\r"
or
escaped = "\\e" and result = 27.toUnicode()
or
escaped = "\\s" and result = " "
or
escaped = ["\\c?", "\\C-?"] and result = 127.toUnicode()
or
result = parseOctalInt(escaped.regexpCapture("\\\\([0-7]{1,3})", 1)).toUnicode()
or
result = parseHexInt(escaped.regexpCapture("\\\\x([0-9a-fA-F]{1,2})", 1)).toUnicode()
or
result = parseHexInt(escaped.regexpCapture("\\\\u([0-9a-fA-F]{4})", 1)).toUnicode()
or
result = parseHexInt(escaped.regexpCapture("\\\\u\\{([0-9a-fA-F]{1,6})\\}", 1)).toUnicode()
}
/**
* Gets the string represented by the escape sequence in `escaped`. For example:
*
* ```
* \\ => \
* \141 => a
* \u0078 => x
* ```
*/
bindingset[escaped]
private string unescapeEscapeSequence(string escaped) {
result = unescapeKnownEscapeSequence(escaped)
or
// Any other character following a backslash is just that character.
not exists(unescapeKnownEscapeSequence(escaped)) and
result = escaped.suffix(1)
}
/**
* An escape sequence component of a string or string-like literal.
*/
class StringEscapeSequenceComponentImpl extends StringComponentImpl,
TStringEscapeSequenceComponentNonRegexp {
private Ruby::EscapeSequence g;
StringEscapeSequenceComponentImpl() { this = TStringEscapeSequenceComponentNonRegexp(g) }
final override string getValue() { result = this.getUnescapedText() }
final string getRawTextImpl() { result = g.getValue() }
final private string getUnescapedText() { result = unescapeEscapeSequence(g.getValue()) }
final override string toString() { result = this.getRawTextImpl() }
}
class StringInterpolationComponentImpl extends StringComponentImpl,
TStringInterpolationComponentNonRegexp {
private Ruby::Interpolation g;
StringInterpolationComponentImpl() { this = TStringInterpolationComponentNonRegexp(g) }
// requires the CFG
final override string getValue() { none() }
final override string toString() { result = "#{...}" }
}
private class TRegExpComponent =
TStringTextComponentRegexp or TStringEscapeSequenceComponentRegexp or
TStringInterpolationComponentRegexp;
abstract class RegExpComponentImpl extends StringComponentImpl, TRegExpComponent { }
class RegExpTextComponentImpl extends RegExpComponentImpl, TStringTextComponentRegexp {
private Ruby::Token g;
RegExpTextComponentImpl() { this = TStringTextComponentRegexp(g) }
final override string getValue() {
// Exclude components that are children of a free-spacing regex.
// We do this because `ParseRegExp.qll` cannot handle free-spacing regexes.
not this.getParent().(RegExpLiteral).hasFreeSpacingFlag() and
result = g.getValue()
}
final override string toString() { result = g.getValue() }
}
class RegExpEscapeSequenceComponentImpl extends RegExpComponentImpl,
TStringEscapeSequenceComponentRegexp {
private Ruby::EscapeSequence g;
RegExpEscapeSequenceComponentImpl() { this = TStringEscapeSequenceComponentRegexp(g) }
final override string getValue() {
// Exclude components that are children of a free-spacing regex.
// We do this because `ParseRegExp.qll` cannot handle free-spacing regexes.
not this.getParent().(RegExpLiteral).hasFreeSpacingFlag() and
result = g.getValue()
}
final override string toString() { result = g.getValue() }
}
class RegExpInterpolationComponentImpl extends RegExpComponentImpl,
TStringInterpolationComponentRegexp {
private Ruby::Interpolation g;
RegExpInterpolationComponentImpl() { this = TStringInterpolationComponentRegexp(g) }
// requires the CFG
final override string getValue() { none() }
final override string toString() { result = "#{...}" }
}
abstract class StringlikeLiteralImpl extends Expr, TStringlikeLiteral {
abstract StringComponentImpl getComponentImpl(int n);
private string getStartDelimiter() {
this instanceof TStringLiteral and
result = "\""
or
this instanceof TRegExpLiteral and
result = "/"
or
this instanceof TSimpleSymbolLiteral and
result = ":"
or
this instanceof TComplexSymbolLiteral and
result = ":\""
or
this instanceof THashKeySymbolLiteral and
result = ""
or
this instanceof TSubshellLiteral and
result = "`"
or
this instanceof THereDoc and
result = ""
}
private string getEndDelimiter() {
this instanceof TStringLiteral and
result = "\""
or
this instanceof TRegExpLiteral and
result = "/"
or
this instanceof TSimpleSymbolLiteral and
result = ""
or
this instanceof TComplexSymbolLiteral and
result = "\""
or
this instanceof THashKeySymbolLiteral and
result = ""
or
this instanceof TSubshellLiteral and
result = "`"
or
this instanceof THereDoc and
result = ""
}
override string toString() {
exists(string full, string summary |
full =
concat(StringComponent c, int i, string s |
c = this.getComponentImpl(i) and
(
s = toGenerated(c).(Ruby::Token).getValue()
or
not toGenerated(c) instanceof Ruby::Token and
s = "#{...}"
)
|
s order by i
) and
(
// summary should be 32 chars max (incl. ellipsis)
full.length() > 32 and summary = full.substring(0, 29) + "..."
or
full.length() <= 32 and summary = full
) and
result = this.getStartDelimiter() + summary + this.getEndDelimiter()
)
}
// 0 components results in the empty string
// if all interpolations have a known string value, we will get a result
language[monotonicAggregates]
final string getStringValue() {
result =
concat(StringComponentImpl c, int i | c = this.getComponentImpl(i) | c.getValue() order by i)
}
}
abstract class StringLiteralImpl extends StringlikeLiteralImpl, TStringLiteral { }
class RegularStringLiteral extends StringLiteralImpl, TRegularStringLiteral {
private Ruby::String g;
RegularStringLiteral() { this = TRegularStringLiteral(g) }
final override StringComponent getComponentImpl(int n) { toGenerated(result) = g.getChild(n) }
}
class BareStringLiteral extends StringLiteralImpl, TBareStringLiteral {
private Ruby::BareString g;
BareStringLiteral() { this = TBareStringLiteral(g) }
final override StringComponent getComponentImpl(int n) { toGenerated(result) = g.getChild(n) }
}
abstract class SymbolLiteralImpl extends StringlikeLiteralImpl, TSymbolLiteral { }
class SimpleSymbolLiteral extends SymbolLiteralImpl, TSimpleSymbolLiteral {
private Ruby::SimpleSymbol g;
SimpleSymbolLiteral() { this = TSimpleSymbolLiteral(g) }
final override StringComponent getComponentImpl(int n) { n = 0 and toGenerated(result) = g }
final override string toString() { result = g.getValue() }
}
abstract class ComplexSymbolLiteral extends SymbolLiteralImpl, TComplexSymbolLiteral { }
class DelimitedSymbolLiteral extends ComplexSymbolLiteral, TDelimitedSymbolLiteral {
private Ruby::DelimitedSymbol g;
DelimitedSymbolLiteral() { this = TDelimitedSymbolLiteral(g) }
final override StringComponent getComponentImpl(int i) { toGenerated(result) = g.getChild(i) }
}
class BareSymbolLiteral extends ComplexSymbolLiteral, TBareSymbolLiteral {
private Ruby::BareSymbol g;
BareSymbolLiteral() { this = TBareSymbolLiteral(g) }
final override StringComponent getComponentImpl(int i) { toGenerated(result) = g.getChild(i) }
}
private class HashKeySymbolLiteral extends SymbolLiteralImpl, THashKeySymbolLiteral {
private Ruby::HashKeySymbol g;
HashKeySymbolLiteral() { this = THashKeySymbolLiteral(g) }
final override StringComponent getComponentImpl(int n) { n = 0 and toGenerated(result) = g }
final override string toString() { result = ":" + g.getValue() }
}
class RegExpLiteralImpl extends StringlikeLiteralImpl, TRegExpLiteral {
private Ruby::Regex g;
RegExpLiteralImpl() { this = TRegExpLiteral(g) }
final override RegExpComponentImpl getComponentImpl(int i) { toGenerated(result) = g.getChild(i) }
}
class SubshellLiteralImpl extends StringlikeLiteralImpl, TSubshellLiteral {
private Ruby::Subshell g;
SubshellLiteralImpl() { this = TSubshellLiteral(g) }
final override StringComponent getComponentImpl(int i) { toGenerated(result) = g.getChild(i) }
}
class CharacterLiteralImpl extends Expr, TCharacterLiteral {
private Ruby::Character g;
CharacterLiteralImpl() { this = TCharacterLiteral(g) }
final string getValue() { result = g.getValue() }
final override string toString() { result = g.getValue() }
}
class HereDocImpl extends StringlikeLiteralImpl, THereDoc {
private Ruby::HeredocBeginning g;
HereDocImpl() { this = THereDoc(g) }
final override StringComponent getComponentImpl(int n) {
toGenerated(result) = getHereDocBody(g).getChild(n)
}
final override string toString() { result = g.getValue() }
}

View File

@@ -303,6 +303,14 @@ private module SetterDesugar {
}
final override predicate location(AstNode n, Location l) {
exists(SetterMethodCall sae, Assignment arg, AstNode lhs, AstNode rhs |
arg = sae.getArgument(sae.getNumberOfArguments() - 1) and
lhs = arg.getLeftOperand() and
rhs = arg.getRightOperand() and
n = [arg, lhs] and
hasLocation(rhs, l)
)
or
exists(SetterAssignExpr sae, StmtSequence seq |
seq = sae.getDesugared() and
l = sae.getMethodCallLocation() and

View File

@@ -406,7 +406,11 @@ import Cached
/** Holds if this scope inherits `name` from an outer scope `outer`. */
private predicate inherits(Scope::Range scope, string name, Scope::Range outer) {
(scope instanceof Ruby::Block or scope instanceof Ruby::DoBlock) and
(
scope instanceof Ruby::Block or
scope instanceof Ruby::DoBlock or
scope instanceof Ruby::Lambda
) and
not scopeDefinesParameterVariable(scope, name, _) and
(
outer = scope.getOuterScope() and

View File

@@ -4,6 +4,7 @@ private import codeql.ruby.AST
private import codeql.ruby.controlflow.BasicBlocks
private import codeql.ruby.dataflow.SSA
private import codeql.ruby.ast.internal.Constant
private import codeql.ruby.ast.internal.Literal
private import ControlFlowGraph
private import internal.ControlFlowGraphImpl
private import internal.Splitting
@@ -101,13 +102,6 @@ class ExprCfgNode extends AstCfgNode {
/** Gets the underlying expression. */
Expr getExpr() { result = e }
private ExprCfgNode getSource() {
exists(Ssa::WriteDefinition def |
def.assigns(result) and
this = def.getARead()
)
}
/**
* DEPRECATED: Use `getConstantValue` instead.
*
@@ -116,8 +110,7 @@ class ExprCfgNode extends AstCfgNode {
deprecated string getValueText() { result = this.getConstantValue().toString() }
/** Gets the constant value of this expression, if any. */
cached
ConstantValue getConstantValue() { result = this.getSource().getConstantValue() }
ConstantValue getConstantValue() { result = getConstantValue(this) }
}
/** A control-flow node that wraps a return-like statement. */
@@ -142,11 +135,8 @@ class StringComponentCfgNode extends AstCfgNode {
}
/** A control-flow node that wraps a `RegExpComponent` AST expression. */
class RegExpComponentCfgNode extends AstCfgNode {
RegExpComponentCfgNode() { this.getNode() instanceof RegExpComponent }
/** Gets the constant value of this regex component. */
ConstantValue getConstantValue() { result = this.getNode().(RegExpComponent).getConstantValue() }
class RegExpComponentCfgNode extends StringComponentCfgNode {
RegExpComponentCfgNode() { e instanceof RegExpComponent }
}
private AstNode desugar(AstNode n) {
@@ -232,8 +222,6 @@ module ExprNodes {
override LiteralChildMapping e;
override Literal getExpr() { result = super.getExpr() }
override ConstantValue getConstantValue() { result = e.getConstantValue() }
}
private class AssignExprChildMapping extends ExprChildMapping, AssignExpr {
@@ -263,31 +251,13 @@ module ExprNodes {
override Operation getExpr() { result = super.getExpr() }
/** Gets the operator of this operation. */
string getOperator() { result = this.getExpr().getOperator() }
/** Gets an operand of this operation. */
final ExprCfgNode getAnOperand() { e.hasCfgChild(e.getAnOperand(), this, result) }
}
private predicate unaryConstFold(UnaryOperationCfgNode unop, string op, ConstantValue value) {
value = unop.getOperand().getConstantValue() and
op = unop.getExpr().getOperator()
}
private class RequiredUnaryConstantValue extends RequiredConstantValue {
override predicate requiredInt(int i) {
exists(ConstantValue value |
unaryConstFold(_, "-", value) and
i = -value.getInt()
)
}
override predicate requiredFloat(float f) {
exists(ConstantValue value |
unaryConstFold(_, "-", value) and
f = -value.getFloat()
)
}
}
/** A control-flow node that wraps a `UnaryOperation` AST expression. */
class UnaryOperationCfgNode extends OperationCfgNode {
private UnaryOperation uo;
@@ -298,93 +268,6 @@ module ExprNodes {
/** Gets the operand of this unary operation. */
final ExprCfgNode getOperand() { e.hasCfgChild(uo.getOperand(), this, result) }
final override ConstantValue getConstantValue() {
// TODO: Implement support for complex numbers and rational numbers
exists(string op, ConstantValue value | unaryConstFold(this, op, value) |
op = "+" and
result = value
or
op = "-" and
(
result.isInt(-value.getInt())
or
result.isFloat(-value.getFloat())
)
)
}
}
private predicate binaryConstFold(
BinaryOperationCfgNode binop, string op, ConstantValue left, ConstantValue right
) {
left = binop.getLeftOperand().getConstantValue() and
right = binop.getRightOperand().getConstantValue() and
op = binop.getExpr().getOperator()
}
private class RequiredBinaryConstantValue extends RequiredConstantValue {
override predicate requiredInt(int i) {
exists(string op, ConstantValue left, ConstantValue right |
binaryConstFold(_, op, left, right)
|
op = "+" and
i = left.getInt() + right.getInt()
or
op = "-" and
i = left.getInt() - right.getInt()
or
op = "*" and
i = left.getInt() * right.getInt()
or
op = "/" and
i = left.getInt() / right.getInt()
)
}
override predicate requiredFloat(float f) {
exists(string op, ConstantValue left, ConstantValue right |
binaryConstFold(_, op, left, right)
|
op = "+" and
f =
[
left.getFloat() + right.getFloat(), left.getInt() + right.getFloat(),
left.getFloat() + right.getInt()
]
or
op = "-" and
f =
[
left.getFloat() - right.getFloat(), left.getInt() - right.getFloat(),
left.getFloat() - right.getInt()
]
or
op = "*" and
f =
[
left.getFloat() * right.getFloat(), left.getInt() * right.getFloat(),
left.getFloat() * right.getInt()
]
or
op = "/" and
f =
[
left.getFloat() / right.getFloat(), left.getInt() / right.getFloat(),
left.getFloat() / right.getInt()
]
)
}
override predicate requiredString(string s) {
exists(string op, ConstantValue left, ConstantValue right |
binaryConstFold(_, op, left, right)
|
op = "+" and
s = left.getString() + right.getString() and
s.length() <= 10000
)
}
}
/** A control-flow node that wraps a `BinaryOperation` AST expression. */
@@ -400,59 +283,6 @@ module ExprNodes {
/** Gets the right operand of this binary operation. */
final ExprCfgNode getRightOperand() { e.hasCfgChild(bo.getRightOperand(), this, result) }
final override ConstantValue getConstantValue() {
// TODO: Implement support for complex numbers and rational numbers
exists(string op, ConstantValue left, ConstantValue right |
binaryConstFold(this, op, left, right)
|
op = "+" and
(
result.isInt(left.getInt() + right.getInt())
or
result
.isFloat([
left.getFloat() + right.getFloat(), left.getInt() + right.getFloat(),
left.getFloat() + right.getInt()
])
or
result.isString(left.getString() + right.getString())
)
or
op = "-" and
(
result.isInt(left.getInt() - right.getInt())
or
result
.isFloat([
left.getFloat() - right.getFloat(), left.getInt() - right.getFloat(),
left.getFloat() - right.getInt()
])
)
or
op = "*" and
(
result.isInt(left.getInt() * right.getInt())
or
result
.isFloat([
left.getFloat() * right.getFloat(), left.getInt() * right.getFloat(),
left.getFloat() * right.getInt()
])
)
or
op = "/" and
(
result.isInt(left.getInt() / right.getInt())
or
result
.isFloat([
left.getFloat() / right.getFloat(), left.getInt() / right.getFloat(),
left.getFloat() / right.getInt()
])
)
)
}
}
private class BlockArgumentChildMapping extends ExprChildMapping, BlockArgument {
@@ -722,8 +552,6 @@ module ExprNodes {
/** Gets the scope expression. */
final ExprCfgNode getScopeExpr() { e.hasCfgChild(e.getScopeExpr(), this, result) }
override ConstantValue getConstantValue() { result = this.getExpr().getConstantValue() }
}
private class StmtSequenceChildMapping extends ExprChildMapping, StmtSequence {
@@ -794,6 +622,20 @@ module ExprNodes {
final override VariableWriteAccess getExpr() { result = ExprCfgNode.super.getExpr() }
}
/** A control-flow node that wraps a `ConstantReadAccess` AST expression. */
class ConstantReadAccessCfgNode extends ExprCfgNode {
override ConstantReadAccess e;
final override ConstantReadAccess getExpr() { result = ExprCfgNode.super.getExpr() }
}
/** A control-flow node that wraps a `ConstantWriteAccess` AST expression. */
class ConstantWriteAccessCfgNode extends ExprCfgNode {
override ConstantWriteAccess e;
final override ConstantWriteAccess getExpr() { result = ExprCfgNode.super.getExpr() }
}
/** A control-flow node that wraps a `InstanceVariableWriteAccess` AST expression. */
class InstanceVariableWriteAccessCfgNode extends ExprCfgNode {
override InstanceVariableWriteAccess e;
@@ -805,11 +647,8 @@ module ExprNodes {
class StringInterpolationComponentCfgNode extends StringComponentCfgNode, StmtSequenceCfgNode {
StringInterpolationComponentCfgNode() { this.getNode() instanceof StringInterpolationComponent }
// If last statement in the interpolation is a constant or local variable read,
// we attempt to look up its string value.
// If there's a result, we return that as the string value of the interpolation.
final override ConstantValue getConstantValue() {
result = this.getLastStmt().getConstantValue()
result = StmtSequenceCfgNode.super.getConstantValue()
}
}
@@ -817,10 +656,8 @@ module ExprNodes {
class RegExpInterpolationComponentCfgNode extends RegExpComponentCfgNode, StmtSequenceCfgNode {
RegExpInterpolationComponentCfgNode() { this.getNode() instanceof RegExpInterpolationComponent }
// If last statement in the interpolation is a constant or local variable read,
// attempt to look up its definition and return the definition's `getConstantValue()`.
final override ConstantValue getConstantValue() {
result = this.getLastStmt().getConstantValue()
result = StmtSequenceCfgNode.super.getConstantValue()
}
}
@@ -828,56 +665,17 @@ module ExprNodes {
override predicate relevantChild(AstNode n) { n = this.getComponent(_) }
}
pragma[nomagic]
private string getStringComponentCfgNodeValue(StringComponentCfgNode c) {
result = c.getConstantValue().toString()
}
// 0 components results in the empty string
// if all interpolations have a known string value, we will get a result
language[monotonicAggregates]
private string getStringlikeLiteralCfgNodeValue(StringlikeLiteralCfgNode n) {
result =
concat(StringComponentCfgNode c, int i |
c = n.getComponent(i)
|
getStringComponentCfgNodeValue(c) order by i
)
}
private class RequiredStringlikeLiteralConstantValue extends RequiredConstantValue {
override predicate requiredString(string s) {
exists(StringlikeLiteralCfgNode n |
s = getStringlikeLiteralCfgNodeValue(n) and
not n.getExpr() instanceof SymbolLiteral
)
}
override predicate requiredSymbol(string s) {
exists(StringlikeLiteralCfgNode n |
s = getStringlikeLiteralCfgNodeValue(n) and
n.getExpr() instanceof SymbolLiteral
)
}
}
/** A control-flow node that wraps a `StringlikeLiteral` AST expression. */
class StringlikeLiteralCfgNode extends ExprCfgNode {
override StringlikeLiteralChildMapping e;
final override StringlikeLiteral getExpr() { result = super.getExpr() }
override StringlikeLiteral getExpr() { result = super.getExpr() }
/** Gets the `n`th component of this `StringlikeLiteral` */
StringComponentCfgNode getComponent(int n) { e.hasCfgChild(e.getComponent(n), this, result) }
/** Gets a component of this `StringlikeLiteral` */
StringComponentCfgNode getAComponent() { result = this.getComponent(_) }
final override ConstantValue getConstantValue() {
if this.getExpr() instanceof SymbolLiteral
then result.isSymbol(getStringlikeLiteralCfgNodeValue(this))
else result.isString(getStringlikeLiteralCfgNodeValue(this))
}
}
/** A control-flow node that wraps a `StringLiteral` AST expression. */
@@ -887,40 +685,15 @@ module ExprNodes {
final override StringLiteral getExpr() { result = super.getExpr() }
}
private class RegExpLiteralChildMapping extends ExprChildMapping, RegExpLiteral {
override predicate relevantChild(AstNode n) { n = this.getComponent(_) }
}
pragma[nomagic]
private string getRegExpComponentCfgNodeValue(RegExpComponentCfgNode c) {
result = c.getConstantValue().toString()
}
language[monotonicAggregates]
private string getRegExpLiteralCfgNodeValue(RegExpLiteralCfgNode n) {
result =
concat(RegExpComponentCfgNode c, int i |
c = n.getComponent(i)
|
getRegExpComponentCfgNodeValue(c) order by i
)
}
private class RequiredRexExpLiteralConstantValue extends RequiredConstantValue {
override predicate requiredString(string s) { s = getRegExpLiteralCfgNodeValue(_) }
}
/** A control-flow node that wraps a `RegExpLiteral` AST expression. */
class RegExpLiteralCfgNode extends ExprCfgNode {
override RegExpLiteralChildMapping e;
class RegExpLiteralCfgNode extends StringlikeLiteralCfgNode {
RegExpLiteralCfgNode() { e instanceof RegExpLiteral }
RegExpComponentCfgNode getComponent(int n) { e.hasCfgChild(e.getComponent(n), this, result) }
final override RegExpComponentCfgNode getComponent(int n) { result = super.getComponent(n) }
final override RegExpComponentCfgNode getAComponent() { result = super.getAComponent() }
final override RegExpLiteral getExpr() { result = super.getExpr() }
final override ConstantValue::ConstantStringValue getConstantValue() {
result.isString(getRegExpLiteralCfgNodeValue(this))
}
}
/** A control-flow node that wraps a `ComparisonOperation` AST expression. */

View File

@@ -1,4 +1,4 @@
private import ruby as rb
private import ruby as RB
private import ControlFlowGraphImpl as Impl
private import Completion as Comp
private import codeql.ruby.ast.internal.Synthesis
@@ -6,11 +6,11 @@ private import Splitting as Splitting
private import codeql.ruby.CFG as CFG
/** The base class for `ControlFlowTree`. */
class ControlFlowTreeBase extends rb::AstNode {
class ControlFlowTreeBase extends RB::AstNode {
ControlFlowTreeBase() { not any(Synthesis s).excludeFromControlFlowTree(this) }
}
class ControlFlowElement = rb::AstNode;
class ControlFlowElement = RB::AstNode;
class Completion = Comp::Completion;
@@ -69,6 +69,6 @@ predicate isAbnormalExitType(SuccessorType t) {
t instanceof CFG::SuccessorTypes::ExitSuccessor
}
class Location = rb::Location;
class Location = RB::Location;
class Node = CFG::CfgNode;

View File

@@ -2,6 +2,8 @@
import ruby
import codeql.ruby.DataFlow
private import codeql.ruby.frameworks.data.ModelsAsData
private import codeql.ruby.ApiGraphs
private import internal.FlowSummaryImpl as Impl
private import internal.DataFlowDispatch
private import internal.DataFlowPrivate
@@ -165,3 +167,33 @@ private class SummarizedCallableAdapter extends Impl::Public::SummarizedCallable
}
class RequiredSummaryComponentStack = Impl::Public::RequiredSummaryComponentStack;
private class SummarizedCallableFromModel extends SummarizedCallable {
string package;
string type;
string path;
SummarizedCallableFromModel() {
ModelOutput::relevantSummaryModel(package, type, path, _, _, _) and
this = package + ";" + type + ";" + path
}
override Call getACall() {
exists(API::MethodAccessNode base |
ModelOutput::resolvedSummaryBase(package, type, path, base) and
result = base.getCallNode().asExpr().getExpr()
)
}
override predicate propagatesFlowExt(string input, string output, boolean preservesValue) {
exists(string kind |
ModelOutput::relevantSummaryModel(package, type, path, input, output, kind)
|
kind = "value" and
preservesValue = true
or
kind = "taint" and
preservesValue = false
)
}
}

View File

@@ -257,6 +257,8 @@ private module Cached {
name = any(KeywordParameter kp).getName()
or
exists(any(Call c).getKeywordArgument(name))
or
FlowSummaryImplSpecific::ParsePositions::isParsedKeywordParameterPosition(_, name)
}
cached
@@ -270,7 +272,11 @@ private module Cached {
or
FlowSummaryImplSpecific::ParsePositions::isParsedArgumentPosition(_, pos)
} or
TKeywordParameterPosition(string name) { name = any(KeywordParameter kp).getName() }
TKeywordParameterPosition(string name) {
name = any(KeywordParameter kp).getName()
or
FlowSummaryImplSpecific::ParsePositions::isParsedKeywordArgumentPosition(_, name)
}
}
import Cached

View File

@@ -87,12 +87,30 @@ abstract class Configuration extends string {
/** Holds if data flow into `node` is prohibited. */
predicate isBarrierIn(Node node) { none() }
/**
* Holds if data flow into `node` is prohibited when the flow state is
* `state`
*/
predicate isBarrierIn(Node node, FlowState state) { none() }
/** Holds if data flow out of `node` is prohibited. */
predicate isBarrierOut(Node node) { none() }
/**
* Holds if data flow out of `node` is prohibited when the flow state is
* `state`
*/
predicate isBarrierOut(Node node, FlowState state) { none() }
/** Holds if data flow through nodes guarded by `guard` is prohibited. */
predicate isBarrierGuard(BarrierGuard guard) { none() }
/**
* Holds if data flow through nodes guarded by `guard` is prohibited when
* the flow state is `state`
*/
predicate isBarrierGuard(BarrierGuard guard, FlowState state) { none() }
/**
* Holds if the additional flow step from `node1` to `node2` must be taken
* into account in the analysis.
@@ -305,7 +323,7 @@ private class RetNodeEx extends NodeEx {
ReturnKindExt getKind() { result = this.asNode().(ReturnNodeExt).getKind() }
}
private predicate inBarrier(NodeEx node, Configuration config) {
private predicate fullInBarrier(NodeEx node, Configuration config) {
exists(Node n |
node.asNode() = n and
config.isBarrierIn(n)
@@ -314,7 +332,16 @@ private predicate inBarrier(NodeEx node, Configuration config) {
)
}
private predicate outBarrier(NodeEx node, Configuration config) {
private predicate stateInBarrier(NodeEx node, FlowState state, Configuration config) {
exists(Node n |
node.asNode() = n and
config.isBarrierIn(n, state)
|
config.isSource(n, state)
)
}
private predicate fullOutBarrier(NodeEx node, Configuration config) {
exists(Node n |
node.asNode() = n and
config.isBarrierOut(n)
@@ -323,6 +350,15 @@ private predicate outBarrier(NodeEx node, Configuration config) {
)
}
private predicate stateOutBarrier(NodeEx node, FlowState state, Configuration config) {
exists(Node n |
node.asNode() = n and
config.isBarrierOut(n, state)
|
config.isSink(n, state)
)
}
pragma[nomagic]
private predicate fullBarrier(NodeEx node, Configuration config) {
exists(Node n | node.asNode() = n |
@@ -345,9 +381,19 @@ private predicate fullBarrier(NodeEx node, Configuration config) {
pragma[nomagic]
private predicate stateBarrier(NodeEx node, FlowState state, Configuration config) {
exists(Node n |
node.asNode() = n and
exists(Node n | node.asNode() = n |
config.isBarrier(n, state)
or
config.isBarrierIn(n, state) and
not config.isSource(n, state)
or
config.isBarrierOut(n, state) and
not config.isSink(n, state)
or
exists(BarrierGuard g |
config.isBarrierGuard(g, state) and
n = g.getAGuardedNode()
)
)
}
@@ -376,8 +422,8 @@ private predicate sinkNode(NodeEx node, FlowState state, Configuration config) {
/** Provides the relevant barriers for a step from `node1` to `node2`. */
pragma[inline]
private predicate stepFilter(NodeEx node1, NodeEx node2, Configuration config) {
not outBarrier(node1, config) and
not inBarrier(node2, config) and
not fullOutBarrier(node1, config) and
not fullInBarrier(node2, config) and
not fullBarrier(node1, config) and
not fullBarrier(node2, config)
}
@@ -430,6 +476,8 @@ private predicate additionalLocalStateStep(
config.isAdditionalFlowStep(n1, s1, n2, s2) and
getNodeEnclosingCallable(n1) = getNodeEnclosingCallable(n2) and
stepFilter(node1, node2, config) and
not stateOutBarrier(node1, s1, config) and
not stateInBarrier(node2, s2, config) and
not stateBarrier(node1, s1, config) and
not stateBarrier(node2, s2, config)
)
@@ -471,6 +519,8 @@ private predicate additionalJumpStateStep(
config.isAdditionalFlowStep(n1, s1, n2, s2) and
getNodeEnclosingCallable(n1) != getNodeEnclosingCallable(n2) and
stepFilter(node1, node2, config) and
not stateOutBarrier(node1, s1, config) and
not stateInBarrier(node2, s2, config) and
not stateBarrier(node1, s1, config) and
not stateBarrier(node2, s2, config) and
not config.getAFeature() instanceof FeatureEqualSourceSinkCallContext
@@ -870,8 +920,8 @@ private module Stage1 {
private predicate throughFlowNodeCand(NodeEx node, Configuration config) {
revFlow(node, true, config) and
fwdFlow(node, true, config) and
not inBarrier(node, config) and
not outBarrier(node, config)
not fullInBarrier(node, config) and
not fullOutBarrier(node, config)
}
/** Holds if flow may return from `callable`. */
@@ -966,8 +1016,8 @@ private predicate flowOutOfCallNodeCand1(
) {
viableReturnPosOutNodeCand1(call, ret.getReturnPosition(), out, config) and
Stage1::revFlow(ret, config) and
not outBarrier(ret, config) and
not inBarrier(out, config)
not fullOutBarrier(ret, config) and
not fullInBarrier(out, config)
}
pragma[nomagic]
@@ -988,8 +1038,8 @@ private predicate flowIntoCallNodeCand1(
) {
viableParamArgNodeCand1(call, p, arg, config) and
Stage1::revFlow(p, config) and
not outBarrier(arg, config) and
not inBarrier(p, config)
not fullOutBarrier(arg, config) and
not fullInBarrier(p, config)
}
/**
@@ -1706,18 +1756,31 @@ private module LocalFlowBigStep {
* Holds if `node` can be the first node in a maximal subsequence of local
* flow steps in a dataflow path.
*/
predicate localFlowEntry(NodeEx node, FlowState state, Configuration config) {
private predicate localFlowEntry(NodeEx node, FlowState state, Configuration config) {
Stage2::revFlow(node, state, config) and
(
sourceNode(node, state, config) or
jumpStep(_, node, config) or
additionalJumpStep(_, node, config) or
additionalJumpStateStep(_, _, node, state, config) or
node instanceof ParamNodeEx or
node.asNode() instanceof OutNodeExt or
store(_, _, node, _, config) or
read(_, _, node, config) or
sourceNode(node, state, config)
or
jumpStep(_, node, config)
or
additionalJumpStep(_, node, config)
or
additionalJumpStateStep(_, _, node, state, config)
or
node instanceof ParamNodeEx
or
node.asNode() instanceof OutNodeExt
or
store(_, _, node, _, config)
or
read(_, _, node, config)
or
node instanceof FlowCheckNode
or
exists(FlowState s |
additionalLocalStateStep(_, s, node, state, config) and
s != state
)
)
}
@@ -1737,6 +1800,9 @@ private module LocalFlowBigStep {
or
exists(NodeEx next, FlowState s | Stage2::revFlow(next, s, config) |
additionalJumpStateStep(node, state, next, s, config)
or
additionalLocalStateStep(node, state, next, s, config) and
s != state
)
or
Stage2::revFlow(node, state, config) and
@@ -1770,42 +1836,40 @@ private module LocalFlowBigStep {
*/
pragma[nomagic]
private predicate localFlowStepPlus(
NodeEx node1, FlowState state1, NodeEx node2, FlowState state2, boolean preservesValue,
DataFlowType t, Configuration config, LocalCallContext cc
NodeEx node1, FlowState state, NodeEx node2, boolean preservesValue, DataFlowType t,
Configuration config, LocalCallContext cc
) {
not isUnreachableInCallCached(node2.asNode(), cc.(LocalCallContextSpecificCall).getCall()) and
(
localFlowEntry(node1, pragma[only_bind_into](state1), pragma[only_bind_into](config)) and
localFlowEntry(node1, pragma[only_bind_into](state), pragma[only_bind_into](config)) and
(
localFlowStepNodeCand1(node1, node2, config) and
state1 = state2 and
preservesValue = true and
t = node1.getDataFlowType() // irrelevant dummy value
t = node1.getDataFlowType() and // irrelevant dummy value
Stage2::revFlow(node2, pragma[only_bind_into](state), pragma[only_bind_into](config))
or
additionalLocalFlowStepNodeCand2(node1, state1, node2, state2, config) and
additionalLocalFlowStepNodeCand2(node1, state, node2, state, config) and
preservesValue = false and
t = node2.getDataFlowType()
) and
node1 != node2 and
cc.relevantFor(node1.getEnclosingCallable()) and
not isUnreachableInCallCached(node1.asNode(), cc.(LocalCallContextSpecificCall).getCall()) and
Stage2::revFlow(node2, pragma[only_bind_into](state2), pragma[only_bind_into](config))
not isUnreachableInCallCached(node1.asNode(), cc.(LocalCallContextSpecificCall).getCall())
or
exists(NodeEx mid |
localFlowStepPlus(node1, state1, mid, pragma[only_bind_into](state2), preservesValue, t,
localFlowStepPlus(node1, pragma[only_bind_into](state), mid, preservesValue, t,
pragma[only_bind_into](config), cc) and
localFlowStepNodeCand1(mid, node2, config) and
not mid instanceof FlowCheckNode and
Stage2::revFlow(node2, pragma[only_bind_into](state2), pragma[only_bind_into](config))
Stage2::revFlow(node2, pragma[only_bind_into](state), pragma[only_bind_into](config))
)
or
exists(NodeEx mid, FlowState st |
localFlowStepPlus(node1, state1, mid, st, _, _, pragma[only_bind_into](config), cc) and
additionalLocalFlowStepNodeCand2(mid, st, node2, state2, config) and
exists(NodeEx mid |
localFlowStepPlus(node1, state, mid, _, _, pragma[only_bind_into](config), cc) and
additionalLocalFlowStepNodeCand2(mid, state, node2, state, config) and
not mid instanceof FlowCheckNode and
preservesValue = false and
t = node2.getDataFlowType() and
Stage2::revFlow(node2, state2, pragma[only_bind_into](config))
t = node2.getDataFlowType()
)
)
}
@@ -1819,9 +1883,19 @@ private module LocalFlowBigStep {
NodeEx node1, FlowState state1, NodeEx node2, FlowState state2, boolean preservesValue,
AccessPathFrontNil apf, Configuration config, LocalCallContext callContext
) {
localFlowStepPlus(node1, state1, node2, state2, preservesValue, apf.getType(), config,
callContext) and
localFlowExit(node2, state2, config)
localFlowStepPlus(node1, state1, node2, preservesValue, apf.getType(), config, callContext) and
localFlowExit(node2, state1, config) and
state1 = state2
or
additionalLocalFlowStepNodeCand2(node1, state1, node2, state2, config) and
state1 != state2 and
preservesValue = false and
apf = TFrontNil(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)
)
}
}
@@ -2695,10 +2769,10 @@ private module Stage4 {
bindingset[node, cc, config]
private LocalCc getLocalCc(NodeEx node, Cc cc, Configuration config) {
localFlowEntry(node, _, config) and
result =
getLocalCallContext(pragma[only_bind_into](pragma[only_bind_out](cc)),
node.getEnclosingCallable())
node.getEnclosingCallable()) and
exists(config)
}
private predicate localStep(

View File

@@ -87,12 +87,30 @@ abstract class Configuration extends string {
/** Holds if data flow into `node` is prohibited. */
predicate isBarrierIn(Node node) { none() }
/**
* Holds if data flow into `node` is prohibited when the flow state is
* `state`
*/
predicate isBarrierIn(Node node, FlowState state) { none() }
/** Holds if data flow out of `node` is prohibited. */
predicate isBarrierOut(Node node) { none() }
/**
* Holds if data flow out of `node` is prohibited when the flow state is
* `state`
*/
predicate isBarrierOut(Node node, FlowState state) { none() }
/** Holds if data flow through nodes guarded by `guard` is prohibited. */
predicate isBarrierGuard(BarrierGuard guard) { none() }
/**
* Holds if data flow through nodes guarded by `guard` is prohibited when
* the flow state is `state`
*/
predicate isBarrierGuard(BarrierGuard guard, FlowState state) { none() }
/**
* Holds if the additional flow step from `node1` to `node2` must be taken
* into account in the analysis.
@@ -305,7 +323,7 @@ private class RetNodeEx extends NodeEx {
ReturnKindExt getKind() { result = this.asNode().(ReturnNodeExt).getKind() }
}
private predicate inBarrier(NodeEx node, Configuration config) {
private predicate fullInBarrier(NodeEx node, Configuration config) {
exists(Node n |
node.asNode() = n and
config.isBarrierIn(n)
@@ -314,7 +332,16 @@ private predicate inBarrier(NodeEx node, Configuration config) {
)
}
private predicate outBarrier(NodeEx node, Configuration config) {
private predicate stateInBarrier(NodeEx node, FlowState state, Configuration config) {
exists(Node n |
node.asNode() = n and
config.isBarrierIn(n, state)
|
config.isSource(n, state)
)
}
private predicate fullOutBarrier(NodeEx node, Configuration config) {
exists(Node n |
node.asNode() = n and
config.isBarrierOut(n)
@@ -323,6 +350,15 @@ private predicate outBarrier(NodeEx node, Configuration config) {
)
}
private predicate stateOutBarrier(NodeEx node, FlowState state, Configuration config) {
exists(Node n |
node.asNode() = n and
config.isBarrierOut(n, state)
|
config.isSink(n, state)
)
}
pragma[nomagic]
private predicate fullBarrier(NodeEx node, Configuration config) {
exists(Node n | node.asNode() = n |
@@ -345,9 +381,19 @@ private predicate fullBarrier(NodeEx node, Configuration config) {
pragma[nomagic]
private predicate stateBarrier(NodeEx node, FlowState state, Configuration config) {
exists(Node n |
node.asNode() = n and
exists(Node n | node.asNode() = n |
config.isBarrier(n, state)
or
config.isBarrierIn(n, state) and
not config.isSource(n, state)
or
config.isBarrierOut(n, state) and
not config.isSink(n, state)
or
exists(BarrierGuard g |
config.isBarrierGuard(g, state) and
n = g.getAGuardedNode()
)
)
}
@@ -376,8 +422,8 @@ private predicate sinkNode(NodeEx node, FlowState state, Configuration config) {
/** Provides the relevant barriers for a step from `node1` to `node2`. */
pragma[inline]
private predicate stepFilter(NodeEx node1, NodeEx node2, Configuration config) {
not outBarrier(node1, config) and
not inBarrier(node2, config) and
not fullOutBarrier(node1, config) and
not fullInBarrier(node2, config) and
not fullBarrier(node1, config) and
not fullBarrier(node2, config)
}
@@ -430,6 +476,8 @@ private predicate additionalLocalStateStep(
config.isAdditionalFlowStep(n1, s1, n2, s2) and
getNodeEnclosingCallable(n1) = getNodeEnclosingCallable(n2) and
stepFilter(node1, node2, config) and
not stateOutBarrier(node1, s1, config) and
not stateInBarrier(node2, s2, config) and
not stateBarrier(node1, s1, config) and
not stateBarrier(node2, s2, config)
)
@@ -471,6 +519,8 @@ private predicate additionalJumpStateStep(
config.isAdditionalFlowStep(n1, s1, n2, s2) and
getNodeEnclosingCallable(n1) != getNodeEnclosingCallable(n2) and
stepFilter(node1, node2, config) and
not stateOutBarrier(node1, s1, config) and
not stateInBarrier(node2, s2, config) and
not stateBarrier(node1, s1, config) and
not stateBarrier(node2, s2, config) and
not config.getAFeature() instanceof FeatureEqualSourceSinkCallContext
@@ -870,8 +920,8 @@ private module Stage1 {
private predicate throughFlowNodeCand(NodeEx node, Configuration config) {
revFlow(node, true, config) and
fwdFlow(node, true, config) and
not inBarrier(node, config) and
not outBarrier(node, config)
not fullInBarrier(node, config) and
not fullOutBarrier(node, config)
}
/** Holds if flow may return from `callable`. */
@@ -966,8 +1016,8 @@ private predicate flowOutOfCallNodeCand1(
) {
viableReturnPosOutNodeCand1(call, ret.getReturnPosition(), out, config) and
Stage1::revFlow(ret, config) and
not outBarrier(ret, config) and
not inBarrier(out, config)
not fullOutBarrier(ret, config) and
not fullInBarrier(out, config)
}
pragma[nomagic]
@@ -988,8 +1038,8 @@ private predicate flowIntoCallNodeCand1(
) {
viableParamArgNodeCand1(call, p, arg, config) and
Stage1::revFlow(p, config) and
not outBarrier(arg, config) and
not inBarrier(p, config)
not fullOutBarrier(arg, config) and
not fullInBarrier(p, config)
}
/**
@@ -1706,18 +1756,31 @@ private module LocalFlowBigStep {
* Holds if `node` can be the first node in a maximal subsequence of local
* flow steps in a dataflow path.
*/
predicate localFlowEntry(NodeEx node, FlowState state, Configuration config) {
private predicate localFlowEntry(NodeEx node, FlowState state, Configuration config) {
Stage2::revFlow(node, state, config) and
(
sourceNode(node, state, config) or
jumpStep(_, node, config) or
additionalJumpStep(_, node, config) or
additionalJumpStateStep(_, _, node, state, config) or
node instanceof ParamNodeEx or
node.asNode() instanceof OutNodeExt or
store(_, _, node, _, config) or
read(_, _, node, config) or
sourceNode(node, state, config)
or
jumpStep(_, node, config)
or
additionalJumpStep(_, node, config)
or
additionalJumpStateStep(_, _, node, state, config)
or
node instanceof ParamNodeEx
or
node.asNode() instanceof OutNodeExt
or
store(_, _, node, _, config)
or
read(_, _, node, config)
or
node instanceof FlowCheckNode
or
exists(FlowState s |
additionalLocalStateStep(_, s, node, state, config) and
s != state
)
)
}
@@ -1737,6 +1800,9 @@ private module LocalFlowBigStep {
or
exists(NodeEx next, FlowState s | Stage2::revFlow(next, s, config) |
additionalJumpStateStep(node, state, next, s, config)
or
additionalLocalStateStep(node, state, next, s, config) and
s != state
)
or
Stage2::revFlow(node, state, config) and
@@ -1770,42 +1836,40 @@ private module LocalFlowBigStep {
*/
pragma[nomagic]
private predicate localFlowStepPlus(
NodeEx node1, FlowState state1, NodeEx node2, FlowState state2, boolean preservesValue,
DataFlowType t, Configuration config, LocalCallContext cc
NodeEx node1, FlowState state, NodeEx node2, boolean preservesValue, DataFlowType t,
Configuration config, LocalCallContext cc
) {
not isUnreachableInCallCached(node2.asNode(), cc.(LocalCallContextSpecificCall).getCall()) and
(
localFlowEntry(node1, pragma[only_bind_into](state1), pragma[only_bind_into](config)) and
localFlowEntry(node1, pragma[only_bind_into](state), pragma[only_bind_into](config)) and
(
localFlowStepNodeCand1(node1, node2, config) and
state1 = state2 and
preservesValue = true and
t = node1.getDataFlowType() // irrelevant dummy value
t = node1.getDataFlowType() and // irrelevant dummy value
Stage2::revFlow(node2, pragma[only_bind_into](state), pragma[only_bind_into](config))
or
additionalLocalFlowStepNodeCand2(node1, state1, node2, state2, config) and
additionalLocalFlowStepNodeCand2(node1, state, node2, state, config) and
preservesValue = false and
t = node2.getDataFlowType()
) and
node1 != node2 and
cc.relevantFor(node1.getEnclosingCallable()) and
not isUnreachableInCallCached(node1.asNode(), cc.(LocalCallContextSpecificCall).getCall()) and
Stage2::revFlow(node2, pragma[only_bind_into](state2), pragma[only_bind_into](config))
not isUnreachableInCallCached(node1.asNode(), cc.(LocalCallContextSpecificCall).getCall())
or
exists(NodeEx mid |
localFlowStepPlus(node1, state1, mid, pragma[only_bind_into](state2), preservesValue, t,
localFlowStepPlus(node1, pragma[only_bind_into](state), mid, preservesValue, t,
pragma[only_bind_into](config), cc) and
localFlowStepNodeCand1(mid, node2, config) and
not mid instanceof FlowCheckNode and
Stage2::revFlow(node2, pragma[only_bind_into](state2), pragma[only_bind_into](config))
Stage2::revFlow(node2, pragma[only_bind_into](state), pragma[only_bind_into](config))
)
or
exists(NodeEx mid, FlowState st |
localFlowStepPlus(node1, state1, mid, st, _, _, pragma[only_bind_into](config), cc) and
additionalLocalFlowStepNodeCand2(mid, st, node2, state2, config) and
exists(NodeEx mid |
localFlowStepPlus(node1, state, mid, _, _, pragma[only_bind_into](config), cc) and
additionalLocalFlowStepNodeCand2(mid, state, node2, state, config) and
not mid instanceof FlowCheckNode and
preservesValue = false and
t = node2.getDataFlowType() and
Stage2::revFlow(node2, state2, pragma[only_bind_into](config))
t = node2.getDataFlowType()
)
)
}
@@ -1819,9 +1883,19 @@ private module LocalFlowBigStep {
NodeEx node1, FlowState state1, NodeEx node2, FlowState state2, boolean preservesValue,
AccessPathFrontNil apf, Configuration config, LocalCallContext callContext
) {
localFlowStepPlus(node1, state1, node2, state2, preservesValue, apf.getType(), config,
callContext) and
localFlowExit(node2, state2, config)
localFlowStepPlus(node1, state1, node2, preservesValue, apf.getType(), config, callContext) and
localFlowExit(node2, state1, config) and
state1 = state2
or
additionalLocalFlowStepNodeCand2(node1, state1, node2, state2, config) and
state1 != state2 and
preservesValue = false and
apf = TFrontNil(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)
)
}
}
@@ -2695,10 +2769,10 @@ private module Stage4 {
bindingset[node, cc, config]
private LocalCc getLocalCc(NodeEx node, Cc cc, Configuration config) {
localFlowEntry(node, _, config) and
result =
getLocalCallContext(pragma[only_bind_into](pragma[only_bind_out](cc)),
node.getEnclosingCallable())
node.getEnclosingCallable()) and
exists(config)
}
private predicate localStep(

File diff suppressed because it is too large Load Diff

View File

@@ -1056,7 +1056,7 @@ module Private {
|
c.relevantSummary(input, output, preservesValue) and
csv =
c.getCallableCsv() + ";;" + getComponentStackCsv(input) + ";" +
c.getCallableCsv() + ";" + getComponentStackCsv(input) + ";" +
getComponentStackCsv(output) + ";" + renderKind(preservesValue)
)
}

View File

@@ -55,17 +55,10 @@ predicate summaryElement(DataFlowCallable c, string input, string output, string
/**
* Gets the summary component for specification component `c`, if any.
*
* This covers all the Ruby-specific components of a flow summary, and
* is currently restricted to `"BlockArgument"`.
* This covers all the Ruby-specific components of a flow summary.
*/
bindingset[c]
SummaryComponent interpretComponentSpecific(AccessPathToken c) {
c = "Receiver" and
result = FlowSummary::SummaryComponent::receiver()
or
c = "BlockArgument" and
result = FlowSummary::SummaryComponent::block()
or
c = "Argument[_]" and
result = FlowSummary::SummaryComponent::argument(any(ParameterPosition pos | pos.isPositional(_)))
or
@@ -83,16 +76,41 @@ SummaryComponent interpretComponentSpecific(AccessPathToken c) {
}
/** Gets the textual representation of a summary component in the format used for flow summaries. */
string getComponentSpecificCsv(SummaryComponent sc) {
sc = TArgumentSummaryComponent(any(ParameterPosition pos | pos.isBlock())) and
result = "BlockArgument"
}
string getComponentSpecificCsv(SummaryComponent sc) { none() }
/** Gets the textual representation of a parameter position in the format used for flow summaries. */
string getParameterPositionCsv(ParameterPosition pos) { result = pos.toString() }
string getParameterPositionCsv(ParameterPosition pos) {
pos.isSelf() and result = "self"
or
pos.isBlock() and result = "block"
or
exists(int i |
pos.isPositional(i) and
result = i.toString()
)
or
exists(string name |
pos.isKeyword(name) and
result = name + ":"
)
}
/** Gets the textual representation of an argument position in the format used for flow summaries. */
string getArgumentPositionCsv(ArgumentPosition pos) { result = pos.toString() }
string getArgumentPositionCsv(ArgumentPosition pos) {
pos.isSelf() and result = "self"
or
pos.isBlock() and result = "block"
or
exists(int i |
pos.isPositional(i) and
result = i.toString()
)
or
exists(string name |
pos.isKeyword(name) and
result = name + ":"
)
}
/** Holds if input specification component `c` needs a reference. */
predicate inputNeedsReferenceSpecific(string c) { none() }
@@ -176,6 +194,16 @@ module ParsePositions {
isArgBody(c) and
i = AccessPath::parseInt(c)
}
predicate isParsedKeywordParameterPosition(string c, string paramName) {
isParamBody(c) and
c = paramName + ":"
}
predicate isParsedKeywordArgumentPosition(string c, string paramName) {
isArgBody(c) and
c = paramName + ":"
}
}
/** Gets the argument position obtained by parsing `X` in `Parameter[X]`. */
@@ -184,6 +212,17 @@ ArgumentPosition parseParamBody(string s) {
ParsePositions::isParsedParameterPosition(s, i) and
result.isPositional(i)
)
or
exists(string name |
ParsePositions::isParsedKeywordParameterPosition(s, name) and
result.isKeyword(name)
)
or
s = "self" and
result.isSelf()
or
s = "block" and
result.isBlock()
}
/** Gets the parameter position obtained by parsing `X` in `Argument[X]`. */
@@ -192,4 +231,15 @@ ParameterPosition parseArgBody(string s) {
ParsePositions::isParsedArgumentPosition(s, i) and
result.isPositional(i)
)
or
exists(string name |
ParsePositions::isParsedKeywordArgumentPosition(s, name) and
result.isKeyword(name)
)
or
s = "self" and
result.isSelf()
or
s = "block" and
result.isBlock()
}

View File

@@ -287,20 +287,6 @@ private module SsaDefReaches {
)
}
/**
* Holds if the SSA definition of `v` at `def` reaches uncertain SSA definition
* `redef` in the same basic block, without crossing another SSA definition of `v`.
*/
predicate ssaDefReachesUncertainDefWithinBlock(
SourceVariable v, Definition def, UncertainWriteDefinition redef
) {
exists(BasicBlock bb, int rnk, int i |
ssaDefReachesRank(bb, def, rnk, v) and
rnk = ssaRefRank(bb, i, v, SsaDef()) - 1 and
redef.definesAt(v, bb, i)
)
}
/**
* Same as `ssaRefRank()`, but restricted to a particular SSA definition `def`.
*/

View File

@@ -64,13 +64,30 @@ abstract class Configuration extends DataFlow::Configuration {
override predicate isSource(DataFlow::Node source) { none() }
/**
* Holds if `sink` is a relevant taint sink.
* Holds if `source` is a relevant taint source with the given initial
* `state`.
*
* The smaller this predicate is, the faster `hasFlow()` will converge.
*/
// overridden to provide taint-tracking specific qldoc
override predicate isSource(DataFlow::Node source, DataFlow::FlowState state) { none() }
/**
* Holds if `sink` is a relevant taint sink
*
* The smaller this predicate is, the faster `hasFlow()` will converge.
*/
// overridden to provide taint-tracking specific qldoc
override predicate isSink(DataFlow::Node sink) { none() }
/**
* Holds if `sink` is a relevant taint sink accepting `state`.
*
* The smaller this predicate is, the faster `hasFlow()` will converge.
*/
// overridden to provide taint-tracking specific qldoc
override predicate isSink(DataFlow::Node sink, DataFlow::FlowState state) { none() }
/** Holds if the node `node` is a taint sanitizer. */
predicate isSanitizer(DataFlow::Node node) { none() }
@@ -79,9 +96,29 @@ abstract class Configuration extends DataFlow::Configuration {
defaultTaintSanitizer(node)
}
/**
* Holds if the node `node` is a taint sanitizer when the flow state is
* `state`.
*/
predicate isSanitizer(DataFlow::Node node, DataFlow::FlowState state) { none() }
final override predicate isBarrier(DataFlow::Node node, DataFlow::FlowState state) {
this.isSanitizer(node, state)
}
/** Holds if taint propagation into `node` is prohibited. */
predicate isSanitizerIn(DataFlow::Node node) { none() }
/**
* Holds if taint propagation into `node` is prohibited when the flow state is
* `state`.
*/
predicate isSanitizerIn(DataFlow::Node node, DataFlow::FlowState state) { none() }
final override predicate isBarrierIn(DataFlow::Node node, DataFlow::FlowState state) {
this.isSanitizerIn(node, state)
}
final override predicate isBarrierIn(DataFlow::Node node) { this.isSanitizerIn(node) }
/** Holds if taint propagation out of `node` is prohibited. */
@@ -89,6 +126,16 @@ abstract class Configuration extends DataFlow::Configuration {
final override predicate isBarrierOut(DataFlow::Node node) { this.isSanitizerOut(node) }
/**
* Holds if taint propagation out of `node` is prohibited when the flow state is
* `state`.
*/
predicate isSanitizerOut(DataFlow::Node node, DataFlow::FlowState state) { none() }
final override predicate isBarrierOut(DataFlow::Node node, DataFlow::FlowState state) {
this.isSanitizerOut(node, state)
}
/** Holds if taint propagation through nodes guarded by `guard` is prohibited. */
predicate isSanitizerGuard(DataFlow::BarrierGuard guard) { none() }
@@ -96,6 +143,16 @@ abstract class Configuration extends DataFlow::Configuration {
this.isSanitizerGuard(guard) or defaultTaintSanitizerGuard(guard)
}
/**
* Holds if taint propagation through nodes guarded by `guard` is prohibited
* when the flow state is `state`.
*/
predicate isSanitizerGuard(DataFlow::BarrierGuard guard, DataFlow::FlowState state) { none() }
final override predicate isBarrierGuard(DataFlow::BarrierGuard guard, DataFlow::FlowState state) {
this.isSanitizerGuard(guard, state)
}
/**
* Holds if the additional taint propagation step from `node1` to `node2`
* must be taken into account in the analysis.
@@ -107,6 +164,25 @@ abstract class Configuration extends DataFlow::Configuration {
defaultAdditionalTaintStep(node1, node2)
}
/**
* Holds if the additional taint propagation step from `node1` to `node2`
* must be taken into account in the analysis. This step is only applicable
* in `state1` and updates the flow state to `state2`.
*/
predicate isAdditionalTaintStep(
DataFlow::Node node1, DataFlow::FlowState state1, DataFlow::Node node2,
DataFlow::FlowState state2
) {
none()
}
final override predicate isAdditionalFlowStep(
DataFlow::Node node1, DataFlow::FlowState state1, DataFlow::Node node2,
DataFlow::FlowState state2
) {
this.isAdditionalTaintStep(node1, state1, node2, state2)
}
override predicate allowImplicitRead(DataFlow::Node node, DataFlow::Content c) {
(this.isSink(node) or this.isAdditionalTaintStep(node, _)) and
defaultImplicitTaintRead(node, c)

View File

@@ -0,0 +1,198 @@
/**
* Provides an implementation of global (interprocedural) taint tracking.
* This file re-exports the local (intraprocedural) taint-tracking analysis
* from `TaintTrackingParameter::Public` and adds a global analysis, mainly
* exposed through the `Configuration` class. For some languages, this file
* exists in several identical copies, allowing queries to use multiple
* `Configuration` classes that depend on each other without introducing
* mutual recursion among those configurations.
*/
import TaintTrackingParameter::Public
private import TaintTrackingParameter::Private
/**
* A configuration of interprocedural taint tracking analysis. This defines
* sources, sinks, and any other configurable aspect of the analysis. Each
* use of the taint tracking library must define its own unique extension of
* this abstract class.
*
* A taint-tracking configuration is a special data flow configuration
* (`DataFlow::Configuration`) that allows for flow through nodes that do not
* necessarily preserve values but are still relevant from a taint tracking
* perspective. (For example, string concatenation, where one of the operands
* is tainted.)
*
* To create a configuration, extend this class with a subclass whose
* characteristic predicate is a unique singleton string. For example, write
*
* ```ql
* class MyAnalysisConfiguration extends TaintTracking::Configuration {
* MyAnalysisConfiguration() { this = "MyAnalysisConfiguration" }
* // Override `isSource` and `isSink`.
* // Optionally override `isSanitizer`.
* // Optionally override `isSanitizerIn`.
* // Optionally override `isSanitizerOut`.
* // Optionally override `isSanitizerGuard`.
* // Optionally override `isAdditionalTaintStep`.
* }
* ```
*
* Then, to query whether there is flow between some `source` and `sink`,
* write
*
* ```ql
* exists(MyAnalysisConfiguration cfg | cfg.hasFlow(source, sink))
* ```
*
* Multiple configurations can coexist, but it is unsupported to depend on
* another `TaintTracking::Configuration` or a `DataFlow::Configuration` in the
* overridden predicates that define sources, sinks, or additional steps.
* Instead, the dependency should go to a `TaintTracking2::Configuration` or a
* `DataFlow2::Configuration`, `DataFlow3::Configuration`, etc.
*/
abstract class Configuration extends DataFlow::Configuration {
bindingset[this]
Configuration() { any() }
/**
* Holds if `source` is a relevant taint source.
*
* The smaller this predicate is, the faster `hasFlow()` will converge.
*/
// overridden to provide taint-tracking specific qldoc
override predicate isSource(DataFlow::Node source) { none() }
/**
* Holds if `source` is a relevant taint source with the given initial
* `state`.
*
* The smaller this predicate is, the faster `hasFlow()` will converge.
*/
// overridden to provide taint-tracking specific qldoc
override predicate isSource(DataFlow::Node source, DataFlow::FlowState state) { none() }
/**
* Holds if `sink` is a relevant taint sink
*
* The smaller this predicate is, the faster `hasFlow()` will converge.
*/
// overridden to provide taint-tracking specific qldoc
override predicate isSink(DataFlow::Node sink) { none() }
/**
* Holds if `sink` is a relevant taint sink accepting `state`.
*
* The smaller this predicate is, the faster `hasFlow()` will converge.
*/
// overridden to provide taint-tracking specific qldoc
override predicate isSink(DataFlow::Node sink, DataFlow::FlowState state) { none() }
/** Holds if the node `node` is a taint sanitizer. */
predicate isSanitizer(DataFlow::Node node) { none() }
final override predicate isBarrier(DataFlow::Node node) {
this.isSanitizer(node) or
defaultTaintSanitizer(node)
}
/**
* Holds if the node `node` is a taint sanitizer when the flow state is
* `state`.
*/
predicate isSanitizer(DataFlow::Node node, DataFlow::FlowState state) { none() }
final override predicate isBarrier(DataFlow::Node node, DataFlow::FlowState state) {
this.isSanitizer(node, state)
}
/** Holds if taint propagation into `node` is prohibited. */
predicate isSanitizerIn(DataFlow::Node node) { none() }
/**
* Holds if taint propagation into `node` is prohibited when the flow state is
* `state`.
*/
predicate isSanitizerIn(DataFlow::Node node, DataFlow::FlowState state) { none() }
final override predicate isBarrierIn(DataFlow::Node node, DataFlow::FlowState state) {
this.isSanitizerIn(node, state)
}
final override predicate isBarrierIn(DataFlow::Node node) { this.isSanitizerIn(node) }
/** Holds if taint propagation out of `node` is prohibited. */
predicate isSanitizerOut(DataFlow::Node node) { none() }
final override predicate isBarrierOut(DataFlow::Node node) { this.isSanitizerOut(node) }
/**
* Holds if taint propagation out of `node` is prohibited when the flow state is
* `state`.
*/
predicate isSanitizerOut(DataFlow::Node node, DataFlow::FlowState state) { none() }
final override predicate isBarrierOut(DataFlow::Node node, DataFlow::FlowState state) {
this.isSanitizerOut(node, state)
}
/** Holds if taint propagation through nodes guarded by `guard` is prohibited. */
predicate isSanitizerGuard(DataFlow::BarrierGuard guard) { none() }
final override predicate isBarrierGuard(DataFlow::BarrierGuard guard) {
this.isSanitizerGuard(guard) or defaultTaintSanitizerGuard(guard)
}
/**
* Holds if taint propagation through nodes guarded by `guard` is prohibited
* when the flow state is `state`.
*/
predicate isSanitizerGuard(DataFlow::BarrierGuard guard, DataFlow::FlowState state) { none() }
final override predicate isBarrierGuard(DataFlow::BarrierGuard guard, DataFlow::FlowState state) {
this.isSanitizerGuard(guard, state)
}
/**
* Holds if the additional taint propagation step from `node1` to `node2`
* must be taken into account in the analysis.
*/
predicate isAdditionalTaintStep(DataFlow::Node node1, DataFlow::Node node2) { none() }
final override predicate isAdditionalFlowStep(DataFlow::Node node1, DataFlow::Node node2) {
this.isAdditionalTaintStep(node1, node2) or
defaultAdditionalTaintStep(node1, node2)
}
/**
* Holds if the additional taint propagation step from `node1` to `node2`
* must be taken into account in the analysis. This step is only applicable
* in `state1` and updates the flow state to `state2`.
*/
predicate isAdditionalTaintStep(
DataFlow::Node node1, DataFlow::FlowState state1, DataFlow::Node node2,
DataFlow::FlowState state2
) {
none()
}
final override predicate isAdditionalFlowStep(
DataFlow::Node node1, DataFlow::FlowState state1, DataFlow::Node node2,
DataFlow::FlowState state2
) {
this.isAdditionalTaintStep(node1, state1, node2, state2)
}
override predicate allowImplicitRead(DataFlow::Node node, DataFlow::Content c) {
(this.isSink(node) or this.isAdditionalTaintStep(node, _)) and
defaultImplicitTaintRead(node, c)
}
/**
* Holds if taint may flow from `source` to `sink` for this configuration.
*/
// overridden to provide taint-tracking specific qldoc
override predicate hasFlow(DataFlow::Node source, DataFlow::Node sink) {
super.hasFlow(source, sink)
}
}

View File

@@ -0,0 +1,6 @@
import codeql.ruby.dataflow.internal.TaintTrackingPublic as Public
module Private {
import codeql.ruby.dataflow.internal.DataFlowImplForLibraries as DataFlow
import codeql.ruby.dataflow.internal.TaintTrackingPrivate
}

View File

@@ -11,6 +11,7 @@ private import codeql.ruby.ast.internal.Module
private import codeql.ruby.ApiGraphs
private import ActionView
private import codeql.ruby.frameworks.ActionDispatch
private import codeql.ruby.Concepts
/**
* A `ClassDeclaration` for a class that extends `ActionController::Base`.
@@ -126,7 +127,7 @@ abstract class ParamsCall extends MethodCall {
* A `RemoteFlowSource::Range` to represent accessing the
* ActionController parameters available via the `params` method.
*/
class ParamsSource extends RemoteFlowSource::Range {
class ParamsSource extends HTTP::Server::RequestInputAccess::Range {
ParamsSource() { this.asExpr().getExpr() instanceof ParamsCall }
override string getSourceType() { result = "ActionController::Metal#params" }
@@ -143,7 +144,7 @@ abstract class CookiesCall extends MethodCall {
* A `RemoteFlowSource::Range` to represent accessing the
* ActionController parameters available via the `cookies` method.
*/
class CookiesSource extends RemoteFlowSource::Range {
class CookiesSource extends HTTP::Server::RequestInputAccess::Range {
CookiesSource() { this.asExpr().getExpr() instanceof CookiesCall }
override string getSourceType() { result = "ActionController::Metal#cookies" }
@@ -188,7 +189,7 @@ class RedirectToCall extends ActionControllerContextCall {
/** Gets the `ActionControllerActionMethod` to redirect to, if any */
ActionControllerActionMethod getRedirectActionMethod() {
exists(string methodName |
this.getKeywordArgument("action").getConstantValue().isStringOrSymbol(methodName) and
this.getKeywordArgument("action").getConstantValue().isStringlikeValue(methodName) and
methodName = result.getName() and
result.getEnclosingModule() = this.getControllerClass()
)
@@ -224,7 +225,7 @@ pragma[nomagic]
private predicate actionControllerHasHelperMethodCall(ActionControllerControllerClass c, string name) {
exists(MethodCall mc |
mc.getMethodName() = "helper_method" and
mc.getAnArgument().getConstantValue().isStringOrSymbol(name) and
mc.getAnArgument().getConstantValue().isStringlikeValue(name) and
mc.getEnclosingModule() = c
)
}
@@ -316,7 +317,7 @@ class ActionControllerSkipForgeryProtectionCall extends CSRFProtectionSetting::R
call.getMethodName() = "skip_forgery_protection"
or
call.getMethodName() = "skip_before_action" and
call.getAnArgument().getConstantValue().isStringOrSymbol("verify_authenticity_token")
call.getAnArgument().getConstantValue().isStringlikeValue("verify_authenticity_token")
)
}

View File

@@ -165,14 +165,14 @@ module ActionDispatch {
override Location getLocation() { result = call.getLocation() }
override string getPathComponent() {
call.getKeywordArgument("path").getConstantValue().isStringOrSymbol(result)
call.getKeywordArgument("path").getConstantValue().isStringlikeValue(result)
or
not exists(call.getKeywordArgument("path")) and
call.getArgument(0).getConstantValue().isStringOrSymbol(result)
call.getArgument(0).getConstantValue().isStringlikeValue(result)
}
override string getControllerComponent() {
call.getKeywordArgument(["controller", "module"]).getConstantValue().isStringOrSymbol(result)
call.getKeywordArgument(["controller", "module"]).getConstantValue().isStringlikeValue(result)
}
}
@@ -201,7 +201,7 @@ module ActionDispatch {
MethodCall getDefiningMethodCall() { result = call }
override string getPathComponent() {
exists(string resource | call.getArgument(0).getConstantValue().isStringOrSymbol(resource) |
exists(string resource | call.getArgument(0).getConstantValue().isStringlikeValue(resource) |
result = resource + "/:" + singularize(resource) + "_id"
)
}
@@ -264,7 +264,7 @@ module ActionDispatch {
override string getControllerComponent() { result = this.getNamespace() }
private string getNamespace() {
call.getArgument(0).getConstantValue().isStringOrSymbol(result)
call.getArgument(0).getConstantValue().isStringlikeValue(result)
}
override string toString() { result = call.toString() }
@@ -482,11 +482,11 @@ module ActionDispatch {
override RouteBlock getParentBlock() { result = parentBlock }
override string getLastPathComponent() {
method.getArgument(0).getConstantValue().isStringOrSymbol(result)
method.getArgument(0).getConstantValue().isStringlikeValue(result)
}
override string getLastControllerComponent() {
method.getKeywordArgument("controller").getConstantValue().isStringOrSymbol(result)
method.getKeywordArgument("controller").getConstantValue().isStringlikeValue(result)
or
not exists(method.getKeywordArgument("controller")) and
(
@@ -510,7 +510,7 @@ module ActionDispatch {
}
private string getActionString() {
method.getKeywordArgument("to").getConstantValue().isStringOrSymbol(result)
method.getKeywordArgument("to").getConstantValue().isStringlikeValue(result)
or
method.getKeywordArgument("to").(MethodCall).getMethodName() = "redirect" and
result = "<redirect>#<redirect>"
@@ -518,7 +518,7 @@ module ActionDispatch {
override string getAction() {
// get "/photos", action: "index"
method.getKeywordArgument("action").getConstantValue().isStringOrSymbol(result)
method.getKeywordArgument("action").getConstantValue().isStringlikeValue(result)
or
not exists(method.getKeywordArgument("action")) and
(
@@ -533,7 +533,7 @@ module ActionDispatch {
or
// get :some_action
not exists(this.getActionString()) and
method.getArgument(0).getConstantValue().isStringOrSymbol(result)
method.getArgument(0).getConstantValue().isStringlikeValue(result)
)
}
@@ -580,7 +580,7 @@ module ActionDispatch {
ResourcesRoute() {
exists(string resource |
this = TResourcesRoute(parent, method, action) and
method.getArgument(0).getConstantValue().isStringOrSymbol(resource) and
method.getArgument(0).getConstantValue().isStringlikeValue(resource) and
isDefaultResourceRoute(resource, httpMethod, pathComponent, action)
)
}
@@ -592,7 +592,7 @@ module ActionDispatch {
override string getLastPathComponent() { result = pathComponent }
override string getLastControllerComponent() {
method.getArgument(0).getConstantValue().isStringOrSymbol(result)
method.getArgument(0).getConstantValue().isStringlikeValue(result)
}
override string getAction() { result = action }
@@ -618,7 +618,7 @@ module ActionDispatch {
SingularResourceRoute() {
exists(string resource |
this = TResourceRoute(parent, method, action) and
method.getArgument(0).getConstantValue().isStringOrSymbol(resource) and
method.getArgument(0).getConstantValue().isStringlikeValue(resource) and
isDefaultSingularResourceRoute(resource, httpMethod, pathComponent, action)
)
}
@@ -630,7 +630,7 @@ module ActionDispatch {
override string getLastPathComponent() { result = pathComponent }
override string getLastControllerComponent() {
method.getArgument(0).getConstantValue().isStringOrSymbol(result)
method.getArgument(0).getConstantValue().isStringlikeValue(result)
}
override string getAction() { result = action }
@@ -663,25 +663,25 @@ module ActionDispatch {
override string getLastPathComponent() {
[method.getArgument(0), method.getArgument(0).(Pair).getKey()]
.getConstantValue()
.isStringOrSymbol(result)
.isStringlikeValue(result)
}
override string getLastControllerComponent() {
result =
extractController(method.getKeywordArgument("to").getConstantValue().getStringOrSymbol()) or
method.getKeywordArgument("controller").getConstantValue().isStringOrSymbol(result) or
extractController(method.getKeywordArgument("to").getConstantValue().getStringlikeValue()) or
method.getKeywordArgument("controller").getConstantValue().isStringlikeValue(result) or
result =
extractController(method
.getArgument(0)
.(Pair)
.getValue()
.getConstantValue()
.getStringOrSymbol())
.getStringlikeValue())
}
override string getHttpMethod() {
exists(string via |
method.getKeywordArgument("via").getConstantValue().isStringOrSymbol(via)
method.getKeywordArgument("via").getConstantValue().isStringlikeValue(via)
|
via = "all" and result = anyHttpMethod()
or
@@ -694,14 +694,20 @@ module ActionDispatch {
.(ArrayLiteral)
.getElement(_)
.getConstantValue()
.getStringOrSymbol()
.getStringlikeValue()
}
override string getAction() {
result = extractAction(method.getKeywordArgument("to").getConstantValue().getStringOrSymbol()) or
method.getKeywordArgument("action").getConstantValue().isStringOrSymbol(result) or
result =
extractAction(method.getArgument(0).(Pair).getValue().getConstantValue().getStringOrSymbol())
extractAction(method.getKeywordArgument("to").getConstantValue().getStringlikeValue()) or
method.getKeywordArgument("action").getConstantValue().isStringlikeValue(result) or
result =
extractAction(method
.getArgument(0)
.(Pair)
.getValue()
.getConstantValue()
.getStringlikeValue())
}
}
@@ -802,7 +808,7 @@ module ActionDispatch {
not exists(m.getKeywordArgument("only"))
or
exists(Expr only | only = m.getKeywordArgument("only") |
[only.(ArrayLiteral).getElement(_), only].getConstantValue().isStringOrSymbol(action)
[only.(ArrayLiteral).getElement(_), only].getConstantValue().isStringlikeValue(action)
)
) and
// Respect the `except` keyword argument, which removes actions from the default set.
@@ -810,7 +816,7 @@ module ActionDispatch {
not exists(m.getKeywordArgument("except"))
or
exists(Expr except | except = m.getKeywordArgument("except") |
[except.(ArrayLiteral).getElement(_), except].getConstantValue().getStringOrSymbol() !=
[except.(ArrayLiteral).getElement(_), except].getConstantValue().getStringlikeValue() !=
action
)
)

View File

@@ -95,7 +95,7 @@ abstract class RenderCall extends MethodCall {
}
private string getTemplatePathValue() {
result = this.getTemplatePathArgument().getConstantValue().getStringOrSymbol()
result = this.getTemplatePathArgument().getConstantValue().getStringlikeValue()
}
// everything up to and including the final slash, but ignoring any leading slash

View File

@@ -7,6 +7,7 @@ private import codeql.ruby.ApiGraphs
private import codeql.ruby.Concepts
private import codeql.ruby.DataFlow
private import codeql.ruby.dataflow.FlowSummary
private import codeql.ruby.frameworks.data.ModelsAsData
/** A call to `ActiveStorage::Filename#sanitized`, considered as a path sanitizer. */
class ActiveStorageFilenameSanitizedCall extends Path::PathSanitization::Range, DataFlow::CallNode {
@@ -17,43 +18,13 @@ class ActiveStorageFilenameSanitizedCall extends Path::PathSanitization::Range,
}
}
/** The taint summary for `ActiveStorage::Filename.new`. */
class ActiveStorageFilenameNewSummary extends SummarizedCallable {
ActiveStorageFilenameNewSummary() { this = "ActiveStorage::Filename.new" }
override MethodCall getACall() {
result =
API::getTopLevelMember("ActiveStorage")
.getMember("Filename")
.getAnInstantiation()
.asExpr()
.getExpr()
}
override predicate propagatesFlowExt(string input, string output, boolean preservesValue) {
input = "Argument[0]" and
output = "ReturnValue" and
preservesValue = false
}
}
/** The taint summary for `ActiveStorage::Filename#sanitized`. */
class ActiveStorageFilenameSanitizedSummary extends SummarizedCallable {
ActiveStorageFilenameSanitizedSummary() { this = "ActiveStorage::Filename#sanitized" }
override MethodCall getACall() {
result =
API::getTopLevelMember("ActiveStorage")
.getMember("Filename")
.getInstance()
.getAMethodCall("sanitized")
.asExpr()
.getExpr()
}
override predicate propagatesFlowExt(string input, string output, boolean preservesValue) {
input = "Argument[-1]" and
output = "ReturnValue" and
preservesValue = false
/** Taint related to `ActiveStorage::Filename`. */
private class Summaries extends ModelInput::SummaryModelCsv {
override predicate row(string row) {
row =
[
"activestorage;;Member[ActiveStorage].Member[Filename].Method[new];Argument[0];ReturnValue;taint",
"activestorage;;Member[ActiveStorage].Member[Filename].Instance.Method[sanitized];Argument[self];ReturnValue;taint",
]
}
}

View File

@@ -61,11 +61,11 @@ private class SplatSummary extends SummarizedCallable {
override predicate propagatesFlowExt(string input, string output, boolean preservesValue) {
(
// *1 = [1]
input = "Receiver" and
input = "Argument[self]" and
output = "ReturnValue.ArrayElement[0]"
or
// *[1] = [1]
input = "Receiver" and
input = "Argument[self]" and
output = "ReturnValue"
) and
preservesValue = true

View File

@@ -28,7 +28,7 @@ private DataFlow::Node ioInstance() {
// will execute a shell command and read its output rather than reading from the
// filesystem.
private predicate pathArgSpawnsSubprocess(Expr arg) {
arg.getConstantValue().getStringOrSymbol().charAt(0) = "|"
arg.getConstantValue().getStringlikeValue().charAt(0) = "|"
}
private DataFlow::Node fileInstanceInstantiation() {
@@ -69,7 +69,10 @@ abstract private class IOOrFileMethodCall extends DataFlow::CallNode {
}
/** Gets the API used to perform this call, either "IO" or "File" */
abstract string getAPI();
abstract string getApi();
/** DEPRECATED: Alias for getApi */
deprecated string getAPI() { result = this.getApi() }
/** Gets a node representing the data read or written by this call */
abstract DataFlow::Node getADataNodeImpl();
@@ -110,7 +113,10 @@ private class IOOrFileReadMethodCall extends IOOrFileMethodCall {
)
}
override string getAPI() { result = api }
override string getApi() { result = api }
/** DEPRECATED: Alias for getApi */
deprecated override string getAPI() { result = this.getApi() }
override DataFlow::Node getADataNodeImpl() { result = this }
@@ -151,7 +157,10 @@ private class IOOrFileWriteMethodCall extends IOOrFileMethodCall {
)
}
override string getAPI() { result = api }
override string getApi() { result = api }
/** DEPRECATED: Alias for getApi */
deprecated override string getAPI() { result = this.getApi() }
override DataFlow::Node getADataNodeImpl() { result = dataNode }
@@ -180,12 +189,6 @@ module IO {
}
}
// "Direct" `IO` instances, i.e. cases where there is no more specific
// subtype such as `File`
private class IOInstanceStrict extends IOInstance {
IOInstanceStrict() { this = ioInstance() }
}
/**
* A `DataFlow::CallNode` that reads data using the `IO` class. For example,
* the `read` and `readline` calls in:
@@ -202,7 +205,7 @@ module IO {
* that use a subclass of `IO` such as `File`.
*/
class IOReader extends IOOrFileReadMethodCall {
IOReader() { this.getAPI() = "IO" }
IOReader() { this.getApi() = "IO" }
}
/**
@@ -221,7 +224,7 @@ module IO {
* that use a subclass of `IO` such as `File`.
*/
class IOWriter extends IOOrFileWriteMethodCall {
IOWriter() { this.getAPI() = "IO" }
IOWriter() { this.getApi() = "IO" }
}
/**
@@ -306,7 +309,7 @@ module File {
* ```
*/
class FileModuleReader extends IO::FileReader {
FileModuleReader() { this.getAPI() = "File" }
FileModuleReader() { this.getApi() = "File" }
override DataFlow::Node getADataNode() { result = this.getADataNodeImpl() }

View File

@@ -248,7 +248,7 @@ class GraphqlFieldDefinitionMethodCall extends GraphqlSchemaObjectClassMethodCal
GraphqlFieldDefinitionMethodCall() { this.getMethodName() = "field" }
/** Gets the name of this GraphQL field. */
string getFieldName() { result = this.getArgument(0).getConstantValue().getStringOrSymbol() }
string getFieldName() { result = this.getArgument(0).getConstantValue().getStringlikeValue() }
}
/**
@@ -284,7 +284,7 @@ private class GraphqlFieldArgumentDefinitionMethodCall extends GraphqlSchemaObje
string getFieldName() { result = this.getFieldDefinition().getFieldName() }
/** Gets the name of the argument (i.e. the first argument to this `argument` method call) */
string getArgumentName() { result = this.getArgument(0).getConstantValue().getStringOrSymbol() }
string getArgumentName() { result = this.getArgument(0).getConstantValue().getStringlikeValue() }
}
/**
@@ -333,7 +333,9 @@ class GraphqlFieldResolutionMethod extends Method, HTTP::Server::RequestHandler:
exists(GraphqlFieldDefinitionMethodCall defn |
// field :foo, resolver_method: :custom_method
// def custom_method(...)
defn.getKeywordArgument("resolver_method").getConstantValue().isStringOrSymbol(this.getName())
defn.getKeywordArgument("resolver_method")
.getConstantValue()
.isStringlikeValue(this.getName())
or
// field :foo
// def foo(...)
@@ -344,7 +346,10 @@ class GraphqlFieldResolutionMethod extends Method, HTTP::Server::RequestHandler:
/** Gets the method call which is the definition of the field corresponding to this resolver method. */
GraphqlFieldDefinitionMethodCall getDefinition() {
result.getKeywordArgument("resolver_method").getConstantValue().isStringOrSymbol(this.getName())
result
.getKeywordArgument("resolver_method")
.getConstantValue()
.isStringlikeValue(this.getName())
or
not exists(result.getKeywordArgument("resolver_method").(SymbolLiteral)) and
result.getFieldName() = this.getName()

View File

@@ -164,7 +164,7 @@ private module Settings {
* A node that sets a Stringlike value.
*/
class StringlikeSetting extends LiteralSetting {
override ConstantValue::ConstantStringValue value;
override ConstantValue::ConstantStringlikeValue value;
}
/**
@@ -172,12 +172,11 @@ private module Settings {
*/
class NillableStringlikeSetting extends LiteralSetting {
NillableStringlikeSetting() {
value instanceof ConstantValue::ConstantStringValue or
value instanceof ConstantValue::ConstantSymbolValue or
value instanceof ConstantValue::ConstantStringlikeValue or
value instanceof ConstantValue::ConstantNilValue
}
string getStringValue() { result = value.getStringOrSymbol() }
string getStringValue() { result = value.getStringlikeValue() }
predicate isNilValue() { value.isNil() }
}

View File

@@ -59,7 +59,7 @@ private class LibXmlRubyXmlParserCall extends XmlParserCall::Range, DataFlow::Ca
exists(CfgNodes::ExprNodes::PairCfgNode pair |
pair =
this.getArgument(1).asExpr().(CfgNodes::ExprNodes::HashLiteralCfgNode).getAKeyValuePair() and
pair.getKey().getConstantValue().isStringOrSymbol("options") and
pair.getKey().getConstantValue().isStringlikeValue("options") and
pair.getValue() =
[
trackEnableFeature(TNOENT()), trackEnableFeature(TDTDLOAD()),

File diff suppressed because it is too large Load Diff

View File

@@ -4,24 +4,16 @@
private import codeql.ruby.ApiGraphs
private import codeql.ruby.dataflow.FlowSummary
private import codeql.ruby.frameworks.data.ModelsAsData
/**
* Provides modeling for the `Regexp` class.
*/
module Regexp {
/** A flow summary for `Regexp.escape` and its alias, `Regexp.quote`. */
class RegexpEscapeSummary extends SummarizedCallable {
RegexpEscapeSummary() { this = "Regexp.escape" }
override MethodCall getACall() {
result =
API::getTopLevelMember("Regexp").getAMethodCall(["escape", "quote"]).asExpr().getExpr()
}
override predicate propagatesFlowExt(string input, string output, boolean preservesValue) {
input = "Argument[0]" and
output = "ReturnValue" and
preservesValue = false
class RegexpEscapeSummary extends ModelInput::SummaryModelCsv {
override predicate row(string row) {
row = ";;Member[Regexp].Method[escape,quote];Argument[0];ReturnValue;taint"
}
}
}

View File

@@ -17,7 +17,7 @@ module String {
* Taint-preserving (but not value-preserving) flow from the receiver to the return value.
*/
private predicate taintIdentityFlow(string input, string output, boolean preservesValue) {
input = "Receiver" and
input = "Argument[self]" and
output = "ReturnValue" and
preservesValue = false
}
@@ -58,7 +58,7 @@ module String {
FormatSummary() { this = "%" }
override predicate propagatesFlowExt(string input, string output, boolean preservesValue) {
input = ["Receiver", "Argument[0]", "Argument[0].ArrayElement"] and
input = ["Argument[self]", "Argument[0]", "Argument[0].ArrayElement"] and
output = "ReturnValue" and
preservesValue = false
}
@@ -94,7 +94,7 @@ module String {
CapitalizeSummary() { this = ["capitalize", "capitalize!"] }
override predicate propagatesFlowExt(string input, string output, boolean preservesValue) {
input = "Receiver" and
input = "Argument[self]" and
preservesValue = false and
output = "ReturnValue"
}
@@ -125,9 +125,9 @@ module String {
taintIdentityFlow(input, output, preservesValue)
or
this = ["chomp!", "chop!"] and
input = "Receiver" and
input = "Argument[self]" and
preservesValue = false and
output = "Receiver"
output = "Argument[self]"
}
}
@@ -152,8 +152,8 @@ module String {
}
override predicate propagatesFlowExt(string input, string output, boolean preservesValue) {
input = ["Receiver", "Argument[_]"] and
output = ["ReturnValue", "Receiver"] and
input = ["Argument[self]", "Argument[_]"] and
output = ["ReturnValue", "Argument[self]"] and
preservesValue = false
}
}
@@ -212,8 +212,8 @@ module String {
override predicate propagatesFlowExt(string input, string output, boolean preservesValue) {
preservesValue = false and
input = "Receiver" and
output = ["BlockArgument.Parameter[0]", "ReturnValue"]
input = "Argument[self]" and
output = ["Argument[block].Parameter[0]", "ReturnValue"]
}
}
@@ -225,7 +225,7 @@ module String {
override predicate propagatesFlowExt(string input, string output, boolean preservesValue) {
preservesValue = false and
input = "Receiver" and
input = "Argument[self]" and
output = "ReturnValue.ArrayElement[?]"
}
}
@@ -278,7 +278,7 @@ module String {
// block return -> return value
preservesValue = false and
output = "ReturnValue" and
input = ["Receiver", "Argument[1]", "BlockArgument.ReturnValue"]
input = ["Argument[self]", "Argument[1]", "Argument[block].ReturnValue"]
}
}
@@ -339,7 +339,7 @@ module String {
PartitionSummary() { this = ["partition", "rpartition"] }
override predicate propagatesFlowExt(string input, string output, boolean preservesValue) {
input = "Receiver" and
input = "Argument[self]" and
output = "ReturnValue.ArrayElement[" + [0, 1, 2] + "]" and
preservesValue = false
}
@@ -353,10 +353,10 @@ module String {
override predicate propagatesFlowExt(string input, string output, boolean preservesValue) {
input = "Argument[0]" and
output = ["ReturnValue", "Receiver"] and
output = ["ReturnValue", "Argument[self]"] and
preservesValue = false
}
// TODO: we should also clear any existing content in Receiver
// TODO: we should also clear any existing content in Argument[self]
}
/**
@@ -386,7 +386,7 @@ module String {
ScanBlockSummary() { this = "scan_with_block" and exists(mc.getBlock()) }
override predicate propagatesFlowExt(string input, string output, boolean preservesValue) {
input = "Receiver" and
input = "Argument[self]" and
preservesValue = false and
output =
[
@@ -394,7 +394,7 @@ module String {
"ReturnValue",
// scan(pattern) {|match, ...| block } -> str
// Parameter[_] doesn't seem to work
"BlockArgument.Parameter[" + [0 .. 10] + "]"
"Argument[block].Parameter[" + [0 .. 10] + "]"
]
}
}
@@ -404,7 +404,7 @@ module String {
override predicate propagatesFlowExt(string input, string output, boolean preservesValue) {
// scan(pattern) -> array
input = "Receiver" and
input = "Argument[self]" and
output = "ReturnValue.ArrayElement[?]" and
preservesValue = false
}
@@ -430,12 +430,12 @@ module String {
or
preservesValue = false and
(
input = "Receiver" and
output = "BlockArgument.Parameter[0]"
input = "Argument[self]" and
output = "Argument[block].Parameter[0]"
or
input = "Argument[0]" and output = "ReturnValue"
or
input = "BlockArgument.ReturnValue" and
input = "Argument[block].ReturnValue" and
output = "ReturnValue"
)
}
@@ -471,7 +471,7 @@ module String {
ShellSplitSummary() { this = "shellsplit" }
override predicate propagatesFlowExt(string input, string output, boolean preservesValue) {
input = "Receiver" and
input = "Argument[self]" and
output = "ReturnValue.ArrayElement[?]" and
preservesValue = false
}
@@ -551,11 +551,11 @@ module String {
override predicate propagatesFlowExt(string input, string output, boolean preservesValue) {
taintIdentityFlow(input, output, preservesValue)
or
input = ["Receiver", "Argument[0]"] and
output = "BlockArgument.Parameter[0]" and
input = ["Argument[self]", "Argument[0]"] and
output = "Argument[block].Parameter[0]" and
preservesValue = false
or
input = "BlockArgument.ReturnValue" and
input = "Argument[block].ReturnValue" and
output = "ReturnValue.ArrayElement[?]" and
preservesValue = false
}
@@ -571,11 +571,11 @@ module String {
}
override predicate propagatesFlowExt(string input, string output, boolean preservesValue) {
input = "Receiver" and
output = "BlockArgument.Parameter[0]" and
input = "Argument[self]" and
output = "Argument[block].Parameter[0]" and
preservesValue = false
or
input = "BlockArgument.ReturnValue" and
input = "Argument[block].ReturnValue" and
output = "ReturnValue.ArrayElement[?]" and
preservesValue = false
}

View File

@@ -0,0 +1,31 @@
/**
* Provides classes for contributing a model, or using the interpreted results
* of a model represented as data.
*
* - Use the `ModelInput` module to contribute new models.
* - Use the `ModelOutput` module to access the model results in terms of API nodes.
*
* The `package` part of a CSV row should be the name of a Ruby gem, or the empty
* string if it's referring to the standard library.
*
* The `type` part can be one of the following:
* - the empty string, referring to the global scope,
* - the string `any`, referring to any expression, or
* - the name of a type definition from `ModelInput::TypeModelCsv`
*/
private import ruby
private import internal.ApiGraphModels as Shared
private import internal.ApiGraphModelsSpecific as Specific
import Shared::ModelInput as ModelInput
import Shared::ModelOutput as ModelOutput
private import codeql.ruby.dataflow.RemoteFlowSources
/**
* A remote flow source originating from a CSV source row.
*/
private class RemoteFlowSourceFromCsv extends RemoteFlowSource::Range {
RemoteFlowSourceFromCsv() { this = ModelOutput::getASourceNode("remote").getAnImmediateUse() }
override string getSourceType() { result = "Remote flow (from model)" }
}

View File

@@ -0,0 +1,522 @@
/**
* INTERNAL use only. This is an experimental API subject to change without notice.
*
* Provides classes and predicates for dealing with flow models specified in CSV format.
*
* The CSV specification has the following columns:
* - Sources:
* `package; type; path; kind`
* - Sinks:
* `package; type; path; kind`
* - Summaries:
* `package; type; path; input; output; kind`
* - Types:
* `package1; type1; package2; type2; path`
*
* The interpretation of a row is similar to API-graphs with a left-to-right
* reading.
* 1. The `package` column selects a package name, as it would be referenced in the source code,
* such as an NPM package, PIP package, or Ruby gem. (See `ModelsAsData.qll` for language-specific details).
* It may also be a synthetic package used for a type definition (see type definitions below).
* 2. The `type` column selects all instances of a named type originating from that package,
* or the empty string if referring to the package itself.
* It can also be a synthetic type name defined by a type definition (see type definitions below).
* 3. The `path` column is a `.`-separated list of "access path tokens" to resolve, starting at the node selected by `package` and `type`.
*
* Every language supports the following tokens:
* - Argument[n]: the n-th argument to a call. May be a range of form `x..y` (inclusive) and/or a comma-separated list.
* Additionally, `N-1` refers to the last argument, `N-2` refers to the second-last, and so on.
* - Parameter[n]: the n-th parameter of a callback. May be a range of form `x..y` (inclusive) and/or a comma-separated list.
* - ReturnValue: the value returned by a function call
* - WithArity[n]: match a call with the given arity. May be a range of form `x..y` (inclusive) and/or a comma-separated list.
*
* The following tokens are common and should be implemented for languages where it makes sense:
* - Member[x]: a member named `x`; exactly what a "member" is depends on the language. May be a comma-separated list of names.
* - Instance: an instance of a class
* - Subclass: a subclass of a class
* - ArrayElement: an element of array
* - Element: an element of a collection-like object
* - MapKey: a key in map-like object
* - MapValue: a value in a map-like object
* - Awaited: the value from a resolved promise/future-like object
*
* For the time being, please consult `ApiGraphModelsSpecific.qll` to see which language-specific tokens are currently supported.
*
* 4. The `input` and `output` columns specify how data enters and leaves the element selected by the
* first `(package, type, path)` tuple. Both strings are `.`-separated access paths
* of the same syntax as the `path` column.
* 5. The `kind` column is a tag that can be referenced from QL to determine to
* which classes the interpreted elements should be added. For example, for
* sources `"remote"` indicates a default remote flow source, and for summaries
* `"taint"` indicates a default additional taint step and `"value"` indicates a
* globally applicable value-preserving step.
*
* ### Types
*
* A type row of form `package1; type1; package2; type2; path` indicates that `package2; type2; path`
* should be seen as an instance of the type `package1; type1`.
*
* A `(package,type)` pair may refer to a static type or a synthetic type name used internally in the model.
* Synthetic type names can be used to reuse intermediate sub-paths, when there are multiple ways to access the same
* element.
* See `ModelsAsData.qll` for the langauge-specific interpretation of packages and static type names.
*
* By convention, if one wants to avoid clashes with static types from the package, the type name
* should be prefixed with a tilde character (`~`). For example, `(foo, ~Bar)` can be used to indicate that
* the type is related to the `foo` package but is not intended to match a static type.
*/
private import ApiGraphModelsSpecific as Specific
private class Unit = Specific::Unit;
private module API = Specific::API;
private import Specific::AccessPathSyntax
/** Module containing hooks for providing input data to be interpreted as a model. */
module ModelInput {
/**
* A unit class for adding additional source model rows.
*
* Extend this class to add additional source definitions.
*/
class SourceModelCsv extends Unit {
/**
* Holds if `row` specifies a source definition.
*
* A row of form
* ```
* package;type;path;kind
* ```
* indicates that the value at `(package, type, path)` should be seen as a flow
* source of the given `kind`.
*
* The kind `remote` represents a general remote flow source.
*/
abstract predicate row(string row);
}
/**
* A unit class for adding additional sink model rows.
*
* Extend this class to add additional sink definitions.
*/
class SinkModelCsv extends Unit {
/**
* Holds if `row` specifies a sink definition.
*
* A row of form
* ```
* package;type;path;kind
* ```
* indicates that the value at `(package, type, path)` should be seen as a sink
* of the given `kind`.
*/
abstract predicate row(string row);
}
/**
* A unit class for adding additional summary model rows.
*
* Extend this class to add additional flow summary definitions.
*/
class SummaryModelCsv extends Unit {
/**
* Holds if `row` specifies a summary definition.
*
* A row of form
* ```
* package;type;path;input;output;kind
* ```
* indicates that for each call to `(package, type, path)`, the value referred to by `input`
* can flow to the value referred to by `output`.
*
* `kind` should be either `value` or `taint`, for value-preserving or taint-preserving steps,
* respectively.
*/
abstract predicate row(string row);
}
/**
* A unit class for adding additional type model rows.
*
* Extend this class to add additional type definitions.
*/
class TypeModelCsv extends Unit {
/**
* Holds if `row` specifies a type definition.
*
* A row of form,
* ```
* package1;type1;package2;type2;path
* ```
* indicates that `(package2, type2, path)` should be seen as an instance of `(package1, type1)`.
*/
abstract predicate row(string row);
}
}
private import ModelInput
/**
* An empty class, except in specific tests.
*
* If this is non-empty, all models are parsed even if the package is not
* considered relevant for the current database.
*/
abstract class TestAllModels extends Unit { }
/**
* Append `;dummy` to the value of `s` to work around the fact that `string.split(delim,n)`
* does not preserve empty trailing substrings.
*/
bindingset[result]
private string inversePad(string s) { s = result + ";dummy" }
private predicate sourceModel(string row) { any(SourceModelCsv s).row(inversePad(row)) }
private predicate sinkModel(string row) { any(SinkModelCsv s).row(inversePad(row)) }
private predicate summaryModel(string row) { any(SummaryModelCsv s).row(inversePad(row)) }
private predicate typeModel(string row) { any(TypeModelCsv s).row(inversePad(row)) }
/** Holds if a source model exists for the given parameters. */
predicate sourceModel(string package, string type, string path, string kind) {
exists(string row |
sourceModel(row) and
row.splitAt(";", 0) = package and
row.splitAt(";", 1) = type and
row.splitAt(";", 2) = path and
row.splitAt(";", 3) = kind
)
}
/** Holds if a sink model exists for the given parameters. */
private predicate sinkModel(string package, string type, string path, string kind) {
exists(string row |
sinkModel(row) and
row.splitAt(";", 0) = package and
row.splitAt(";", 1) = type and
row.splitAt(";", 2) = path and
row.splitAt(";", 3) = kind
)
}
/** Holds if a summary model `row` exists for the given parameters. */
private predicate summaryModel(
string package, string type, string path, string input, string output, string kind
) {
exists(string row |
summaryModel(row) and
row.splitAt(";", 0) = package and
row.splitAt(";", 1) = type and
row.splitAt(";", 2) = path and
row.splitAt(";", 3) = input and
row.splitAt(";", 4) = output and
row.splitAt(";", 5) = kind
)
}
/** Holds if an type model exists for the given parameters. */
private predicate typeModel(
string package1, string type1, string package2, string type2, string path
) {
exists(string row |
typeModel(row) and
row.splitAt(";", 0) = package1 and
row.splitAt(";", 1) = type1 and
row.splitAt(";", 2) = package2 and
row.splitAt(";", 3) = type2 and
row.splitAt(";", 4) = path
)
}
/**
* Gets a package that should be seen as an alias for the given other `package`,
* or the `package` itself.
*/
bindingset[package]
bindingset[result]
string getAPackageAlias(string package) {
typeModel(package, "", result, "", "")
or
result = package
}
/**
* Holds if CSV rows involving `package` might be relevant for the analysis of this database.
*/
private predicate isRelevantPackage(string package) {
(
sourceModel(package, _, _, _) or
sinkModel(package, _, _, _) or
summaryModel(package, _, _, _, _, _) or
typeModel(package, _, _, _, _)
) and
(
Specific::isPackageUsed(package)
or
exists(TestAllModels t)
)
or
exists(string other |
isRelevantPackage(other) and
typeModel(package, _, other, _, _)
)
}
/**
* Holds if `package,type,path` is used in some CSV row.
*/
pragma[nomagic]
predicate isRelevantFullPath(string package, string type, string path) {
isRelevantPackage(package) and
(
sourceModel(package, type, path, _) or
sinkModel(package, type, path, _) or
summaryModel(package, type, path, _, _, _) or
typeModel(_, _, package, type, path)
)
}
/** A string from a CSV row that should be parsed as an access path. */
private class AccessPathRange extends AccessPath::Range {
AccessPathRange() {
isRelevantFullPath(_, _, this)
or
exists(string package | isRelevantPackage(package) |
summaryModel(package, _, _, this, _, _) or
summaryModel(package, _, _, _, this, _)
)
}
}
/**
* Gets a successor of `node` in the API graph.
*/
bindingset[token]
API::Node getSuccessorFromNode(API::Node node, AccessPathToken token) {
// API graphs use the same label for arguments and parameters. An edge originating from a
// use-node represents be an argument, and an edge originating from a def-node represents a parameter.
// We just map both to the same thing.
token.getName() = ["Argument", "Parameter"] and
result = node.getParameter(AccessPath::parseIntUnbounded(token.getAnArgument()))
or
token.getName() = "ReturnValue" and
result = node.getReturn()
or
// Language-specific tokens
result = Specific::getExtraSuccessorFromNode(node, token)
}
/**
* Gets an API-graph successor for the given invocation.
*/
bindingset[token]
API::Node getSuccessorFromInvoke(Specific::InvokeNode invoke, AccessPathToken token) {
token.getName() = "Argument" and
result =
invoke
.getParameter(AccessPath::parseIntWithArity(token.getAnArgument(), invoke.getNumArgument()))
or
token.getName() = "ReturnValue" and
result = invoke.getReturn()
or
// Language-specific tokens
result = Specific::getExtraSuccessorFromInvoke(invoke, token)
}
/**
* Holds if `invoke` invokes a call-site filter given by `token`.
*/
pragma[inline]
private predicate invocationMatchesCallSiteFilter(Specific::InvokeNode invoke, AccessPathToken token) {
token.getName() = "WithArity" and
invoke.getNumArgument() = AccessPath::parseIntUnbounded(token.getAnArgument())
or
Specific::invocationMatchesExtraCallSiteFilter(invoke, token)
}
/**
* Gets the API node identified by the first `n` tokens of `path` in the given `(package, type, path)` tuple.
*/
pragma[nomagic]
private API::Node getNodeFromPath(string package, string type, AccessPath path, int n) {
isRelevantFullPath(package, type, path) and
(
n = 0 and
exists(string package2, string type2, AccessPath path2 |
typeModel(package, type, package2, type2, path2) and
result = getNodeFromPath(package2, type2, path2, path2.getNumToken())
)
or
// Language-specific cases, such as handling of global variables
result = Specific::getExtraNodeFromPath(package, type, path, n)
)
or
result = getSuccessorFromNode(getNodeFromPath(package, type, path, n - 1), path.getToken(n - 1))
or
// Similar to the other recursive case, but where the path may have stepped through one or more call-site filters
result =
getSuccessorFromInvoke(getInvocationFromPath(package, type, path, n - 1), path.getToken(n - 1))
}
/** Gets the node identified by the given `(package, type, path)` tuple. */
API::Node getNodeFromPath(string package, string type, AccessPath path) {
result = getNodeFromPath(package, type, path, path.getNumToken())
}
/**
* Gets an invocation identified by the given `(package, type, path)` tuple.
*
* Unlike `getNodeFromPath`, the `path` may end with one or more call-site filters.
*/
Specific::InvokeNode getInvocationFromPath(string package, string type, AccessPath path, int n) {
result = Specific::getAnInvocationOf(getNodeFromPath(package, type, path, n))
or
result = getInvocationFromPath(package, type, path, n - 1) and
invocationMatchesCallSiteFilter(result, path.getToken(n - 1))
}
/** Gets an invocation identified by the given `(package, type, path)` tuple. */
Specific::InvokeNode getInvocationFromPath(string package, string type, AccessPath path) {
result = getInvocationFromPath(package, type, path, path.getNumToken())
}
/**
* Holds if `name` is a valid name for an access path token in the identifying access path.
*/
bindingset[name]
predicate isValidTokenNameInIdentifyingAccessPath(string name) {
name = ["Argument", "Parameter", "ReturnValue", "WithArity"]
or
Specific::isExtraValidTokenNameInIdentifyingAccessPath(name)
}
/**
* Holds if `name` is a valid name for an access path token with no arguments, occuring
* in an identifying access path.
*/
bindingset[name]
predicate isValidNoArgumentTokenInIdentifyingAccessPath(string name) {
name = "ReturnValue"
or
Specific::isExtraValidNoArgumentTokenInIdentifyingAccessPath(name)
}
/**
* Holds if `argument` is a valid argument to an access path token with the given `name`, occurring
* in an identifying access path.
*/
bindingset[name, argument]
predicate isValidTokenArgumentInIdentifyingAccessPath(string name, string argument) {
name = ["Argument", "Parameter"] and
argument.regexpMatch("(N-|-)?\\d+(\\.\\.((N-|-)?\\d+)?)?")
or
name = "WithArity" and
argument.regexpMatch("\\d+(\\.\\.(\\d+)?)?")
or
Specific::isExtraValidTokenArgumentInIdentifyingAccessPath(name, argument)
}
/**
* Module providing access to the imported models in terms of API graph nodes.
*/
module ModelOutput {
/**
* Holds if a CSV source model contributed `source` with the given `kind`.
*/
API::Node getASourceNode(string kind) {
exists(string package, string type, string path |
sourceModel(package, type, path, kind) and
result = getNodeFromPath(package, type, path)
)
}
/**
* Holds if a CSV sink model contributed `sink` with the given `kind`.
*/
API::Node getASinkNode(string kind) {
exists(string package, string type, string path |
sinkModel(package, type, path, kind) and
result = getNodeFromPath(package, type, path)
)
}
/**
* Holds if a relevant CSV summary exists for these parameters.
*/
predicate relevantSummaryModel(
string package, string type, string path, string input, string output, string kind
) {
isRelevantPackage(package) and
summaryModel(package, type, path, input, output, kind)
}
/**
* Holds if a `baseNode` is an invocation identified by the `package,type,path` part of a summary row.
*/
predicate resolvedSummaryBase(
string package, string type, string path, Specific::InvokeNode baseNode
) {
summaryModel(package, type, path, _, _, _) and
baseNode = getInvocationFromPath(package, type, path)
}
/**
* Holds if `node` is seen as an instance of `(package,type)` due to a type definition
* contributed by a CSV model.
*/
API::Node getATypeNode(string package, string type) {
exists(string package2, string type2, AccessPath path |
typeModel(package, type, package2, type2, path) and
result = getNodeFromPath(package2, type2, path)
)
}
/**
* Gets an error message relating to an invalid CSV row in a model.
*/
string getAWarning() {
// Check number of columns
exists(string row, string kind, int expectedArity, int actualArity |
any(SourceModelCsv csv).row(row) and kind = "source" and expectedArity = 4
or
any(SinkModelCsv csv).row(row) and kind = "sink" and expectedArity = 4
or
any(SummaryModelCsv csv).row(row) and kind = "summary" and expectedArity = 6
or
any(TypeModelCsv csv).row(row) and kind = "type" and expectedArity = 5
|
actualArity = count(row.indexOf(";")) + 1 and
actualArity != expectedArity and
result =
"CSV " + kind + " row should have " + expectedArity + " columns but has " + actualArity +
": " + row
)
or
// Check names and arguments of access path tokens
exists(AccessPath path, AccessPathToken token |
isRelevantFullPath(_, _, path) and
token = path.getToken(_)
|
not isValidTokenNameInIdentifyingAccessPath(token.getName()) and
result = "Invalid token name '" + token.getName() + "' in access path: " + path
or
isValidTokenNameInIdentifyingAccessPath(token.getName()) and
exists(string argument |
argument = token.getAnArgument() and
not isValidTokenArgumentInIdentifyingAccessPath(token.getName(), argument) and
result =
"Invalid argument '" + argument + "' in token '" + token + "' in access path: " + path
)
or
isValidTokenNameInIdentifyingAccessPath(token.getName()) and
token.getNumArgument() = 0 and
not isValidNoArgumentTokenInIdentifyingAccessPath(token.getName()) and
result = "Invalid token '" + token + "' is missing its arguments, in access path: " + path
)
}
}

View File

@@ -0,0 +1,184 @@
/**
* Contains the language-specific part of the models-as-data implementation found in `ApiGraphModels.qll`.
*
* It must export the following members:
* ```ql
* class Unit // a unit type
* class InvokeNode // a type representing an invocation connected to the API graph
* module API // the API graph module
* predicate isPackageUsed(string package)
* API::Node getExtraNodeFromPath(string package, string type, string path, int n)
* API::Node getExtraSuccessorFromNode(API::Node node, AccessPathToken token)
* API::Node getExtraSuccessorFromInvoke(InvokeNode node, AccessPathToken token)
* predicate invocationMatchesExtraCallSiteFilter(InvokeNode invoke, AccessPathToken token)
* InvokeNode getAnInvocationOf(API::Node node)
* ```
*/
private import ruby
private import codeql.ruby.DataFlow
private import codeql.ruby.dataflow.internal.DataFlowPrivate as DataFlowPrivate
private import ApiGraphModels
class Unit = DataFlowPrivate::Unit;
// Re-export libraries needed by ApiGraphModels.qll
import codeql.ruby.ApiGraphs
import codeql.ruby.dataflow.internal.AccessPathSyntax as AccessPathSyntax
private import AccessPathSyntax
private import codeql.ruby.dataflow.internal.FlowSummaryImplSpecific as FlowSummaryImplSpecific
private import codeql.ruby.dataflow.internal.DataFlowDispatch as DataFlowDispatch
/**
* Holds if models describing `package` may be relevant for the analysis of this database.
*
* In the context of Ruby, this is the name of a Ruby gem.
*/
bindingset[package]
predicate isPackageUsed(string package) {
// For now everything is modelled as an access path starting at any top-level, so the package name has no effect.
//
// We allow an arbitrary package name so that the model can record the name of the package in case it's needed in the future.
//
// In principle we should consider a package to be "used" if there is a transitive dependency on it, but we can only
// reliably see the direct dependencies.
//
// In practice, packages try to use unique top-level module names, which mitigates the precision loss of not checking
// the package name.
any()
}
/** Gets a Ruby-specific interpretation of the `(package, type, path)` tuple after resolving the first `n` access path tokens. */
bindingset[package, type, path]
API::Node getExtraNodeFromPath(string package, string type, AccessPath path, int n) {
isRelevantFullPath(package, type, path) and
exists(package) and // Allow any package name, see `isPackageUsed`.
type = "" and
n = 0 and
result = API::root()
or
// A row of form `;any;Method[foo]` should match any method named `foo`.
exists(package) and
type = "any" and
n = 1 and
exists(EntryPointFromAnyType entry |
methodMatchedByName(path, entry.getName()) and
result = entry.getANode()
)
}
/**
* Holds if `path` occurs in a CSV row with type `any`, meaning it can start
* matching anywhere, and the path begins with `Method[methodName]`.
*/
private predicate methodMatchedByName(AccessPath path, string methodName) {
isRelevantFullPath(_, "any", path) and
exists(AccessPathToken token |
token = path.getToken(0) and
token.getName() = "Method" and
methodName = token.getAnArgument()
)
}
/**
* 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`.
*/
bindingset[token]
API::Node getExtraSuccessorFromNode(API::Node node, AccessPathToken token) {
token.getName() = "Member" and
result = node.getMember(token.getAnArgument())
or
token.getName() = "Method" and
result = node.getMethod(token.getAnArgument())
or
token.getName() = "Instance" and
result = node.getInstance()
or
token.getName() = "Parameter" and
result =
node.getASuccessor(API::Label::getLabelFromArgumentPosition(FlowSummaryImplSpecific::parseParamBody(token
.getAnArgument())))
// Note: The "ArrayElement" token is not implemented yet, as it ultimately requires type-tracking and
// API graphs to be aware of the steps involving ArrayElement contributed by the standard library model.
// Type-tracking cannot summarize function calls on its own, so it doesn't benefit from synthesized callables.
}
/**
* Gets a Ruby-specific API graph successor of `node` reachable by resolving `token`.
*/
bindingset[token]
API::Node getExtraSuccessorFromInvoke(InvokeNode node, AccessPathToken token) {
token.getName() = "Argument" and
result =
node.getASuccessor(API::Label::getLabelFromParameterPosition(FlowSummaryImplSpecific::parseArgBody(token
.getAnArgument())))
}
/**
* Holds if `invoke` matches the Ruby-specific call site filter in `token`.
*/
bindingset[token]
predicate invocationMatchesExtraCallSiteFilter(InvokeNode invoke, AccessPathToken token) {
token.getName() = "WithBlock" and
exists(invoke.getBlock())
or
token.getName() = "WithoutBlock" and
not exists(invoke.getBlock())
}
/** An API graph node representing a method call. */
class InvokeNode extends API::MethodAccessNode {
/** Gets the number of arguments to the call. */
int getNumArgument() { result = getCallNode().getNumberOfArguments() }
}
/** Gets the `InvokeNode` corresponding to a specific invocation of `node`. */
InvokeNode getAnInvocationOf(API::Node node) { result = node }
/**
* Holds if `name` is a valid name for an access path token in the identifying access path.
*/
bindingset[name]
predicate isExtraValidTokenNameInIdentifyingAccessPath(string name) {
name = ["Member", "Method", "Instance", "WithBlock", "WithoutBlock"]
}
/**
* Holds if `name` is a valid name for an access path token with no arguments, occuring
* in an identifying access path.
*/
predicate isExtraValidNoArgumentTokenInIdentifyingAccessPath(string name) {
name = ["Instance", "WithBlock", "WithoutBlock"]
}
/**
* Holds if `argument` is a valid argument to an access path token with the given `name`, occurring
* in an identifying access path.
*/
bindingset[name, argument]
predicate isExtraValidTokenArgumentInIdentifyingAccessPath(string name, string argument) {
name = ["Member", "Method"] and
exists(argument)
or
name = ["Argument", "Parameter"] and
(
argument = ["self", "block"]
or
argument.regexpMatch("\\w+:") // keyword argument
)
}

View File

@@ -52,7 +52,7 @@ class ExconHttpRequest extends HTTP::Client::Request::Range {
override DataFlow::Node getResponseBody() { result = requestNode.getAMethodCall("body") }
override DataFlow::Node getURL() {
override DataFlow::Node getAUrlPart() {
// For one-off requests, the URL is in the first argument of the request method call.
// For connection re-use, the URL is split between the first argument of the `new` call
// and the `path` keyword argument of the request method call.
@@ -125,7 +125,7 @@ private predicate setsDefaultVerification(DataFlow::CallNode callNode, boolean v
private predicate isSslVerifyPeerLiteral(DataFlow::Node node) {
exists(DataFlow::LocalSourceNode literal |
literal.asExpr().getExpr().getConstantValue().isStringOrSymbol("ssl_verify_peer") and
literal.asExpr().getExpr().getConstantValue().isStringlikeValue("ssl_verify_peer") and
literal.flowsTo(node)
)
}

View File

@@ -45,7 +45,7 @@ class FaradayHttpRequest extends HTTP::Client::Request::Range {
override DataFlow::Node getResponseBody() { result = requestNode.getAMethodCall("body") }
override DataFlow::Node getURL() {
override DataFlow::Node getAUrlPart() {
result = requestUse.getArgument(0) or
result = connectionUse.(DataFlow::CallNode).getArgument(0) or
result = connectionUse.(DataFlow::CallNode).getKeywordArgument("url")
@@ -96,7 +96,7 @@ private predicate isSslOptionsPairDisablingValidation(CfgNodes::ExprNodes::PairC
/** Holds if `node` represents the symbol literal with the given `valueText`. */
private predicate isSymbolLiteral(DataFlow::Node node, string valueText) {
exists(DataFlow::LocalSourceNode literal |
literal.asExpr().getExpr().getConstantValue().isStringOrSymbol(valueText) and
literal.asExpr().getExpr().getConstantValue().isStringlikeValue(valueText) and
literal.flowsTo(node)
)
}

View File

@@ -36,7 +36,7 @@ class HttpClientRequest extends HTTP::Client::Request::Range {
this = requestUse.asExpr().getExpr()
}
override DataFlow::Node getURL() { result = requestUse.getArgument(0) }
override DataFlow::Node getAUrlPart() { result = requestUse.getArgument(0) }
override DataFlow::Node getResponseBody() {
// The `get_content` and `post_content` methods return the response body as

View File

@@ -35,7 +35,7 @@ class HttpartyRequest extends HTTP::Client::Request::Range {
this = requestUse.asExpr().getExpr()
}
override DataFlow::Node getURL() { result = requestUse.getArgument(0) }
override DataFlow::Node getAUrlPart() { result = requestUse.getArgument(0) }
override DataFlow::Node getResponseBody() {
// If HTTParty can recognise the response type, it will parse and return it
@@ -79,7 +79,7 @@ class HttpartyRequest extends HTTP::Client::Request::Range {
/** Holds if `node` represents the symbol literal `verify` or `verify_peer`. */
private predicate isVerifyLiteral(DataFlow::Node node) {
exists(DataFlow::LocalSourceNode literal |
literal.asExpr().getExpr().getConstantValue().isStringOrSymbol(["verify", "verify_peer"]) and
literal.asExpr().getExpr().getConstantValue().isStringlikeValue(["verify", "verify_peer"]) and
literal.flowsTo(node)
)
}

View File

@@ -51,7 +51,7 @@ class NetHttpRequest extends HTTP::Client::Request::Range {
* Gets the node representing the URL of the request.
* Currently unused, but may be useful in future, e.g. to filter out certain requests.
*/
override DataFlow::Node getURL() { result = request.getArgument(0) }
override DataFlow::Node getAUrlPart() { result = request.getArgument(0) }
override DataFlow::Node getResponseBody() { result = responseBody }

View File

@@ -32,7 +32,7 @@ class OpenUriRequest extends HTTP::Client::Request::Range {
this = requestUse.asExpr().getExpr()
}
override DataFlow::Node getURL() { result = requestUse.getArgument(0) }
override DataFlow::Node getAUrlPart() { result = requestUse.getArgument(0) }
override DataFlow::Node getResponseBody() {
result = requestNode.getAMethodCall(["read", "readlines"])
@@ -65,7 +65,7 @@ class OpenUriKernelOpenRequest extends HTTP::Client::Request::Range {
this = requestUse.asExpr().getExpr()
}
override DataFlow::Node getURL() { result = requestUse.getArgument(0) }
override DataFlow::Node getAUrlPart() { result = requestUse.getArgument(0) }
override DataFlow::CallNode getResponseBody() {
result.asExpr().getExpr().(MethodCall).getMethodName() in ["read", "readlines"] and
@@ -117,7 +117,7 @@ private predicate isSslVerifyModeNonePair(CfgNodes::ExprNodes::PairCfgNode p) {
/** Holds if `node` can represent the symbol literal `:ssl_verify_mode`. */
private predicate isSslVerifyModeLiteral(DataFlow::Node node) {
exists(DataFlow::LocalSourceNode literal |
literal.asExpr().getExpr().getConstantValue().isStringOrSymbol("ssl_verify_mode") and
literal.asExpr().getExpr().getConstantValue().isStringlikeValue("ssl_verify_mode") and
literal.flowsTo(node)
)
}

View File

@@ -38,7 +38,7 @@ class RestClientHttpRequest extends HTTP::Client::Request::Range {
)
}
override DataFlow::Node getURL() {
override DataFlow::Node getAUrlPart() {
result = requestUse.getKeywordArgument("url")
or
result = requestUse.getArgument(0) and
@@ -86,7 +86,7 @@ private predicate isVerifySslNonePair(CfgNodes::ExprNodes::PairCfgNode p) {
/** Holds if `node` can represent the symbol literal `:verify_ssl`. */
private predicate isSslVerifyModeLiteral(DataFlow::Node node) {
exists(DataFlow::LocalSourceNode literal |
literal.asExpr().getExpr().getConstantValue().isStringOrSymbol("verify_ssl") and
literal.asExpr().getExpr().getConstantValue().isStringlikeValue("verify_ssl") and
literal.flowsTo(node)
)
}

View File

@@ -26,7 +26,7 @@ class TyphoeusHttpRequest extends HTTP::Client::Request::Range {
this = requestUse.asExpr().getExpr()
}
override DataFlow::Node getURL() { result = requestUse.getArgument(0) }
override DataFlow::Node getAUrlPart() { result = requestUse.getArgument(0) }
override DataFlow::Node getResponseBody() { result = requestNode.getAMethodCall("body") }
@@ -67,7 +67,7 @@ private predicate isSslVerifyPeerFalsePair(CfgNodes::ExprNodes::PairCfgNode p) {
/** Holds if `node` represents the symbol literal `verify` or `verify_peer`. */
private predicate isSslVerifyPeerLiteral(DataFlow::Node node) {
exists(DataFlow::LocalSourceNode literal |
literal.asExpr().getExpr().getConstantValue().isStringOrSymbol("ssl_verifypeer") and
literal.asExpr().getExpr().getConstantValue().isStringlikeValue("ssl_verifypeer") and
literal.flowsTo(node)
)
}

View File

@@ -0,0 +1,6 @@
/**
* This file contains imports required for the Ruby version of `ConceptsShared.qll`.
* Since they are language-specific, they can't be placed directly in that file, as it is shared between languages.
*/
import codeql.ruby.DataFlow

View File

@@ -0,0 +1,13 @@
/**
* Provides Concepts which are shared across languages.
*
* Each language has a language specific `Concepts.qll` file that can import the
* shared concepts from this file. A language can either re-export the concept directly,
* or can add additional member-predicates that are needed for that language.
*
* Moving forward, `Concepts.qll` will be the staging ground for brand new concepts from
* each language, but we will maintain a discipline of moving those concepts to
* `ConceptsShared.qll` ASAP.
*/
private import ConceptsImports

View File

@@ -7,7 +7,7 @@
*/
private import AST
private import codeql.ruby.security.performance.RegExpTreeView as RETV
private import codeql.ruby.Regexp as RE
private import codeql.ruby.ast.internal.Synthesis
/**
@@ -37,7 +37,7 @@ private predicate shouldPrintAstEdge(AstNode parent, string edgeName, AstNode ch
newtype TPrintNode =
TPrintRegularAstNode(AstNode n) { shouldPrintNode(n) } or
TPrintRegExpNode(RETV::RegExpTerm term) {
TPrintRegExpNode(RE::RegExpTerm term) {
exists(RegExpLiteral literal |
shouldPrintNode(literal) and
term.getRootTerm() = literal.getParsed()
@@ -107,7 +107,7 @@ class PrintRegularAstNode extends PrintAstNode, TPrintRegularAstNode {
or
// If this AST node is a regexp literal, add the parsed regexp tree as a
// child.
exists(RETV::RegExpTerm t | t = astNode.(RegExpLiteral).getParsed() |
exists(RE::RegExpTerm t | t = astNode.(RegExpLiteral).getParsed() |
result = TPrintRegExpNode(t) and edgeName = "getParsed"
)
}
@@ -134,7 +134,7 @@ class PrintRegularAstNode extends PrintAstNode, TPrintRegularAstNode {
/** A parsed regexp node in the output tree. */
class PrintRegExpNode extends PrintAstNode, TPrintRegExpNode {
RETV::RegExpTerm regexNode;
RE::RegExpTerm regexNode;
PrintRegExpNode() { this = TPrintRegExpNode(regexNode) }
@@ -147,7 +147,7 @@ class PrintRegExpNode extends PrintAstNode, TPrintRegExpNode {
exists(int i | result = TPrintRegExpNode(regexNode.getChild(i)) and edgeName = i.toString())
}
override int getOrder() { exists(RETV::RegExpTerm p | p.getChild(result) = regexNode) }
override int getOrder() { exists(RE::RegExpTerm p | p.getChild(result) = regexNode) }
override predicate hasLocationInfo(
string filepath, int startline, int startcolumn, int endline, int endcolumn

View File

@@ -1,76 +1,66 @@
/** Provides a class hierarchy corresponding to a parse tree of regular expressions. */
private import internal.ParseRegExp
private import codeql.NumberUtils
private import codeql.ruby.ast.Literal as AST
private import ParseRegExp
import codeql.Locations
private import codeql.ruby.DataFlow
private import codeql.Locations
/**
* Holds if `term` is an ecape class representing e.g. `\d`.
* `clazz` is which character class it represents, e.g. "d" for `\d`.
* An element containing a regular expression term, that is, either
* a string literal (parsed as a regular expression)
* or another regular expression term.
*
* For sequences and alternations, we require at least one child.
* Otherwise, we wish to represent the term differently.
* This avoids multiple representations of the same term.
*/
predicate isEscapeClass(RegExpTerm term, string clazz) {
exists(RegExpCharacterClassEscape escape | term = escape | escape.getValue() = clazz)
or
// TODO: expand to cover more properties
exists(RegExpNamedCharacterProperty escape | term = escape |
escape.getName().toLowerCase() = "digit" and
if escape.isInverted() then clazz = "D" else clazz = "d"
private newtype TRegExpParent =
/** A string literal used as a regular expression */
TRegExpLiteral(RegExp re) or
/** A quantified term */
TRegExpQuantifier(RegExp re, int start, int end) { re.qualifiedItem(start, end, _, _) } or
/** A sequence term */
TRegExpSequence(RegExp re, int start, int end) { re.sequence(start, end) } or
/** An alternation term */
TRegExpAlt(RegExp re, int start, int end) { re.alternation(start, end) } or
/** A character class term */
TRegExpCharacterClass(RegExp re, int start, int end) { re.charSet(start, end) } or
/** A character range term */
TRegExpCharacterRange(RegExp re, int start, int end) { re.charRange(_, start, _, _, end) } or
/** A group term */
TRegExpGroup(RegExp re, int start, int end) { re.group(start, end) } or
/** A special character */
TRegExpSpecialChar(RegExp re, int start, int end) { re.specialCharacter(start, end, _) } or
/** A normal character */
TRegExpNormalChar(RegExp re, int start, int end) {
re.normalCharacterSequence(start, end)
or
escape.getName().toLowerCase() = "space" and
if escape.isInverted() then clazz = "S" else clazz = "s"
or
escape.getName().toLowerCase() = "word" and
if escape.isInverted() then clazz = "W" else clazz = "w"
)
}
/**
* Holds if the regular expression should not be considered.
*/
predicate isExcluded(RegExpParent parent) {
parent.(RegExpTerm).getRegExp().(AST::RegExpLiteral).hasFreeSpacingFlag() // exclude free-spacing mode regexes
}
/**
* A module containing predicates for determining which flags a regular expression have.
*/
module RegExpFlags {
/**
* Holds if `root` has the `i` flag for case-insensitive matching.
*/
predicate isIgnoreCase(RegExpTerm root) {
root.isRootTerm() and
root.getLiteral().isIgnoreCase()
re.escapedCharacter(start, end) and
not re.specialCharacter(start, end, _)
} or
/** A back reference */
TRegExpBackRef(RegExp re, int start, int end) { re.backreference(start, end) } or
/** A named character property */
TRegExpNamedCharacterProperty(RegExp re, int start, int end) {
re.namedCharacterProperty(start, end, _)
}
/**
* Gets the flags for `root`, or the empty string if `root` has no flags.
*/
string getFlags(RegExpTerm root) {
root.isRootTerm() and
result = root.getLiteral().getFlags()
}
/**
* Holds if `root` has the `s` flag for multi-line matching.
*/
predicate isDotAll(RegExpTerm root) {
root.isRootTerm() and
root.getLiteral().isDotAll()
}
}
/**
* An element containing a regular expression term, that is, either
* a string literal (parsed as a regular expression)
* or another regular expression term.
*/
class RegExpParent extends TRegExpParent {
/** Gets a textual representation of this element. */
string toString() { result = "RegExpParent" }
/** Gets the `i`th child term. */
RegExpTerm getChild(int i) { none() }
/** Gets a child term . */
final RegExpTerm getAChild() { result = this.getChild(_) }
/** Gets the number of child terms. */
int getNumChild() { result = count(this.getAChild()) }
/**
@@ -86,6 +76,7 @@ class RegExpParent extends TRegExpParent {
final string getPrimaryQlClasses() { result = concat(this.getAPrimaryQlClass(), ",") }
}
/** A string literal used as a regular expression */
class RegExpLiteral extends TRegExpLiteral, RegExpParent {
RegExp re;
@@ -93,15 +84,22 @@ class RegExpLiteral extends TRegExpLiteral, RegExpParent {
override RegExpTerm getChild(int i) { i = 0 and result.getRegExp() = re and result.isRootTerm() }
/** Holds if dot, `.`, matches all characters, including newlines. */
predicate isDotAll() { re.isDotAll() }
/** Holds if this regex matching is case-insensitive for this regex. */
predicate isIgnoreCase() { re.isIgnoreCase() }
/** Get a string representing all modes for this regex. */
string getFlags() { result = re.getFlags() }
/** Gets the primary QL class for this regex. */
override string getAPrimaryQlClass() { result = "RegExpLiteral" }
}
/**
* A regular expression term, that is, a syntactic part of a regular expression.
*/
class RegExpTerm extends RegExpParent {
RegExp re;
int start;
@@ -130,14 +128,24 @@ class RegExpTerm extends RegExpParent {
this = TRegExpNamedCharacterProperty(re, start, end)
}
/**
* Gets the outermost term of this regular expression.
*/
RegExpTerm getRootTerm() {
this.isRootTerm() and result = this
or
result = this.getParent().(RegExpTerm).getRootTerm()
}
/**
* Holds if this term is part of a string literal
* that is interpreted as a regular expression.
*/
predicate isUsedAsRegExp() { any() }
/**
* Holds if this is the root term of a regular expression.
*/
predicate isRootTerm() { start = 0 and end = re.getText().length() }
override RegExpTerm getChild(int i) {
@@ -162,38 +170,58 @@ class RegExpTerm extends RegExpParent {
result = this.(RegExpNamedCharacterProperty).getChild(i)
}
/**
* Gets the parent term of this regular expression term, or the
* regular expression literal if this is the root term.
*/
RegExpParent getParent() { result.getAChild() = this }
/** Gets the associated `RegExp`. */
RegExp getRegExp() { result = re }
/** Gets the offset at which this term starts. */
int getStart() { result = start }
/** Gets the offset at which this term ends. */
int getEnd() { result = end }
override string toString() { result = re.getText().substring(start, end) }
override string getAPrimaryQlClass() { result = "RegExpTerm" }
/**
* Gets the location of the surrounding regex, as locations inside the regex do not exist.
* To get location information corresponding to the term inside the regex,
* use `hasLocationInfo`.
*/
Location getLocation() { result = re.getLocation() }
pragma[noinline]
private predicate componentHasLocationInfo(
int i, string filepath, int startline, int startcolumn, int endline, int endcolumn
) {
re.getComponent(i)
.getLocation()
.hasLocationInfo(filepath, startline, startcolumn, endline, endcolumn)
}
/** Holds if this term is found at the specified location offsets. */
predicate hasLocationInfo(
string filepath, int startline, int startcolumn, int endline, int endcolumn
) {
exists(int re_start, int re_end |
re.getComponent(0).getLocation().hasLocationInfo(filepath, startline, re_start, _, _) and
re.getComponent(re.getNumberOfComponents() - 1)
.getLocation()
.hasLocationInfo(filepath, _, _, endline, re_end)
|
this.componentHasLocationInfo(0, filepath, startline, re_start, _, _) and
this.componentHasLocationInfo(re.getNumberOfComponents() - 1, filepath, _, _, endline, re_end) and
startcolumn = re_start + start and
endcolumn = re_start + end - 1
)
}
/** Gets the file in which this term is found. */
File getFile() { result = this.getLocation().getFile() }
/** Gets the raw source text of this term. */
string getRawValue() { result = this.toString() }
/** Gets the string literal in which this term is found. */
RegExpLiteral getLiteral() { result = TRegExpLiteral(re) }
/** Gets the regular expression term that is matched (textually) before this one, if any. */
@@ -217,28 +245,20 @@ class RegExpTerm extends RegExpParent {
result = parent.getSuccessor()
)
}
/** Gets the primary QL class for this term. */
override string getAPrimaryQlClass() { result = "RegExpTerm" }
}
newtype TRegExpParent =
TRegExpLiteral(RegExp re) or
TRegExpQuantifier(RegExp re, int start, int end) { re.qualifiedItem(start, end, _, _) } or
TRegExpSequence(RegExp re, int start, int end) { re.sequence(start, end) } or
TRegExpAlt(RegExp re, int start, int end) { re.alternation(start, end) } or
TRegExpCharacterClass(RegExp re, int start, int end) { re.charSet(start, end) } or
TRegExpCharacterRange(RegExp re, int start, int end) { re.charRange(_, start, _, _, end) } or
TRegExpGroup(RegExp re, int start, int end) { re.group(start, end) } or
TRegExpSpecialChar(RegExp re, int start, int end) { re.specialCharacter(start, end, _) } or
TRegExpNormalChar(RegExp re, int start, int end) {
re.normalCharacterSequence(start, end)
or
re.escapedCharacter(start, end) and
not re.specialCharacter(start, end, _)
} or
TRegExpBackRef(RegExp re, int start, int end) { re.backreference(start, end) } or
TRegExpNamedCharacterProperty(RegExp re, int start, int end) {
re.namedCharacterProperty(start, end, _)
}
/**
* A quantified regular expression term.
*
* Example:
*
* ```
* ((ECMA|Java)[sS]cript)*
* ```
*/
class RegExpQuantifier extends RegExpTerm, TRegExpQuantifier {
int part_end;
boolean may_repeat_forever;
@@ -255,45 +275,90 @@ class RegExpQuantifier extends RegExpTerm, TRegExpQuantifier {
result.getEnd() = part_end
}
/** Hols if this term may match an unlimited number of times. */
predicate mayRepeatForever() { may_repeat_forever = true }
/** Gets the qualifier for this term. That is e.g "?" for "a?". */
string getQualifier() { result = re.getText().substring(part_end, end) }
override string getAPrimaryQlClass() { result = "RegExpQuantifier" }
}
/**
* A regular expression term that permits unlimited repetitions.
*/
class InfiniteRepetitionQuantifier extends RegExpQuantifier {
InfiniteRepetitionQuantifier() { this.mayRepeatForever() }
override string getAPrimaryQlClass() { result = "InfiniteRepetitionQuantifier" }
}
/**
* A star-quantified term.
*
* Example:
*
* ```
* \w*
* ```
*/
class RegExpStar extends InfiniteRepetitionQuantifier {
RegExpStar() { this.getQualifier().charAt(0) = "*" }
override string getAPrimaryQlClass() { result = "RegExpStar" }
}
/**
* A plus-quantified term.
*
* Example:
*
* ```
* \w+
* ```
*/
class RegExpPlus extends InfiniteRepetitionQuantifier {
RegExpPlus() { this.getQualifier().charAt(0) = "+" }
override string getAPrimaryQlClass() { result = "RegExpPlus" }
}
/**
* An optional term.
*
* Example:
*
* ```
* ;?
* ```
*/
class RegExpOpt extends RegExpQuantifier {
RegExpOpt() { this.getQualifier().charAt(0) = "?" }
override string getAPrimaryQlClass() { result = "RegExpOpt" }
}
/**
* A range-quantified term
*
* Examples:
*
* ```
* \w{2,4}
* \w{2,}
* \w{2}
* ```
*/
class RegExpRange extends RegExpQuantifier {
string upper;
string lower;
RegExpRange() { re.multiples(part_end, end, lower, upper) }
/** Gets the string defining the upper bound of this range, if any. */
string getUpper() { result = upper }
/** Gets the string defining the lower bound of this range, if any. */
string getLower() { result = lower }
/**
@@ -311,6 +376,17 @@ class RegExpRange extends RegExpQuantifier {
override string getAPrimaryQlClass() { result = "RegExpRange" }
}
/**
* A sequence term.
*
* Example:
*
* ```
* (ECMA|Java)Script
* ```
*
* This is a sequence with the elements `(ECMA|Java)` and `Script`.
*/
class RegExpSequence extends RegExpTerm, TRegExpSequence {
RegExpSequence() {
this = TRegExpSequence(re, start, end) and
@@ -359,6 +435,15 @@ private RegExpTerm seqChild(RegExp re, int start, int end, int i) {
)
}
/**
* An alternative term, that is, a term of the form `a|b`.
*
* Example:
*
* ```
* ECMA|Java
* ```
*/
class RegExpAlt extends RegExpTerm, TRegExpAlt {
RegExpAlt() { this = TRegExpAlt(re, start, end) }
@@ -384,6 +469,19 @@ class RegExpAlt extends RegExpTerm, TRegExpAlt {
override string getAPrimaryQlClass() { result = "RegExpAlt" }
}
class RegExpCharEscape = RegExpEscape;
/**
* An escaped regular expression term, that is, a regular expression
* term starting with a backslash, which is not a backreference.
*
* Example:
*
* ```
* \.
* \w
* ```
*/
class RegExpEscape extends RegExpNormalChar {
RegExpEscape() { re.escapedCharacter(start, end) }
@@ -400,14 +498,24 @@ class RegExpEscape extends RegExpNormalChar {
or
this.getUnescaped() = "t" and result = "\t"
or
this.getUnescaped() = "f" and result = 12.toUnicode()
or
this.getUnescaped() = "v" and result = 11.toUnicode()
or
this.isUnicode() and
result = this.getUnicode()
}
/** Holds if this terms name is given by the part following the escape character. */
predicate isIdentityEscape() {
not this.getUnescaped() in ["n", "r", "t"] and not this.isUnicode()
not this.getUnescaped() in ["n", "r", "t", "f", "v"] and not this.isUnicode()
}
override string getAPrimaryQlClass() { result = "RegExpEscape" }
/** Gets the part of the term following the escape character. That is e.g. "w" if the term is "\w". */
string getUnescaped() { result = this.getText().suffix(1) }
/**
* Gets the text for this escape. That is e.g. "\w".
*/
@@ -423,46 +531,9 @@ class RegExpEscape extends RegExpNormalChar {
* E.g. for `\u0061` this returns "a".
*/
private string getUnicode() {
exists(int codepoint | codepoint = sum(this.getHexValueFromUnicode(_)) |
result = codepoint.toUnicode()
)
}
/**
* Gets int value for the `index`th char in the hex number of the unicode escape.
* E.g. for `\u0061` and `index = 2` this returns 96 (the number `6` interpreted as hex).
*/
private int getHexValueFromUnicode(int index) {
this.isUnicode() and
exists(string hex, string char | hex = this.getText().suffix(2) |
char = hex.charAt(index) and
result = 16.pow(hex.length() - index - 1) * toHex(char)
)
result = parseHexInt(this.getText().suffix(2)).toUnicode()
}
string getUnescaped() { result = this.getText().suffix(1) }
override string getAPrimaryQlClass() { result = "RegExpEscape" }
}
/**
* Gets the hex number for the `hex` char.
*/
private int toHex(string hex) {
hex = [0 .. 9].toString() and
result = hex.toInt()
or
result = 10 and hex = ["a", "A"]
or
result = 11 and hex = ["b", "B"]
or
result = 12 and hex = ["c", "C"]
or
result = 13 and hex = ["d", "D"]
or
result = 14 and hex = ["e", "E"]
or
result = 15 and hex = ["f", "F"]
}
/**
@@ -486,15 +557,13 @@ class RegExpWordBoundary extends RegExpSpecialChar {
class RegExpCharacterClassEscape extends RegExpEscape {
RegExpCharacterClassEscape() { this.getValue() in ["d", "D", "s", "S", "w", "W", "h", "H"] }
/** Gets the name of the character class; for example, `w` for `\w`. */
// override string getValue() { result = value }
override RegExpTerm getChild(int i) { none() }
override string getAPrimaryQlClass() { result = "RegExpCharacterClassEscape" }
}
/**
* A character class.
* A character class in a regular expression.
*
* Examples:
*
@@ -506,8 +575,10 @@ class RegExpCharacterClassEscape extends RegExpEscape {
class RegExpCharacterClass extends RegExpTerm, TRegExpCharacterClass {
RegExpCharacterClass() { this = TRegExpCharacterClass(re, start, end) }
/** Holds if this character class is inverted, matching the opposite of its content. */
predicate isInverted() { re.getChar(start + 1) = "^" }
/** Holds if this character class can match anything. */
predicate isUniversalClass() {
// [^]
this.isInverted() and not exists(this.getAChild())
@@ -543,6 +614,15 @@ class RegExpCharacterClass extends RegExpTerm, TRegExpCharacterClass {
override string getAPrimaryQlClass() { result = "RegExpCharacterClass" }
}
/**
* A character range in a character class in a regular expression.
*
* Example:
*
* ```
* a-z
* ```
*/
class RegExpCharacterRange extends RegExpTerm, TRegExpCharacterRange {
int lower_end;
int upper_start;
@@ -552,6 +632,7 @@ class RegExpCharacterRange extends RegExpTerm, TRegExpCharacterRange {
re.charRange(_, start, lower_end, upper_start, end)
}
/** Holds if this range goes from `lo` to `hi`, in effect is `lo-hi`. */
predicate isRange(string lo, string hi) {
lo = re.getText().substring(start, lower_end) and
hi = re.getText().substring(upper_start, end)
@@ -572,11 +653,26 @@ class RegExpCharacterRange extends RegExpTerm, TRegExpCharacterRange {
override string getAPrimaryQlClass() { result = "RegExpCharacterRange" }
}
/**
* A normal character in a regular expression, that is, a character
* without special meaning. This includes escaped characters.
*
* Examples:
* ```
* t
* \t
* ```
*/
class RegExpNormalChar extends RegExpTerm, TRegExpNormalChar {
RegExpNormalChar() { this = TRegExpNormalChar(re, start, end) }
/**
* Holds if this constant represents a valid Unicode character (as opposed
* to a surrogate code point that does not correspond to a character by itself.)
*/
predicate isCharacter() { any() }
/** Gets the string representation of the char matched by this term. */
string getValue() { result = re.getText().substring(start, end) }
override RegExpTerm getChild(int i) { none() }
@@ -584,6 +680,16 @@ class RegExpNormalChar extends RegExpTerm, TRegExpNormalChar {
override string getAPrimaryQlClass() { result = "RegExpNormalChar" }
}
/**
* A constant regular expression term, that is, a regular expression
* term matching a single string. Currently, this will always be a single character.
*
* Example:
*
* ```
* a
* ```
*/
class RegExpConstant extends RegExpTerm {
string value;
@@ -602,8 +708,13 @@ class RegExpConstant extends RegExpTerm {
value = this.(RegExpSpecialChar).getChar()
}
/**
* Holds if this constant represents a valid Unicode character (as opposed
* to a surrogate code point that does not correspond to a character by itself.)
*/
predicate isCharacter() { any() }
/** Gets the string matched by this constant term. */
string getValue() { result = value }
override RegExpTerm getChild(int i) { none() }
@@ -611,6 +722,17 @@ class RegExpConstant extends RegExpTerm {
override string getAPrimaryQlClass() { result = "RegExpConstant" }
}
/**
* A grouped regular expression.
*
* Examples:
*
* ```
* (ECMA|Java)
* (?:ECMA|Java)
* (?<quote>['"])
* ```
*/
class RegExpGroup extends RegExpTerm, TRegExpGroup {
RegExpGroup() { this = TRegExpGroup(re, start, end) }
@@ -625,16 +747,15 @@ class RegExpGroup extends RegExpTerm, TRegExpGroup {
*/
int getNumber() { result = re.getGroupNumber(start, end) }
/** Holds if this is a capture group. */
predicate isCapture() { exists(this.getNumber()) }
/** Holds if this is a named capture group. */
predicate isNamed() { exists(this.getName()) }
/** Gets the name of this capture group, if any. */
string getName() { result = re.getGroupName(start, end) }
predicate isCharacter() { any() }
string getValue() { result = re.getText().substring(start, end) }
override RegExpTerm getChild(int i) {
result.getRegExp() = re and
i = 0 and
@@ -644,6 +765,16 @@ class RegExpGroup extends RegExpTerm, TRegExpGroup {
override string getAPrimaryQlClass() { result = "RegExpGroup" }
}
/**
* A special character in a regular expression.
*
* Examples:
* ```
* ^
* $
* .
* ```
*/
class RegExpSpecialChar extends RegExpTerm, TRegExpSpecialChar {
string char;
@@ -652,8 +783,13 @@ class RegExpSpecialChar extends RegExpTerm, TRegExpSpecialChar {
re.specialCharacter(start, end, char)
}
/**
* Holds if this constant represents a valid Unicode character (as opposed
* to a surrogate code point that does not correspond to a character by itself.)
*/
predicate isCharacter() { any() }
/** Gets the char for this term. */
string getChar() { result = char }
override RegExpTerm getChild(int i) { none() }
@@ -661,29 +797,63 @@ class RegExpSpecialChar extends RegExpTerm, TRegExpSpecialChar {
override string getAPrimaryQlClass() { result = "RegExpSpecialChar" }
}
/**
* A dot regular expression.
*
* Example:
*
* ```
* .
* ```
*/
class RegExpDot extends RegExpSpecialChar {
RegExpDot() { this.getChar() = "." }
override string getAPrimaryQlClass() { result = "RegExpDot" }
}
/**
* A dollar assertion `$` or `\Z` matching the end of a line.
*
* Example:
*
* ```
* $
* ```
*/
class RegExpDollar extends RegExpSpecialChar {
RegExpDollar() { this.getChar() = ["$", "\\Z", "\\z"] }
override string getAPrimaryQlClass() { result = "RegExpDollar" }
}
/**
* A caret assertion `^` or `\A` matching the beginning of a line.
*
* Example:
*
* ```
* ^
* ```
*/
class RegExpCaret extends RegExpSpecialChar {
RegExpCaret() { this.getChar() = ["^", "\\A"] }
override string getAPrimaryQlClass() { result = "RegExpCaret" }
}
/**
* A zero-width match, that is, either an empty group or an assertion.
*
* Examples:
* ```
* ()
* (?=\w)
* ```
*/
class RegExpZeroWidthMatch extends RegExpGroup {
RegExpZeroWidthMatch() { re.zeroWidthMatch(start, end) }
override predicate isCharacter() { any() }
override RegExpTerm getChild(int i) { none() }
override string getAPrimaryQlClass() { result = "RegExpZeroWidthMatch" }
@@ -714,34 +884,101 @@ class RegExpSubPattern extends RegExpZeroWidthMatch {
}
}
/**
* A zero-width lookahead assertion.
*
* Examples:
*
* ```
* (?=\w)
* (?!\n)
* ```
*/
abstract class RegExpLookahead extends RegExpSubPattern { }
/**
* A positive-lookahead assertion.
*
* Examples:
*
* ```
* (?=\w)
* ```
*/
class RegExpPositiveLookahead extends RegExpLookahead {
RegExpPositiveLookahead() { re.positiveLookaheadAssertionGroup(start, end) }
override string getAPrimaryQlClass() { result = "RegExpPositiveLookahead" }
}
/**
* A negative-lookahead assertion.
*
* Examples:
*
* ```
* (?!\n)
* ```
*/
class RegExpNegativeLookahead extends RegExpLookahead {
RegExpNegativeLookahead() { re.negativeLookaheadAssertionGroup(start, end) }
override string getAPrimaryQlClass() { result = "RegExpNegativeLookahead" }
}
/**
* A zero-width lookbehind assertion.
*
* Examples:
*
* ```
* (?<=\.)
* (?<!\\)
* ```
*/
abstract class RegExpLookbehind extends RegExpSubPattern { }
/**
* A positive-lookbehind assertion.
*
* Examples:
*
* ```
* (?<=\.)
* ```
*/
class RegExpPositiveLookbehind extends RegExpLookbehind {
RegExpPositiveLookbehind() { re.positiveLookbehindAssertionGroup(start, end) }
override string getAPrimaryQlClass() { result = "RegExpPositiveLookbehind" }
}
/**
* A negative-lookbehind assertion.
*
* Examples:
*
* ```
* (?<!\\)
* ```
*/
class RegExpNegativeLookbehind extends RegExpLookbehind {
RegExpNegativeLookbehind() { re.negativeLookbehindAssertionGroup(start, end) }
override string getAPrimaryQlClass() { result = "RegExpNegativeLookbehind" }
}
/**
* A back reference, that is, a term of the form `\i` or `\k<name>`
* in a regular expression.
*
* Examples:
*
* ```
* \1
* (?P=quote)
* ```
*/
class RegExpBackRef extends RegExpTerm, TRegExpBackRef {
RegExpBackRef() { this = TRegExpBackRef(re, start, end) }
@@ -793,50 +1030,7 @@ class RegExpNamedCharacterProperty extends RegExpTerm, TRegExpNamedCharacterProp
predicate isInverted() { re.namedCharacterPropertyIsInverted(start, end) }
}
/** Gets the parse tree resulting from parsing `re`, if such has been constructed. */
RegExpTerm getParsedRegExp(AST::RegExpLiteral re) {
result.getRegExp() = re and result.isRootTerm()
}
/**
* A node whose value may flow to a position where it is interpreted
* as a part of a regular expression.
*/
abstract class RegExpPatternSource extends DataFlow::Node {
/**
* Gets a node where the pattern of this node is parsed as a part of
* a regular expression.
*/
abstract DataFlow::Node getAParse();
/**
* Gets the root term of the regular expression parsed from this pattern.
*/
abstract RegExpTerm getRegExpTerm();
}
/**
* A regular expression literal, viewed as the pattern source for itself.
*/
private class RegExpLiteralPatternSource extends RegExpPatternSource {
private AST::RegExpLiteral astNode;
RegExpLiteralPatternSource() { astNode = this.asExpr().getExpr() }
override DataFlow::Node getAParse() { result = this }
override RegExpTerm getRegExpTerm() { result = astNode.getParsed() }
}
/**
* A node whose string value may flow to a position where it is interpreted
* as a part of a regular expression.
*/
private class StringRegExpPatternSource extends RegExpPatternSource {
private DataFlow::Node parse;
StringRegExpPatternSource() { this = regExpSource(parse) }
override DataFlow::Node getAParse() { result = parse }
override RegExpTerm getRegExpTerm() { result.getRegExp() = this.asExpr().getExpr() }
}

View File

@@ -5,12 +5,8 @@
* the `x` (free-spacing) flag.
*/
private import codeql.ruby.ast.Literal as AST
private import codeql.ruby.AST as AST
private import codeql.Locations
private import codeql.ruby.DataFlow
private import codeql.ruby.TaintTracking
private import codeql.ruby.typetracking.TypeTracker
private import codeql.ruby.ApiGraphs
/**
* A `StringlikeLiteral` containing a regular expression term, that is, either
@@ -116,6 +112,7 @@ abstract class RegExp extends AST::StringlikeLiteral {
)
}
/** Holds if a character set starts between `start` and `end`. */
predicate charSetStart(int start, int end) {
this.charSetStart(start) = true and
(
@@ -145,14 +142,21 @@ abstract class RegExp extends AST::StringlikeLiteral {
)
}
predicate charSetToken(int charsetStart, int index, int tokenStart, int tokenEnd) {
/**
* Holds if the character set starting at `charsetStart` contains either
* a character or a `-` found between `start` and `end`.
*/
private predicate charSetToken(int charsetStart, int index, int tokenStart, int tokenEnd) {
tokenStart =
rank[index](int start, int end | this.charSetToken(charsetStart, start, end) | start) and
this.charSetToken(charsetStart, tokenStart, tokenEnd)
}
/** Either a char or a - */
predicate charSetToken(int charsetStart, int start, int end) {
/**
* Holds if the character set starting at `charsetStart` contains either
* a character or a `-` found between `start` and `end`.
*/
private predicate charSetToken(int charsetStart, int start, int end) {
this.charSetStart(charsetStart, start) and
(
this.escapedCharacter(start, end)
@@ -174,6 +178,10 @@ abstract class RegExp extends AST::StringlikeLiteral {
)
}
/**
* Holds if the character set starting at `charsetStart` contains either
* a character or a range found between `start` and `end`.
*/
predicate charSetChild(int charsetStart, int start, int end) {
this.charSetToken(charsetStart, start, end) and
not exists(int rangeStart, int rangeEnd |
@@ -185,6 +193,11 @@ abstract class RegExp extends AST::StringlikeLiteral {
this.charRange(charsetStart, start, _, _, end)
}
/**
* Holds if the character set starting at `charset_start` contains a character range
* with lower bound found between `start` and `lower_end`
* and upper bound found between `upper_start` and `end`.
*/
predicate charRange(int charsetStart, int start, int lowerEnd, int upperStart, int end) {
exists(int index |
this.charRangeEnd(charsetStart, index) = true and
@@ -193,6 +206,13 @@ abstract class RegExp extends AST::StringlikeLiteral {
)
}
/**
* Helper predicate for `charRange`.
* We can determine where character ranges end by a left to right sweep.
*
* To avoid negative recursion we return a boolean. See `escaping`,
* the helper for `escapingChar`, for a clean use of this pattern.
*/
private boolean charRangeEnd(int charsetStart, int index) {
this.charSetToken(charsetStart, index, _, _) and
(
@@ -216,8 +236,15 @@ abstract class RegExp extends AST::StringlikeLiteral {
)
}
/** Holds if the character at `pos` is a "\" that is actually escaping what comes after. */
predicate escapingChar(int pos) { this.escaping(pos) = true }
/**
* Helper predicate for `escapingChar`.
* In order to avoid negative recusrion, we return a boolean.
* This way, we can refer to `escaping(pos - 1).booleanNot()`
* rather than to a negated version of `escaping(pos)`.
*/
private boolean escaping(int pos) {
pos = -1 and result = false
or
@@ -227,10 +254,16 @@ abstract class RegExp extends AST::StringlikeLiteral {
}
/** Gets the text of this regex */
string getText() { result = this.getConstantValue().getString() }
string getText() {
exists(AST::ConstantValue c | c = this.getConstantValue() |
result = [this.getConstantValue().getString(), this.getConstantValue().getRegExp()]
)
}
/** Gets the `i`th character of this regex */
string getChar(int i) { result = this.getText().charAt(i) }
/** Gets the `i`th character of this regex, unless it is part of a character escape sequence. */
string nonEscapedCharAt(int i) {
result = this.getText().charAt(i) and
not exists(int x, int y | this.escapedCharacter(x, y) and i in [x .. y - 1])
@@ -242,6 +275,9 @@ abstract class RegExp extends AST::StringlikeLiteral {
private predicate isGroupStart(int i) { this.nonEscapedCharAt(i) = "(" and not this.inCharSet(i) }
/**
* Holds if the `i`th character could not be parsed.
*/
predicate failedToParse(int i) {
exists(this.getChar(i)) and
not exists(int start, int end |
@@ -331,6 +367,11 @@ abstract class RegExp extends AST::StringlikeLiteral {
this.getChar(start + 3) = "^"
}
/**
* Holds if an escaped character is found between `start` and `end`.
* Escaped characters include hex values, octal values and named escapes,
* but excludes backreferences.
*/
predicate escapedCharacter(int start, int end) {
this.escapingChar(start) and
not this.numberedBackreference(start, _, _) and
@@ -350,17 +391,25 @@ abstract class RegExp extends AST::StringlikeLiteral {
)
}
/**
* Holds if the character at `index` is inside a character set.
*/
predicate inCharSet(int index) {
exists(int x, int y | this.charSet(x, y) and index in [x + 1 .. y - 2])
}
/**
* Holds if the character at `index` is inside a posix bracket.
*/
predicate inPosixBracket(int index) {
exists(int x, int y |
this.posixStyleNamedCharacterProperty(x, y, _) and index in [x + 1 .. y - 2]
)
}
/** 'Simple' characters are any that don't alter the parsing of the regex. */
/**
* 'simple' characters are any that don't alter the parsing of the regex.
*/
private predicate simpleCharacter(int start, int end) {
end = start + 1 and
not this.charSet(start, _) and
@@ -391,6 +440,9 @@ abstract class RegExp extends AST::StringlikeLiteral {
)
}
/**
* Holds if a simple or escaped character is found between `start` and `end`.
*/
predicate character(int start, int end) {
(
this.simpleCharacter(start, end) and
@@ -406,12 +458,18 @@ abstract class RegExp extends AST::StringlikeLiteral {
not exists(int x, int y | this.multiples(x, y, _, _) and x <= start and y >= end)
}
/**
* Holds if a normal character is found between `start` and `end`.
*/
predicate normalCharacter(int start, int end) {
end = start + 1 and
this.character(start, end) and
not this.specialCharacter(start, end, _)
}
/**
* Holds if a special character is found between `start` and `end`.
*/
predicate specialCharacter(int start, int end, string char) {
this.character(start, end) and
not this.inCharSet(start) and
@@ -480,6 +538,7 @@ abstract class RegExp extends AST::StringlikeLiteral {
/** Gets the number of the group in start,end */
int getGroupNumber(int start, int end) {
this.group(start, end) and
not this.nonCapturingGroupStart(start, _) and
result =
count(int i | this.group(i, _) and i < start and not this.nonCapturingGroupStart(i, _)) + 1
}
@@ -504,6 +563,7 @@ abstract class RegExp extends AST::StringlikeLiteral {
this.positiveLookbehindAssertionGroup(start, end)
}
/** Holds if an empty group is found between `start` and `end`. */
predicate emptyGroup(int start, int end) {
exists(int endm1 | end = endm1 + 1 |
this.groupStart(start, endm1) and
@@ -537,24 +597,28 @@ abstract class RegExp extends AST::StringlikeLiteral {
)
}
/** Holds if a negative lookahead is found between `start` and `end` */
predicate negativeLookaheadAssertionGroup(int start, int end) {
exists(int inStart | this.negativeLookaheadAssertionStart(start, inStart) |
this.groupContents(start, end, inStart, _)
)
}
/** Holds if a negative lookbehind is found between `start` and `end` */
predicate negativeLookbehindAssertionGroup(int start, int end) {
exists(int inStart | this.negativeLookbehindAssertionStart(start, inStart) |
this.groupContents(start, end, inStart, _)
)
}
/** Holds if a positive lookahead is found between `start` and `end` */
predicate positiveLookaheadAssertionGroup(int start, int end) {
exists(int inStart | this.lookaheadAssertionStart(start, inStart) |
this.groupContents(start, end, inStart, _)
)
}
/** Holds if a positive lookbehind is found between `start` and `end` */
predicate positiveLookbehindAssertionGroup(int start, int end) {
exists(int inStart | this.lookbehindAssertionStart(start, inStart) |
this.groupContents(start, end, inStart, _)
@@ -583,7 +647,7 @@ abstract class RegExp extends AST::StringlikeLiteral {
private predicate nonCapturingGroupStart(int start, int end) {
this.isGroupStart(start) and
this.getChar(start + 1) = "?" and
this.getChar(start + 2) = ":" and
this.getChar(start + 2) = [":", "=", "<", "!", "#"] and
end = start + 3
}
@@ -660,6 +724,7 @@ abstract class RegExp extends AST::StringlikeLiteral {
end = start + 3
}
/** Matches the contents of a group. */
predicate groupContents(int start, int end, int inStart, int inEnd) {
this.groupStart(start, inStart) and
end = inEnd + 1 and
@@ -746,6 +811,11 @@ abstract class RegExp extends AST::StringlikeLiteral {
)
}
/**
* Holds if a repetition quantifier is found between `start` and `end`,
* with the given lower and upper bounds. If a bound is omitted, the corresponding
* string is empty.
*/
predicate multiples(int start, int end, string lower, string upper) {
exists(string text, string match, string inner |
text = this.getText() and
@@ -773,6 +843,13 @@ abstract class RegExp extends AST::StringlikeLiteral {
this.qualifiedPart(start, _, end, maybeEmpty, mayRepeatForever)
}
/**
* Holds if a qualified part is found between `start` and `part_end` and the qualifier is
* found between `part_end` and `end`.
*
* `maybe_empty` is true if the part is optional.
* `may_repeat_forever` is true if the part may be repeated unboundedly.
*/
predicate qualifiedPart(
int start, int partEnd, int end, boolean maybeEmpty, boolean mayRepeatForever
) {
@@ -780,6 +857,7 @@ abstract class RegExp extends AST::StringlikeLiteral {
this.qualifier(partEnd, end, maybeEmpty, mayRepeatForever)
}
/** Holds if the range `start`, `end` contains a character, a quantifier, a character set or a group. */
predicate item(int start, int end) {
this.qualifiedItem(start, end, _, _)
or
@@ -959,63 +1037,3 @@ abstract class RegExp extends AST::StringlikeLiteral {
this.lastPart(start, end)
}
}
private class RegExpLiteralRegExp extends RegExp, AST::RegExpLiteral {
override predicate isDotAll() { this.hasMultilineFlag() }
override predicate isIgnoreCase() { this.hasCaseInsensitiveFlag() }
override string getFlags() { result = this.getFlagString() }
}
private class ParsedStringRegExp extends RegExp {
private DataFlow::Node parse;
ParsedStringRegExp() { this = regExpSource(parse).asExpr().getExpr() }
DataFlow::Node getAParse() { result = parse }
override predicate isDotAll() { none() }
override predicate isIgnoreCase() { none() }
override string getFlags() { none() }
}
/**
* Holds if `source` may be interpreted as a regular expression.
*/
private predicate isInterpretedAsRegExp(DataFlow::Node source) {
// The first argument to an invocation of `Regexp.new` or `Regexp.compile`.
source = API::getTopLevelMember("Regexp").getAMethodCall(["compile", "new"]).getArgument(0)
or
// The argument of a call that coerces the argument to a regular expression.
exists(DataFlow::CallNode mce |
mce.getMethodName() = ["match", "match?"] and
source = mce.getArgument(0)
)
}
/**
* Gets a node whose value may flow (inter-procedurally) to `re`, where it is interpreted
* as a part of a regular expression.
*/
private DataFlow::Node regExpSource(DataFlow::Node re, TypeBackTracker t) {
t.start() and
re = result and
isInterpretedAsRegExp(result)
or
exists(TypeBackTracker t2, DataFlow::Node succ | succ = regExpSource(re, t2) |
t2 = t.smallstep(result, succ)
or
TaintTracking::localTaintStep(result, succ) and
t = t2
)
}
/**
* Gets a node whose value may flow (inter-procedurally) to `re`, where it is interpreted
* as a part of a regular expression.
*/
cached
DataFlow::Node regExpSource(DataFlow::Node re) { result = regExpSource(re, TypeBackTracker::end()) }

View File

@@ -176,8 +176,8 @@ private module RegexpMatching {
}
/** A class to test whether a regular expression matches certain HTML tags. */
class HTMLMatchingRegExp extends RegexpMatching::MatchedRegExp {
HTMLMatchingRegExp() {
class HtmlMatchingRegExp extends RegexpMatching::MatchedRegExp {
HtmlMatchingRegExp() {
// the regexp must mention "<" and ">" explicitly.
forall(string angleBracket | angleBracket = ["<", ">"] |
any(RegExpConstant term | term.getValue().matches("%" + angleBracket + "%")).getRootTerm() =
@@ -204,12 +204,15 @@ class HTMLMatchingRegExp extends RegexpMatching::MatchedRegExp {
}
}
/** DEPRECATED: Alias for HtmlMatchingRegExp */
deprecated class HTMLMatchingRegExp = HtmlMatchingRegExp;
/**
* Holds if `regexp` matches some HTML tags, but misses some HTML tags that it should match.
*
* When adding a new case to this predicate, make sure the test string used in `matches(..)` calls are present in `HTMLMatchingRegExp::test` / `HTMLMatchingRegExp::testWithGroups`.
*/
predicate isBadRegexpFilter(HTMLMatchingRegExp regexp, string msg) {
predicate isBadRegexpFilter(HtmlMatchingRegExp regexp, string msg) {
// CVE-2021-33829 - matching both "<!-- foo -->" and "<!-- foo --!>", but in different capture groups
regexp.matches("<!-- foo -->") and
regexp.matches("<!-- foo --!>") and

View File

@@ -6,12 +6,8 @@
private import ruby
private import codeql.ruby.DataFlow
private import codeql.ruby.TaintTracking::TaintTracking
private import codeql.ruby.Concepts
private import codeql.ruby.dataflow.RemoteFlowSources
private import internal.SensitiveDataHeuristics::HeuristicNames
private import codeql.ruby.CFG
private import codeql.ruby.dataflow.SSA
private import internal.CleartextSources
/**
* Provides default sources, sinks and sanitizers for reasoning about
@@ -22,265 +18,25 @@ module CleartextLogging {
/**
* A data flow source for cleartext logging of sensitive information.
*/
abstract class Source extends DataFlow::Node {
/** Gets a string that describes the type of this data flow source. */
abstract string describe();
}
class Source = CleartextSources::Source;
/**
* A sanitizer for cleartext logging of sensitive information.
*/
class Sanitizer = CleartextSources::Sanitizer;
/** Holds if `nodeFrom` taints `nodeTo`. */
predicate isAdditionalTaintStep = CleartextSources::isAdditionalTaintStep/2;
/**
* A data flow sink for cleartext logging of sensitive information.
*/
abstract class Sink extends DataFlow::Node { }
/**
* A sanitizer for cleartext logging of sensitive information.
*/
abstract class Sanitizer extends DataFlow::Node { }
/**
* Holds if `re` may be a regular expression that can be used to sanitize
* sensitive data with a call to `sub`.
*/
private predicate effectiveSubRegExp(CfgNodes::ExprNodes::RegExpLiteralCfgNode re) {
re.getConstantValue().getStringOrSymbol().matches([".*", ".+"])
}
/**
* Holds if `re` may be a regular expression that can be used to sanitize
* sensitive data with a call to `gsub`.
*/
private predicate effectiveGsubRegExp(CfgNodes::ExprNodes::RegExpLiteralCfgNode re) {
re.getConstantValue().getStringOrSymbol().matches(".")
}
/**
* A call to `sub`/`sub!` or `gsub`/`gsub!` that seems to mask sensitive information.
*/
private class MaskingReplacerSanitizer extends Sanitizer, DataFlow::CallNode {
MaskingReplacerSanitizer() {
exists(CfgNodes::ExprNodes::RegExpLiteralCfgNode re |
re = this.getArgument(0).asExpr() and
(
this.getMethodName() = ["sub", "sub!"] and effectiveSubRegExp(re)
or
this.getMethodName() = ["gsub", "gsub!"] and effectiveGsubRegExp(re)
)
)
}
}
/**
* Like `MaskingReplacerSanitizer` but updates the receiver for methods that
* sanitize the receiver.
* Taint is thereby cleared for any subsequent read.
*/
private class InPlaceMaskingReplacerSanitizer extends Sanitizer {
InPlaceMaskingReplacerSanitizer() {
exists(MaskingReplacerSanitizer m | m.getMethodName() = ["gsub!", "sub!"] |
m.getReceiver() = this
)
}
}
/**
* Holds if `name` is for a method or variable that appears, syntactically, to
* not be sensitive.
*/
bindingset[name]
private predicate nameIsNotSensitive(string name) {
name.regexpMatch(notSensitiveRegexp()) and
// By default `notSensitiveRegexp()` includes some false positives for
// common ruby method names that are not necessarily non-sensitive.
// We explicitly exclude element references, element assignments, and
// mutation methods.
not name = ["[]", "[]="] and
not name.matches("%!")
}
/**
* A call that might obfuscate a password, for example through hashing.
*/
private class ObfuscatorCall extends Sanitizer, DataFlow::CallNode {
ObfuscatorCall() { nameIsNotSensitive(this.getMethodName()) }
}
/**
* A data flow node that does not contain a clear-text password, according to its syntactic name.
*/
private class NameGuidedNonCleartextPassword extends NonCleartextPassword {
NameGuidedNonCleartextPassword() {
exists(string name | nameIsNotSensitive(name) |
// accessing a non-sensitive variable
this.asExpr().getExpr().(VariableReadAccess).getVariable().getName() = name
or
// dereferencing a non-sensitive field
this.asExpr()
.(CfgNodes::ExprNodes::ElementReferenceCfgNode)
.getArgument(0)
.getConstantValue()
.getStringOrSymbol() = name
or
// calling a non-sensitive method
this.(DataFlow::CallNode).getMethodName() = name
)
or
// avoid i18n strings
this.asExpr()
.(CfgNodes::ExprNodes::ElementReferenceCfgNode)
.getReceiver()
.getConstantValue()
.getStringOrSymbol()
.regexpMatch("(?is).*(messages|strings).*")
}
}
/**
* A data flow node that receives flow that is not a clear-text password.
*/
private class NonCleartextPasswordFlow extends NonCleartextPassword {
NonCleartextPasswordFlow() {
any(NonCleartextPassword other).(DataFlow::LocalSourceNode).flowsTo(this)
}
}
/**
* A data flow node that does not contain a clear-text password.
*/
abstract private class NonCleartextPassword extends DataFlow::Node { }
// `writeNode` assigns pair with key `name` to `val`
private predicate hashKeyWrite(DataFlow::CallNode writeNode, string name, DataFlow::Node val) {
writeNode.asExpr().getExpr() instanceof SetterMethodCall and
// hash[name]
writeNode.getArgument(0).asExpr().getConstantValue().getStringOrSymbol() = name and
// val
writeNode.getArgument(1).asExpr().(CfgNodes::ExprNodes::AssignExprCfgNode).getRhs() =
val.asExpr()
}
/**
* A write to a hash entry with a value that may contain password information.
*/
private class HashKeyWritePasswordSource extends Source {
private string name;
private DataFlow::ExprNode recv;
HashKeyWritePasswordSource() {
exists(DataFlow::Node val |
name.regexpMatch(maybePassword()) and
not nameIsNotSensitive(name) and
// avoid safe values assigned to presumably unsafe names
not val instanceof NonCleartextPassword and
(
// hash[name] = val
hashKeyWrite(this, name, val) and
recv = this.(DataFlow::CallNode).getReceiver()
)
)
}
override string describe() { result = "a write to " + name }
/** Gets the name of the key */
string getName() { result = name }
/**
* Gets the name of the hash variable that this password source is assigned
* to, if applicable.
*/
LocalVariable getVariable() {
result = recv.getExprNode().getExpr().(VariableReadAccess).getVariable()
}
}
/**
* A hash literal with an entry that may contain a password
*/
private class HashLiteralPasswordSource extends Source {
private string name;
HashLiteralPasswordSource() {
exists(DataFlow::Node val, CfgNodes::ExprNodes::HashLiteralCfgNode lit |
name.regexpMatch(maybePassword()) and
not name.regexpMatch(notSensitiveRegexp()) and
// avoid safe values assigned to presumably unsafe names
not val instanceof NonCleartextPassword and
// hash = { name: val }
exists(CfgNodes::ExprNodes::PairCfgNode p |
this.asExpr() = lit and p = lit.getAKeyValuePair()
|
p.getKey().getConstantValue().getStringOrSymbol() = name and
p.getValue() = val.asExpr()
)
)
}
override string describe() { result = "an write to " + name }
}
/** An assignment that may assign a password to a variable */
private class AssignPasswordVariableSource extends Source {
string name;
AssignPasswordVariableSource() {
// avoid safe values assigned to presumably unsafe names
not this instanceof NonCleartextPassword and
name.regexpMatch(maybePassword()) and
exists(Assignment a |
this.asExpr().getExpr() = a.getRightOperand() and
a.getLeftOperand().getAVariable().getName() = name
)
}
override string describe() { result = "an assignment to " + name }
}
/** A parameter that may contain a password. */
private class ParameterPasswordSource extends Source {
private string name;
ParameterPasswordSource() {
name.regexpMatch(maybePassword()) and
not this instanceof NonCleartextPassword and
exists(Parameter p, LocalVariable v |
v = p.getAVariable() and
v.getName() = name and
this.asExpr().getExpr() = v.getAnAccess()
)
}
override string describe() { result = "a parameter " + name }
}
/** A call that might return a password. */
private class CallPasswordSource extends DataFlow::CallNode, Source {
private string name;
CallPasswordSource() {
name = this.getMethodName() and
name.regexpMatch("(?is)getPassword")
}
override string describe() { result = "a call to " + name }
}
private string commonLogMethodName() {
result = ["info", "debug", "warn", "warning", "error", "log"]
}
/** Holds if `nodeFrom` taints `nodeTo`. */
predicate isAdditionalTaintStep(DataFlow::Node nodeFrom, DataFlow::Node nodeTo) {
exists(string name, ElementReference ref, LocalVariable hashVar |
// from `hsh[password] = "changeme"` to a `hsh[password]` read
nodeFrom.(HashKeyWritePasswordSource).getName() = name and
nodeTo.asExpr().getExpr() = ref and
ref.getArgument(0).getConstantValue().getStringOrSymbol() = name and
nodeFrom.(HashKeyWritePasswordSource).getVariable() = hashVar and
ref.getReceiver().(VariableReadAccess).getVariable() = hashVar and
nodeFrom.asExpr().getASuccessor*() = nodeTo.asExpr()
)
}
/**
* A node representing an expression whose value is logged.
*/

View File

@@ -0,0 +1,49 @@
/**
* Provides default sources, sinks and sanitizers for reasoning about
* cleartext storage of sensitive information, as well as extension points for
* adding your own.
*/
private import ruby
private import codeql.ruby.DataFlow
private import codeql.ruby.Concepts
private import internal.CleartextSources
/**
* Provides default sources, sinks and sanitizers for reasoning about
* cleartext storage of sensitive information, as well as extension points for
* adding your own.
*/
module CleartextStorage {
/**
* A data flow source for cleartext storage of sensitive information.
*/
class Source = CleartextSources::Source;
/**
* A sanitizer for cleartext storage of sensitive information.
*/
class Sanitizer = CleartextSources::Sanitizer;
/** Holds if `nodeFrom` taints `nodeTo`. */
predicate isAdditionalTaintStep = CleartextSources::isAdditionalTaintStep/2;
/**
* A data flow sink for cleartext storage of sensitive information.
*/
abstract class Sink extends DataFlow::Node { }
/**
* A node representing data written to the filesystem.
*/
private class FileSystemWriteAccessDataNodeAsSink extends Sink {
FileSystemWriteAccessDataNodeAsSink() { this = any(FileSystemWriteAccess write).getADataNode() }
}
/**
* A node representing data written to a persistent data store.
*/
private class PersistentWriteAccessAsSink extends Sink {
PersistentWriteAccessAsSink() { this = any(PersistentWriteAccess write).getValue() }
}
}

View File

@@ -0,0 +1,33 @@
/**
* Provides a taint-tracking configuration for "Clear-text storage of sensitive information".
*
* Note, for performance reasons: only import this file if
* `Configuration` is needed, otherwise `CleartextStorageCustomizations` should be
* imported instead.
*/
private import ruby
private import codeql.ruby.DataFlow
private import codeql.ruby.TaintTracking
private import CleartextStorageCustomizations::CleartextStorage as CleartextStorage
/**
* A taint-tracking configuration for detecting "Clear-text storage of sensitive information".
*/
class Configuration extends TaintTracking::Configuration {
Configuration() { this = "CleartextStorage" }
override predicate isSource(DataFlow::Node source) { source instanceof CleartextStorage::Source }
override predicate isSink(DataFlow::Node sink) { sink instanceof CleartextStorage::Sink }
override predicate isSanitizer(DataFlow::Node node) {
super.isSanitizer(node)
or
node instanceof CleartextStorage::Sanitizer
}
override predicate isAdditionalTaintStep(DataFlow::Node nodeFrom, DataFlow::Node nodeTo) {
CleartextStorage::isAdditionalTaintStep(nodeFrom, nodeTo)
}
}

View File

@@ -0,0 +1,34 @@
/**
* Provides default sources, sinks and sanitizers for reasoning about
* writing user-controlled data to files, as well as extension points
* for adding your own.
*/
/**
* Provides default sources, sinks and sanitizers for reasoning about
* writing user-controlled data to files, as well as extension points
* for adding your own.
*/
module HttpToFileAccess {
import HttpToFileAccessSpecific
/**
* A data flow source for writing user-controlled data to files.
*/
abstract class Source extends DataFlow::Node { }
/**
* A data flow sink for writing user-controlled data to files.
*/
abstract class Sink extends DataFlow::Node { }
/**
* A sanitizer for writing user-controlled data to files.
*/
abstract class Sanitizer extends DataFlow::Node { }
/** A sink that represents file access method (write, append) argument */
class FileAccessAsSink extends Sink {
FileAccessAsSink() { exists(FileSystemWriteAccess src | this = src.getADataNode()) }
}
}

View File

@@ -0,0 +1,25 @@
/**
* Provides a taint tracking configuration for reasoning about writing user-controlled data to files.
*
* Note, for performance reasons: only import this file if
* `HttpToFileAccess::Configuration` is needed, otherwise
* `HttpToFileAccessCustomizations` should be imported instead.
*/
private import HttpToFileAccessCustomizations::HttpToFileAccess
/**
* A taint tracking configuration for writing user-controlled data to files.
*/
class Configuration extends TaintTracking::Configuration {
Configuration() { this = "HttpToFileAccess" }
override predicate isSource(DataFlow::Node source) { source instanceof Source }
override predicate isSink(DataFlow::Node sink) { sink instanceof Sink }
override predicate isSanitizer(DataFlow::Node node) {
super.isSanitizer(node) or
node instanceof Sanitizer
}
}

View File

@@ -0,0 +1,21 @@
/**
* Provides imports and classes needed for `HttpToFileAccessQuery` and `HttpToFileAccessCustomizations`.
*/
import ruby
import codeql.ruby.DataFlow
import codeql.ruby.dataflow.RemoteFlowSources
import codeql.ruby.Concepts
import codeql.ruby.TaintTracking
private import HttpToFileAccessCustomizations::HttpToFileAccess
/**
* An access to a user-controlled HTTP request input, considered as a flow source for writing user-controlled data to files
*/
private class RequestInputAccessAsSource extends Source instanceof HTTP::Server::RequestInputAccess {
}
/** A response from an outgoing HTTP request, considered as a flow source for writing user-controlled data to files. */
private class HttpResponseAsSource extends Source {
HttpResponseAsSource() { this = any(HTTP::Client::Request r).getResponseBody() }
}

View File

@@ -13,8 +13,8 @@ import codeql.ruby.TaintTracking
/**
* Provides a taint-tracking configuration for detecting "reflected server-side cross-site scripting" vulnerabilities.
*/
module ReflectedXSS {
import XSS::ReflectedXSS
module ReflectedXss {
import XSS::ReflectedXss
/**
* A taint-tracking configuration for detecting "reflected server-side cross-site scripting" vulnerabilities.
@@ -33,7 +33,10 @@ module ReflectedXSS {
}
override predicate isAdditionalTaintStep(DataFlow::Node node1, DataFlow::Node node2) {
isAdditionalXSSTaintStep(node1, node2)
isAdditionalXssTaintStep(node1, node2)
}
}
}
/** DEPRECATED: Alias for ReflectedXss */
deprecated module ReflectedXSS = ReflectedXss;

View File

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

View File

@@ -11,8 +11,9 @@ import ruby
import codeql.ruby.DataFlow
import codeql.ruby.TaintTracking
module StoredXSS {
import XSS::StoredXSS
/** Provides a taint-tracking configuration for cross-site scripting vulnerabilities. */
module StoredXss {
import XSS::StoredXss
/**
* A taint-tracking configuration for reasoning about Stored XSS.
@@ -34,7 +35,10 @@ module StoredXSS {
}
override predicate isAdditionalTaintStep(DataFlow::Node node1, DataFlow::Node node2) {
isAdditionalXSSTaintStep(node1, node2)
isAdditionalXssTaintStep(node1, node2)
}
}
}
/** DEPRECATED: Alias for StoredXss */
deprecated module StoredXSS = StoredXss;

View File

@@ -0,0 +1,43 @@
/**
* Provides default sources, sinks and sanitizers for reasoning about
* format injections, as well as extension points for adding your own.
*/
/**
* Provides default sources, sinks and sanitizers for reasoning about
* format injections, as well as extension points for adding your own.
*/
module TaintedFormatString {
import TaintedFormatStringSpecific
/**
* A data flow source for format injections.
*/
abstract class Source extends DataFlow::Node { }
/**
* A data flow sink for format injections.
*/
abstract class Sink extends DataFlow::Node { }
/**
* A sanitizer for format injections.
*/
abstract class Sanitizer extends DataFlow::Node { }
/** A source of remote user input, considered as a flow source for format injection. */
class RemoteSource extends Source instanceof RemoteFlowSource { }
/**
* A format argument to a printf-like function, considered as a flow sink for format injection.
*/
class FormatSink extends Sink {
FormatSink() {
exists(PrintfStyleCall printf |
this = printf.getFormatString() and
// exclude trivial case where there are no arguments to interpolate
exists(printf.getFormatArgument(_))
)
}
}
}

View File

@@ -0,0 +1,27 @@
/**
* Provides a taint-tracking configuration for reasoning about format
* injections.
*
*
* Note, for performance reasons: only import this file if
* `TaintedFormatString::Configuration` is needed, otherwise
* `TaintedFormatStringCustomizations` should be imported instead.
*/
private import TaintedFormatStringCustomizations::TaintedFormatString
/**
* A taint-tracking configuration for format injections.
*/
class Configuration extends TaintTracking::Configuration {
Configuration() { this = "TaintedFormatString" }
override predicate isSource(DataFlow::Node source) { source instanceof Source }
override predicate isSink(DataFlow::Node sink) { sink instanceof Sink }
override predicate isSanitizer(DataFlow::Node node) {
super.isSanitizer(node) or
node instanceof Sanitizer
}
}

View File

@@ -0,0 +1,71 @@
/**
* Provides Ruby-specific imports and classes needed for `TaintedFormatStringQuery` and `TaintedFormatStringCustomizations`.
*/
import ruby
import codeql.ruby.DataFlow
import codeql.ruby.dataflow.RemoteFlowSources
import codeql.ruby.ApiGraphs
import codeql.ruby.TaintTracking
private import codeql.ruby.frameworks.Files::IO
private import codeql.ruby.controlflow.CfgNodes
/**
* A call to `printf` or `sprintf`.
*/
abstract class PrintfStyleCall extends DataFlow::CallNode {
// We assume that most printf-like calls have the signature f(format_string, args...)
/**
* Gets the format string of this call.
*/
DataFlow::Node getFormatString() { result = this.getArgument(0) }
/**
* Gets then `n`th formatted argument of this call.
*/
DataFlow::Node getFormatArgument(int n) { n >= 0 and result = this.getArgument(n + 1) }
}
/**
* A call to `Kernel.printf`.
*/
class KernelPrintfCall extends PrintfStyleCall {
KernelPrintfCall() {
this = API::getTopLevelMember("Kernel").getAMethodCall("printf")
or
this.asExpr().getExpr() instanceof UnknownMethodCall and
this.getMethodName() = "printf"
}
// Kernel#printf supports two signatures:
// printf(io, string, ...)
// printf(string, ...)
override DataFlow::Node getFormatString() {
// Because `printf` has two different signatures, we can't be sure which
// argument is the format string, so we use a heuristic:
// If the first argument has a string value, then we assume it is the format string.
// Otherwise we treat both the first and second args as the format string.
if this.getArgument(0).getExprNode().getConstantValue().isString(_)
then result = this.getArgument(0)
else result = this.getArgument([0, 1])
}
}
/**
* A call to `Kernel.sprintf`.
*/
class KernelSprintfCall extends PrintfStyleCall {
KernelSprintfCall() {
this = API::getTopLevelMember("Kernel").getAMethodCall("sprintf")
or
this.asExpr().getExpr() instanceof UnknownMethodCall and
this.getMethodName() = "sprintf"
}
}
/**
* A call to `IO#printf`.
*/
class IOPrintfCall extends PrintfStyleCall {
IOPrintfCall() { this.getReceiver() instanceof IOInstance and this.getMethodName() = "printf" }
}

View File

@@ -74,7 +74,7 @@ module UnsafeDeserialization {
}
private predicate isOjModePair(CfgNodes::ExprNodes::PairCfgNode p, string modeValue) {
p.getKey().getConstantValue().isStringOrSymbol("mode") and
p.getKey().getConstantValue().isStringlikeValue("mode") and
exists(DataFlow::LocalSourceNode symbolLiteral, DataFlow::Node value |
symbolLiteral.asExpr().getExpr().getConstantValue().isSymbol(modeValue) and
symbolLiteral.flowsTo(value) and

View File

@@ -141,7 +141,7 @@ private module Shared {
exists(RenderCall call, Pair kvPair |
call.getLocals().getAKeyValuePair() = kvPair and
kvPair.getValue() = value and
kvPair.getKey().getConstantValue().isStringOrSymbol(hashKey) and
kvPair.getKey().getConstantValue().isStringlikeValue(hashKey) and
call.getTemplateFile() = erb
)
}
@@ -154,7 +154,7 @@ private module Shared {
argNode.asExpr() = refNode.getArgument(0) and
refNode.getReceiver().getExpr().(MethodCall).getMethodName() = "local_assigns" and
argNode.getALocalSource() = DataFlow::exprNode(strNode) and
strNode.getExpr().getConstantValue().isStringOrSymbol(hashKey) and
strNode.getExpr().getConstantValue().isStringlikeValue(hashKey) and
erb = refNode.getFile()
)
}
@@ -245,7 +245,7 @@ private module Shared {
/**
* An additional step that is preserves dataflow in the context of XSS.
*/
predicate isAdditionalXSSFlowStep(DataFlow::Node node1, DataFlow::Node node2) {
predicate isAdditionalXssFlowStep(DataFlow::Node node1, DataFlow::Node node2) {
isFlowFromLocals(node1, node2)
or
isFlowFromControllerInstanceVariable(node1, node2)
@@ -254,6 +254,9 @@ private module Shared {
or
isFlowFromHelperMethod(node1, node2)
}
/** DEPRECATED: Alias for isAdditionalXssFlowStep */
deprecated predicate isAdditionalXSSFlowStep = isAdditionalXssFlowStep/2;
}
/**
@@ -261,7 +264,7 @@ private module Shared {
* "reflected cross-site scripting" vulnerabilities, as well as
* extension points for adding your own.
*/
module ReflectedXSS {
module ReflectedXss {
/** A data flow source for stored XSS vulnerabilities. */
abstract class Source extends Shared::Source { }
@@ -277,7 +280,10 @@ module ReflectedXSS {
/**
* An additional step that is preserves dataflow in the context of reflected XSS.
*/
predicate isAdditionalXSSTaintStep = Shared::isAdditionalXSSFlowStep/2;
predicate isAdditionalXssTaintStep = Shared::isAdditionalXssFlowStep/2;
/** DEPRECATED: Alias for isAdditionalXssTaintStep */
deprecated predicate isAdditionalXSSTaintStep = isAdditionalXssTaintStep/2;
/**
* A source of remote user input, considered as a flow source.
@@ -285,6 +291,9 @@ module ReflectedXSS {
class RemoteFlowSourceAsSource extends Source, RemoteFlowSource { }
}
/** DEPRECATED: Alias for ReflectedXss */
deprecated module ReflectedXSS = ReflectedXss;
private module OrmTracking {
/**
* A data flow configuration to track flow from finder calls to field accesses.
@@ -298,7 +307,7 @@ private module OrmTracking {
override predicate isSink(DataFlow2::Node sink) { sink instanceof DataFlow2::CallNode }
override predicate isAdditionalFlowStep(DataFlow2::Node node1, DataFlow2::Node node2) {
Shared::isAdditionalXSSFlowStep(node1, node2)
Shared::isAdditionalXssFlowStep(node1, node2)
or
// Propagate flow through arbitrary method calls
node2.(DataFlow2::CallNode).getReceiver() = node1
@@ -309,7 +318,8 @@ private module OrmTracking {
}
}
module StoredXSS {
/** Provides default sources, sinks and sanitizers for detecting stored cross-site scripting (XSS) vulnerabilities. */
module StoredXss {
/** A data flow source for stored XSS vulnerabilities. */
abstract class Source extends Shared::Source { }
@@ -325,7 +335,10 @@ module StoredXSS {
/**
* An additional step that preserves dataflow in the context of stored XSS.
*/
predicate isAdditionalXSSTaintStep = Shared::isAdditionalXSSFlowStep/2;
predicate isAdditionalXssTaintStep = Shared::isAdditionalXssFlowStep/2;
/** DEPRECATED: Alias for isAdditionalXssTaintStep */
deprecated predicate isAdditionalXSSTaintStep = isAdditionalXssTaintStep/2;
private class OrmFieldAsSource extends Source instanceof DataFlow2::CallNode {
OrmFieldAsSource() {
@@ -341,3 +354,6 @@ module StoredXSS {
private class FileSystemReadAccessAsSource extends Source instanceof FileSystemReadAccess { }
// TODO: Consider `FileNameSource` flowing to script tag `src` attributes and similar
}
/** DEPRECATED: Alias for StoredXss */
deprecated module StoredXSS = StoredXss;

View File

@@ -0,0 +1,275 @@
/**
* Provides default sources and sanitizers for reasoning about data flow from
* sources of sensitive information, as well as extension points for adding
* your own sources and sanitizers.
*/
private import ruby
private import codeql.ruby.DataFlow
private import codeql.ruby.TaintTracking::TaintTracking
private import codeql.ruby.dataflow.RemoteFlowSources
private import SensitiveDataHeuristics::HeuristicNames
private import codeql.ruby.CFG
private import codeql.ruby.dataflow.SSA
/**
* Provides default sources and sanitizers for reasoning about data flow from
* sources of sensitive information, as well as extension points for adding
* your own sources and sanitizers.
*/
module CleartextSources {
/**
* A data flow source of cleartext sensitive information.
*/
abstract class Source extends DataFlow::Node {
/** Gets a string that describes the type of this data flow source. */
abstract string describe();
}
/**
* A sanitizer for cleartext sensitive information.
*/
abstract class Sanitizer extends DataFlow::Node { }
/**
* Holds if `re` may be a regular expression that can be used to sanitize
* sensitive data with a call to `sub`.
*/
private predicate effectiveSubRegExp(CfgNodes::ExprNodes::RegExpLiteralCfgNode re) {
re.getConstantValue().getStringlikeValue().matches([".*", ".+"])
}
/**
* Holds if `re` may be a regular expression that can be used to sanitize
* sensitive data with a call to `gsub`.
*/
private predicate effectiveGsubRegExp(CfgNodes::ExprNodes::RegExpLiteralCfgNode re) {
re.getConstantValue().getStringlikeValue().matches(".")
}
/**
* A call to `sub`/`sub!` or `gsub`/`gsub!` that seems to mask sensitive information.
*/
private class MaskingReplacerSanitizer extends Sanitizer, DataFlow::CallNode {
MaskingReplacerSanitizer() {
exists(CfgNodes::ExprNodes::RegExpLiteralCfgNode re |
re = this.getArgument(0).asExpr() and
(
this.getMethodName() = ["sub", "sub!"] and effectiveSubRegExp(re)
or
this.getMethodName() = ["gsub", "gsub!"] and effectiveGsubRegExp(re)
)
)
}
}
/**
* Like `MaskingReplacerSanitizer` but updates the receiver for methods that
* sanitize the receiver.
* Taint is thereby cleared for any subsequent read.
*/
private class InPlaceMaskingReplacerSanitizer extends Sanitizer {
InPlaceMaskingReplacerSanitizer() {
exists(MaskingReplacerSanitizer m | m.getMethodName() = ["gsub!", "sub!"] |
m.getReceiver() = this
)
}
}
/**
* Holds if `name` is for a method or variable that appears, syntactically, to
* not be sensitive.
*/
bindingset[name]
predicate nameIsNotSensitive(string name) {
name.regexpMatch(notSensitiveRegexp()) and
// By default `notSensitiveRegexp()` includes some false positives for
// common ruby method names that are not necessarily non-sensitive.
// We explicitly exclude element references, element assignments, and
// mutation methods.
not name = ["[]", "[]="] and
not name.matches("%!")
}
/**
* A call that might obfuscate a password, for example through hashing.
*/
private class ObfuscatorCall extends Sanitizer, DataFlow::CallNode {
ObfuscatorCall() { nameIsNotSensitive(this.getMethodName()) }
}
/**
* A data flow node that does not contain a clear-text password, according to its syntactic name.
*/
private class NameGuidedNonCleartextPassword extends NonCleartextPassword {
NameGuidedNonCleartextPassword() {
exists(string name | nameIsNotSensitive(name) |
// accessing a non-sensitive variable
this.asExpr().getExpr().(VariableReadAccess).getVariable().getName() = name
or
// dereferencing a non-sensitive field
this.asExpr()
.(CfgNodes::ExprNodes::ElementReferenceCfgNode)
.getArgument(0)
.getConstantValue()
.getStringlikeValue() = name
or
// calling a non-sensitive method
this.(DataFlow::CallNode).getMethodName() = name
)
or
// avoid i18n strings
this.asExpr()
.(CfgNodes::ExprNodes::ElementReferenceCfgNode)
.getReceiver()
.getConstantValue()
.getStringlikeValue()
.regexpMatch("(?is).*(messages|strings).*")
}
}
/**
* A data flow node that receives flow that is not a clear-text password.
*/
class NonCleartextPasswordFlow extends NonCleartextPassword {
NonCleartextPasswordFlow() {
any(NonCleartextPassword other).(DataFlow::LocalSourceNode).flowsTo(this)
}
}
/**
* A data flow node that does not contain a clear-text password.
*/
abstract private class NonCleartextPassword extends DataFlow::Node { }
// `writeNode` assigns pair with key `name` to `val`
private predicate hashKeyWrite(DataFlow::CallNode writeNode, string name, DataFlow::Node val) {
writeNode.asExpr().getExpr() instanceof SetterMethodCall and
// hash[name]
writeNode.getArgument(0).asExpr().getConstantValue().getStringlikeValue() = name and
// val
writeNode.getArgument(1).asExpr().(CfgNodes::ExprNodes::AssignExprCfgNode).getRhs() =
val.asExpr()
}
/**
* A write to a hash entry with a value that may contain password information.
*/
private class HashKeyWritePasswordSource extends Source {
private string name;
private DataFlow::ExprNode recv;
HashKeyWritePasswordSource() {
exists(DataFlow::Node val |
name.regexpMatch(maybePassword()) and
not nameIsNotSensitive(name) and
// avoid safe values assigned to presumably unsafe names
not val instanceof NonCleartextPassword and
(
// hash[name] = val
hashKeyWrite(this, name, val) and
recv = this.(DataFlow::CallNode).getReceiver()
)
)
}
override string describe() { result = "a write to " + name }
/** Gets the name of the key */
string getName() { result = name }
/**
* Gets the name of the hash variable that this password source is assigned
* to, if applicable.
*/
LocalVariable getVariable() {
result = recv.getExprNode().getExpr().(VariableReadAccess).getVariable()
}
}
/**
* A hash literal with an entry that may contain a password
*/
private class HashLiteralPasswordSource extends Source {
private string name;
HashLiteralPasswordSource() {
exists(DataFlow::Node val, CfgNodes::ExprNodes::HashLiteralCfgNode lit |
name.regexpMatch(maybePassword()) and
not nameIsNotSensitive(name) and
// avoid safe values assigned to presumably unsafe names
not val instanceof NonCleartextPassword and
// hash = { name: val }
exists(CfgNodes::ExprNodes::PairCfgNode p |
this.asExpr() = lit and p = lit.getAKeyValuePair()
|
p.getKey().getConstantValue().getStringlikeValue() = name and
p.getValue() = val.asExpr()
)
)
}
override string describe() { result = "a write to " + name }
}
/** An assignment that may assign a password to a variable */
private class AssignPasswordVariableSource extends Source {
string name;
AssignPasswordVariableSource() {
// avoid safe values assigned to presumably unsafe names
not this instanceof NonCleartextPassword and
name.regexpMatch(maybePassword()) and
not nameIsNotSensitive(name) and
exists(Assignment a |
this.asExpr().getExpr() = a.getRightOperand() and
a.getLeftOperand().getAVariable().getName() = name
)
}
override string describe() { result = "an assignment to " + name }
}
/** A parameter that may contain a password. */
private class ParameterPasswordSource extends Source {
private string name;
ParameterPasswordSource() {
name.regexpMatch(maybePassword()) and
not nameIsNotSensitive(name) and
not this instanceof NonCleartextPassword and
exists(Parameter p, LocalVariable v |
v = p.getAVariable() and
v.getName() = name and
this.asExpr().getExpr() = v.getAnAccess()
)
}
override string describe() { result = "a parameter " + name }
}
/** A call that might return a password. */
private class CallPasswordSource extends DataFlow::CallNode, Source {
private string name;
CallPasswordSource() {
name = this.getMethodName() and
name.regexpMatch("(?is)getPassword")
}
override string describe() { result = "a call to " + name }
}
/** Holds if `nodeFrom` taints `nodeTo`. */
predicate isAdditionalTaintStep(DataFlow::Node nodeFrom, DataFlow::Node nodeTo) {
exists(string name, ElementReference ref, LocalVariable hashVar |
// from `hsh[password] = "changeme"` to a `hsh[password]` read
nodeFrom.(HashKeyWritePasswordSource).getName() = name and
nodeTo.asExpr().getExpr() = ref and
ref.getArgument(0).getConstantValue().getStringlikeValue() = name and
nodeFrom.(HashKeyWritePasswordSource).getVariable() = hashVar and
ref.getReceiver().(VariableReadAccess).getVariable() = hashVar and
nodeFrom.asExpr().getASuccessor*() = nodeTo.asExpr()
)
}
}

View File

@@ -98,7 +98,8 @@ module HeuristicNames {
* suggesting nouns within the string do not represent the meaning of the whole string (e.g. a URL or a SQL query).
*/
string notSensitiveRegexp() {
result = "(?is).*([^\\w$.-]|redact|censor|obfuscate|hash|md5|sha|((?<!un)(en))?(crypt|code)).*"
result =
"(?is).*([^\\w$.-]|redact|censor|obfuscate|hash|md5|sha|random|((?<!un)(en))?(crypt|code)).*"
}
/**

View File

@@ -1,9 +1,5 @@
private import ReDoSUtil
private import RegExpTreeView
private import codeql.Locations
/*
* This query implements the analysis described in the following two papers:
/**
* This library implements the analysis described in the following two papers:
*
* James Kirrage, Asiri Rathnayake, Hayo Thielecke: Static Analysis for
* Regular Expression Denial-of-Service Attacks. NSS 2013.
@@ -31,9 +27,9 @@ private import codeql.Locations
* condition is equivalent to saying that `(q, q)` is reachable from `(r1, r2)`
* in the product NFA.
*
* This is what the query does. It makes a simple attempt to construct a
* This is what the library does. It makes a simple attempt to construct a
* prefix `v` leading into `q`, but only to improve the alert message.
* And the query tries to prove the existence of a suffix that ensures
* And the library tries to prove the existence of a suffix that ensures
* rejection. This check might fail, which can cause false positives.
*
* Finally, sometimes it depends on the translation whether the NFA generated
@@ -41,7 +37,7 @@ private import codeql.Locations
* particular translation, which may result in false positives or negatives
* relative to some particular JavaScript engine.
*
* More precisely, the query constructs an NFA from a regular expression `r`
* More precisely, the library constructs an NFA from a regular expression `r`
* as follows:
*
* * Every sub-term `t` gives rise to an NFA state `Match(t,i)`, representing
@@ -66,6 +62,8 @@ private import codeql.Locations
* a suffix `x` (possible empty) that is most likely __not__ accepted.
*/
import ReDoSUtil
/**
* Holds if state `s` might be inside a backtracking repetition.
*/
@@ -90,18 +88,19 @@ private class MaybeBacktrackingRepetition extends InfiniteRepetitionQuantifier {
/**
* A state in the product automaton.
*
* We lazily only construct those states that we are actually
* going to need: `(q, q)` for every fork state `q`, and any
* pair of states that can be reached from a pair that we have
* already constructed. To cut down on the number of states,
* we only represent states `(q1, q2)` where `q1` is lexicographically
* no bigger than `q2`.
*
* States are only constructed if both states in the pair are
* inside a repetition that might backtrack.
*/
private newtype TStatePair =
/**
* We lazily only construct those states that we are actually
* going to need: `(q, q)` for every fork state `q`, and any
* pair of states that can be reached from a pair that we have
* already constructed. To cut down on the number of states,
* we only represent states `(q1, q2)` where `q1` is lexicographically
* no bigger than `q2`.
*
* States are only constructed if both states in the pair are
* inside a repetition that might backtrack.
*/
MkStatePair(State q1, State q2) {
isFork(q1, _, _, _, _) and q2 = q1
or

View File

@@ -8,8 +8,7 @@ private import codeql.ruby.AST as AST
private import codeql.ruby.CFG
private import codeql.ruby.DataFlow
private import codeql.ruby.dataflow.RemoteFlowSources
private import codeql.ruby.security.performance.ParseRegExp as RegExp
private import codeql.ruby.security.performance.RegExpTreeView
private import codeql.ruby.Regexp
private import codeql.ruby.security.performance.SuperlinearBackTracking
module PolynomialReDoS {

View File

@@ -12,7 +12,7 @@
* states that will cause backtracking (a rejecting suffix exists).
*/
import RegExpTreeView
import ReDoSUtilSpecific
/**
* A configuration for which parts of a regular expression should be considered relevant for

View File

@@ -0,0 +1,63 @@
/**
* Provides Ruby-specific definitions for use in the ReDoSUtil module.
*/
import codeql.ruby.Regexp
import codeql.Locations
private import codeql.ruby.ast.Literal as AST
/**
* Holds if `term` is an ecape class representing e.g. `\d`.
* `clazz` is which character class it represents, e.g. "d" for `\d`.
*/
predicate isEscapeClass(RegExpTerm term, string clazz) {
exists(RegExpCharacterClassEscape escape | term = escape | escape.getValue() = clazz)
or
// TODO: expand to cover more properties
exists(RegExpNamedCharacterProperty escape | term = escape |
escape.getName().toLowerCase() = "digit" and
if escape.isInverted() then clazz = "D" else clazz = "d"
or
escape.getName().toLowerCase() = "space" and
if escape.isInverted() then clazz = "S" else clazz = "s"
or
escape.getName().toLowerCase() = "word" and
if escape.isInverted() then clazz = "W" else clazz = "w"
)
}
/**
* Holds if the regular expression should not be considered.
*/
predicate isExcluded(RegExpParent parent) {
parent.(RegExpTerm).getRegExp().(AST::RegExpLiteral).hasFreeSpacingFlag() // exclude free-spacing mode regexes
}
/**
* A module containing predicates for determining which flags a regular expression have.
*/
module RegExpFlags {
/**
* Holds if `root` has the `i` flag for case-insensitive matching.
*/
predicate isIgnoreCase(RegExpTerm root) {
root.isRootTerm() and
root.getLiteral().isIgnoreCase()
}
/**
* Gets the flags for `root`, or the empty string if `root` has no flags.
*/
string getFlags(RegExpTerm root) {
root.isRootTerm() and
result = root.getLiteral().getFlags()
}
/**
* Holds if `root` has the `s` flag for multi-line matching.
*/
predicate isDotAll(RegExpTerm root) {
root.isRootTerm() and
root.getLiteral().isDotAll()
}
}

View File

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

View File

@@ -1,3 +1,5 @@
## 0.0.11
## 0.0.10
### New Queries

View File

@@ -0,0 +1,4 @@
---
category: newQuery
---
* Added a new query, `rb/incomplete-hostname-regexp`. The query finds instances where a hostname is incompletely sanitized due to an unescaped character in a regular expression.

View File

@@ -0,0 +1,4 @@
---
category: newQuery
---
* Added a new query, `rb/incomplete-url-substring-sanitization`. The query finds instances where a URL is incompletely sanitized due to insufficient checks.

View File

@@ -0,0 +1,4 @@
---
category: newQuery
---
* Added a new query, `rb/http-to-file-access`. The query finds cases where data from remote user input is written to a file.

View File

@@ -0,0 +1,4 @@
---
category: newQuery
---
* Added a new query, `rb/clear-text-storage-sensitive-data`. The query finds cases where sensitive information, such as user credentials, are stored as cleartext.

View File

@@ -0,0 +1,4 @@
---
category: newQuery
---
* Added a new query, `rb/http-tainted-format-string`. The query finds cases where data from remote user input is used in a string formatting method in a way that allows arbitrary format specifiers to be inserted.

View File

@@ -0,0 +1 @@
## 0.0.11

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