mirror of
https://github.com/github/codeql.git
synced 2026-05-05 13:45:19 +02:00
Merge remote-tracking branch 'origin/main' into ruby/weak-cryptographic-algorithm
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
@@ -1,4 +0,0 @@
|
||||
---
|
||||
category: minorAnalysis
|
||||
---
|
||||
* The regular expression parser now groups sequences of normal characters. This reduces the number of instances of `RegExpNormalChar`.
|
||||
@@ -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`.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
4
ruby/ql/lib/change-notes/2022-03-21-regex.md
Normal file
4
ruby/ql/lib/change-notes/2022-03-21-regex.md
Normal file
@@ -0,0 +1,4 @@
|
||||
---
|
||||
category: minorAnalysis
|
||||
---
|
||||
* The `ParseRegExp` and `RegExpTreeView` modules are now "internal" modules. Users should use `codeql.ruby.Regexp` instead.
|
||||
@@ -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)`.
|
||||
@@ -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`.
|
||||
@@ -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`.
|
||||
@@ -1,2 +1,2 @@
|
||||
---
|
||||
lastReleaseVersion: 0.0.10
|
||||
lastReleaseVersion: 0.0.11
|
||||
|
||||
129
ruby/ql/lib/codeql/NumberUtils.qll
Normal file
129
ruby/ql/lib/codeql/NumberUtils.qll
Normal 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) }
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
134
ruby/ql/lib/codeql/ruby/InclusionTests.qll
Normal file
134
ruby/ql/lib/codeql/ruby/InclusionTests.qll
Normal 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 }
|
||||
}
|
||||
}
|
||||
143
ruby/ql/lib/codeql/ruby/Regexp.qll
Normal file
143
ruby/ql/lib/codeql/ruby/Regexp.qll
Normal 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))
|
||||
}
|
||||
180
ruby/ql/lib/codeql/ruby/StringOps.qll
Normal file
180
ruby/ql/lib/codeql/ruby/StringOps.qll
Normal 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(_) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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" }
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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 + " _"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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() }
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
@@ -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)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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`.
|
||||
*/
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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")
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
)
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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() }
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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() }
|
||||
}
|
||||
|
||||
@@ -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
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
31
ruby/ql/lib/codeql/ruby/frameworks/data/ModelsAsData.qll
Normal file
31
ruby/ql/lib/codeql/ruby/frameworks/data/ModelsAsData.qll
Normal 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)" }
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
@@ -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)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
}
|
||||
|
||||
6
ruby/ql/lib/codeql/ruby/internal/ConceptsImports.qll
Normal file
6
ruby/ql/lib/codeql/ruby/internal/ConceptsImports.qll
Normal 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
|
||||
13
ruby/ql/lib/codeql/ruby/internal/ConceptsShared.qll
Normal file
13
ruby/ql/lib/codeql/ruby/internal/ConceptsShared.qll
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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() }
|
||||
}
|
||||
@@ -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()) }
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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() }
|
||||
}
|
||||
}
|
||||
33
ruby/ql/lib/codeql/ruby/security/CleartextStorageQuery.qll
Normal file
33
ruby/ql/lib/codeql/ruby/security/CleartextStorageQuery.qll
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -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()) }
|
||||
}
|
||||
}
|
||||
25
ruby/ql/lib/codeql/ruby/security/HttpToFileAccessQuery.qll
Normal file
25
ruby/ql/lib/codeql/ruby/security/HttpToFileAccessQuery.qll
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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() }
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(_))
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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" }
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
275
ruby/ql/lib/codeql/ruby/security/internal/CleartextSources.qll
Normal file
275
ruby/ql/lib/codeql/ruby/security/internal/CleartextSources.qll
Normal 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()
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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)).*"
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
## 0.0.11
|
||||
|
||||
## 0.0.10
|
||||
|
||||
### New Queries
|
||||
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
1
ruby/ql/src/change-notes/released/0.0.11.md
Normal file
1
ruby/ql/src/change-notes/released/0.0.11.md
Normal file
@@ -0,0 +1 @@
|
||||
## 0.0.11
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user