Merge branch 'main' of github.com:github/codeql into labelNaming

This commit is contained in:
Erik Krogh Kristensen
2022-03-28 15:30:33 +02:00
35 changed files with 2025 additions and 1371 deletions

View File

@@ -413,10 +413,10 @@ predicate isValidNoArgumentTokenInIdentifyingAccessPath(string name) {
bindingset[name, argument]
predicate isValidTokenArgumentInIdentifyingAccessPath(string name, string argument) {
name = ["Argument", "Parameter"] and
argument.regexpMatch("(N-|-)?\\d+(\\.\\.(N-|-)?\\d+)?")
argument.regexpMatch("(N-|-)?\\d+(\\.\\.((N-|-)?\\d+)?)?")
or
name = "WithArity" and
argument.regexpMatch("\\d+(\\.\\.\\d+)?")
argument.regexpMatch("\\d+(\\.\\.(\\d+)?)?")
or
Specific::isExtraValidTokenArgumentInIdentifyingAccessPath(name, argument)
}

View File

@@ -120,6 +120,10 @@ API::Node getExtraSuccessorFromNode(API::Node node, AccessPathToken token) {
// API graphs do not use store/load steps for arrays
token.getName() = ["ArrayElement", "Element"] and
result = node.getUnknownMember()
or
token.getName() = "Parameter" and
token.getAnArgument() = "this" and
result = node.getReceiver()
}
/**
@@ -129,6 +133,10 @@ bindingset[token]
API::Node getExtraSuccessorFromInvoke(API::InvokeNode node, AccessPathToken token) {
token.getName() = "Instance" and
result = node.getInstance()
or
token.getName() = "Argument" and
token.getAnArgument() = "this" and
result.getARhs() = node.(DataFlow::CallNode).getReceiver()
}
/**

View File

@@ -14,69 +14,73 @@ taintFlow
| test.js:24:43:24:50 | source() | test.js:24:8:24:51 | testlib ... urce()) |
| test.js:31:29:31:36 | source() | test.js:32:10:32:10 | y |
| test.js:37:29:37:36 | source() | test.js:38:10:38:10 | y |
| test.js:46:18:46:25 | source() | test.js:46:18:46:25 | source() |
| test.js:47:22:47:29 | source() | test.js:47:22:47:29 | source() |
| test.js:49:24:49:31 | source() | test.js:49:24:49:31 | source() |
| test.js:53:27:53:34 | source() | test.js:53:27:53:34 | source() |
| test.js:58:31:58:38 | source() | test.js:58:31:58:38 | source() |
| test.js:62:34:62:41 | source() | test.js:62:34:62:41 | source() |
| test.js:67:31:67:38 | source() | test.js:67:31:67:38 | source() |
| test.js:68:34:68:41 | source() | test.js:68:34:68:41 | source() |
| test.js:72:36:72:43 | source() | test.js:72:36:72:43 | source() |
| test.js:73:39:73:46 | source() | test.js:73:39:73:46 | source() |
| test.js:75:28:75:35 | source() | test.js:75:28:75:35 | source() |
| test.js:76:31:76:38 | source() | test.js:76:31:76:38 | source() |
| test.js:77:34:77:41 | source() | test.js:77:34:77:41 | source() |
| test.js:81:28:81:35 | source() | test.js:81:28:81:35 | source() |
| test.js:87:17:87:24 | source() | test.js:87:17:87:24 | source() |
| test.js:88:17:88:24 | source() | test.js:88:17:88:24 | source() |
| test.js:89:17:89:24 | source() | test.js:89:17:89:24 | source() |
| test.js:43:29:43:36 | source() | test.js:44:10:44:10 | y |
| test.js:47:33:47:40 | source() | test.js:49:10:49:13 | this |
| test.js:54:18:54:25 | source() | test.js:54:18:54:25 | source() |
| test.js:55:22:55:29 | source() | test.js:55:22:55:29 | source() |
| test.js:57:24:57:31 | source() | test.js:57:24:57:31 | source() |
| test.js:61:27:61:34 | source() | test.js:61:27:61:34 | source() |
| test.js:66:31:66:38 | source() | test.js:66:31:66:38 | source() |
| test.js:70:34:70:41 | source() | test.js:70:34:70:41 | source() |
| test.js:75:31:75:38 | source() | test.js:75:31:75:38 | source() |
| test.js:76:34:76:41 | source() | test.js:76:34:76:41 | source() |
| test.js:80:36:80:43 | source() | test.js:80:36:80:43 | source() |
| test.js:81:39:81:46 | source() | test.js:81:39:81:46 | source() |
| test.js:83:28:83:35 | source() | test.js:83:28:83:35 | source() |
| test.js:84:31:84:38 | source() | test.js:84:31:84:38 | source() |
| test.js:85:34:85:41 | source() | test.js:85:34:85:41 | source() |
| test.js:89:28:89:35 | source() | test.js:89:28:89:35 | source() |
| test.js:95:17:95:24 | source() | test.js:95:17:95:24 | source() |
| test.js:96:17:96:24 | source() | test.js:96:17:96:24 | source() |
| test.js:97:17:97:24 | source() | test.js:97:17:97:24 | source() |
| test.js:102:16:102:34 | testlib.getSource() | test.js:103:8:103:13 | source |
| test.js:102:16:102:34 | testlib.getSource() | test.js:104:8:104:24 | source.continue() |
isSink
| test.js:46:18:46:25 | source() | test-sink |
| test.js:47:22:47:29 | source() | test-sink |
| test.js:49:24:49:31 | source() | test-sink |
| test.js:53:27:53:34 | source() | test-sink |
| test.js:55:38:55:38 | 4 | test-sink |
| test.js:56:38:56:38 | 4 | test-sink |
| test.js:57:38:57:38 | 4 | test-sink |
| test.js:58:31:58:38 | source() | test-sink |
| test.js:60:41:60:41 | 3 | test-sink |
| test.js:61:41:61:41 | 3 | test-sink |
| test.js:62:34:62:41 | source() | test-sink |
| test.js:63:34:63:34 | 3 | test-sink |
| test.js:65:38:65:38 | 3 | test-sink |
| test.js:65:41:65:41 | 4 | test-sink |
| test.js:66:38:66:38 | 3 | test-sink |
| test.js:66:41:66:41 | 4 | test-sink |
| test.js:67:31:67:38 | source() | test-sink |
| test.js:67:41:67:41 | 4 | test-sink |
| test.js:68:31:68:31 | 3 | test-sink |
| test.js:68:34:68:41 | source() | test-sink |
| test.js:70:43:70:43 | 3 | test-sink |
| test.js:70:46:70:46 | 4 | test-sink |
| test.js:71:43:71:43 | 3 | test-sink |
| test.js:71:46:71:46 | 4 | test-sink |
| test.js:72:36:72:43 | source() | test-sink |
| test.js:72:46:72:46 | 4 | test-sink |
| test.js:73:36:73:36 | 3 | test-sink |
| test.js:73:39:73:46 | source() | test-sink |
| test.js:75:28:75:35 | source() | test-sink |
| test.js:75:38:75:38 | 2 | test-sink |
| test.js:75:41:75:41 | 3 | test-sink |
| test.js:76:28:76:28 | 1 | test-sink |
| test.js:76:31:76:38 | source() | test-sink |
| test.js:76:41:76:41 | 3 | test-sink |
| test.js:77:28:77:28 | 1 | test-sink |
| test.js:77:31:77:31 | 2 | test-sink |
| test.js:77:34:77:41 | source() | test-sink |
| test.js:78:28:78:28 | 1 | test-sink |
| test.js:78:31:78:31 | 2 | test-sink |
| test.js:78:34:78:34 | 3 | test-sink |
| test.js:81:28:81:35 | source() | test-sink |
| test.js:82:28:82:28 | 1 | test-sink |
| test.js:87:17:87:24 | source() | test-sink |
| test.js:88:17:88:24 | source() | test-sink |
| test.js:89:17:89:24 | source() | test-sink |
| test.js:54:18:54:25 | source() | test-sink |
| test.js:55:22:55:29 | source() | test-sink |
| test.js:57:24:57:31 | source() | test-sink |
| test.js:61:27:61:34 | source() | test-sink |
| test.js:63:38:63:38 | 4 | test-sink |
| test.js:64:38:64:38 | 4 | test-sink |
| test.js:65:38:65:38 | 4 | test-sink |
| test.js:66:31:66:38 | source() | test-sink |
| test.js:68:41:68:41 | 3 | test-sink |
| test.js:69:41:69:41 | 3 | test-sink |
| test.js:70:34:70:41 | source() | test-sink |
| test.js:71:34:71:34 | 3 | test-sink |
| test.js:73:38:73:38 | 3 | test-sink |
| test.js:73:41:73:41 | 4 | test-sink |
| test.js:74:38:74:38 | 3 | test-sink |
| test.js:74:41:74:41 | 4 | test-sink |
| test.js:75:31:75:38 | source() | test-sink |
| test.js:75:41:75:41 | 4 | test-sink |
| test.js:76:31:76:31 | 3 | test-sink |
| test.js:76:34:76:41 | source() | test-sink |
| test.js:78:43:78:43 | 3 | test-sink |
| test.js:78:46:78:46 | 4 | test-sink |
| test.js:79:43:79:43 | 3 | test-sink |
| test.js:79:46:79:46 | 4 | test-sink |
| test.js:80:36:80:43 | source() | test-sink |
| test.js:80:46:80:46 | 4 | test-sink |
| test.js:81:36:81:36 | 3 | test-sink |
| test.js:81:39:81:46 | source() | test-sink |
| test.js:83:28:83:35 | source() | test-sink |
| test.js:83:38:83:38 | 2 | test-sink |
| test.js:83:41:83:41 | 3 | test-sink |
| test.js:84:28:84:28 | 1 | test-sink |
| test.js:84:31:84:38 | source() | test-sink |
| test.js:84:41:84:41 | 3 | test-sink |
| test.js:85:28:85:28 | 1 | test-sink |
| test.js:85:31:85:31 | 2 | test-sink |
| test.js:85:34:85:41 | source() | test-sink |
| test.js:86:28:86:28 | 1 | test-sink |
| test.js:86:31:86:31 | 2 | test-sink |
| test.js:86:34:86:34 | 3 | test-sink |
| test.js:89:28:89:35 | source() | test-sink |
| test.js:90:28:90:28 | 1 | test-sink |
| test.js:95:17:95:24 | source() | test-sink |
| test.js:96:17:96:24 | source() | test-sink |
| test.js:97:17:97:24 | source() | test-sink |
syntaxErrors
| Member[foo |
| Member[foo] .Member[bar] |

View File

@@ -40,6 +40,14 @@ function testPreserveTaint() {
testlib.taintIntoCallback(source(), undefined, undefined, y => {
sink(y); // OK - only callback 1-2 receive taint
});
testlib.taintIntoCallback(source(), function(y) {
sink(y); // NOT OK
sink(this); // OK - receiver is not tainted
});
testlib.taintIntoCallbackThis(source(), function(y) {
sink(y); // OK - only receiver is tainted
sink(this); // NOT OK
});
}
function testSinks() {
@@ -89,3 +97,10 @@ function testSinks() {
testlib.sink3(source()); // NOT OK
testlib.sink4(source()); // OK
}
function testFlowThroughReceiver() {
let source = testlib.getSource();
sink(source); // NOT OK
sink(source.continue()); // NOT OK
sink(source.blah()); // OK
}

View File

@@ -9,9 +9,11 @@ class Steps extends ModelInput::SummaryModelCsv {
[
"testlib;;Member[preserveTaint];Argument[0];ReturnValue;taint",
"testlib;;Member[taintIntoCallback];Argument[0];Argument[1..2].Parameter[0];taint",
"testlib;;Member[taintIntoCallbackThis];Argument[0];Argument[1..2].Parameter[this];taint",
"testlib;;Member[preserveArgZeroAndTwo];Argument[0,2];ReturnValue;taint",
"testlib;;Member[preserveAllButFirstArgument];Argument[1..];ReturnValue;taint",
"testlib;;Member[preserveAllIfCall].Call;Argument[0..];ReturnValue;taint"
"testlib;;Member[preserveAllIfCall].Call;Argument[0..];ReturnValue;taint",
"testlib;;Member[getSource].ReturnValue.Member[continue];Argument[this];ReturnValue;taint",
]
}
}
@@ -35,11 +37,17 @@ class Sinks extends ModelInput::SinkModelCsv {
}
}
class Sources extends ModelInput::SourceModelCsv {
override predicate row(string row) { row = "testlib;;Member[getSource].ReturnValue;test-source" }
}
class BasicTaintTracking extends TaintTracking::Configuration {
BasicTaintTracking() { this = "BasicTaintTracking" }
override predicate isSource(DataFlow::Node source) {
source.(DataFlow::CallNode).getCalleeName() = "source"
or
source = ModelOutput::getASourceNode("test-source").getAnImmediateUse()
}
override predicate isSink(DataFlow::Node sink) {

View File

@@ -1,10 +1,6 @@
| CSV type row should have 5 columns but has 2: test;TooFewColumns |
| CSV type row should have 5 columns but has 8: test;TooManyColumns;;;Member[Foo].Instance;too;many;columns |
| Invalid argument '0-1' in token 'Argument[0-1]' in access path: Method[foo].Argument[0-1] |
| Invalid argument '0..' in token 'Argument[0..]' in access path: Argument[0..].Member[password] |
| Invalid argument '0..' in token 'Argument[0..]' in access path: Argument[0..].Member[username] |
| Invalid argument '0..' in token 'Argument[0..]' in access path: Member[executeSql].Argument[0..].Parameter[1] |
| Invalid argument '0..' in token 'Argument[0..]' in access path: Member[run].Argument[0..].Parameter[1] |
| Invalid argument '*' in token 'Argument[*]' in access path: Method[foo].Argument[*] |
| Invalid token 'Argument' is missing its arguments, in access path: Method[foo].Argument |
| Invalid token 'Member' is missing its arguments, in access path: Method[foo].Member |

View File

@@ -552,7 +552,7 @@ class RegExpWordBoundary extends RegExpSpecialChar {
/**
* A character class escape in a regular expression.
* That is, an escaped charachter that denotes multiple characters.
* That is, an escaped character that denotes multiple characters.
*
* Examples:
*

View File

@@ -188,7 +188,7 @@ abstract class RegexString extends Expr {
)
}
/** Hold is a character set starts between `start` and `end`. */
/** Holds if a character set starts between `start` and `end`. */
predicate char_set_start(int start, int end) {
this.char_set_start(start) = true and
(
@@ -316,8 +316,10 @@ abstract class RegexString extends Expr {
result = this.(Bytes).getS()
}
/** 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])
@@ -329,6 +331,9 @@ abstract class RegexString extends Expr {
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 |
@@ -417,6 +422,9 @@ abstract class RegexString extends Expr {
)
}
/**
* Holds if a simple or escaped character is found between `start` and `end`.
*/
predicate character(int start, int end) {
(
this.simpleCharacter(start, end) and
@@ -428,12 +436,18 @@ abstract class RegexString extends Expr {
not exists(int x, int y | this.backreference(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) {
not this.inCharSet(start) and
this.character(start, end) and
@@ -492,7 +506,7 @@ abstract class RegexString extends Expr {
this.specialCharacter(start, end, _)
}
/** Whether the text in the range start,end is a group */
/** Whether the text in the range `start,end` is a group */
predicate group(int start, int end) {
this.groupContents(start, end, _, _)
or
@@ -611,6 +625,7 @@ abstract class RegexString extends Expr {
this.simple_group_start(start, end)
}
/** Matches the start of a non-capturing group, e.g. `(?:` */
private predicate non_capturing_group_start(int start, int end) {
this.isGroupStart(start) and
this.getChar(start + 1) = "?" and
@@ -618,12 +633,18 @@ abstract class RegexString extends Expr {
end = start + 3
}
/** Matches the start of a simple group, e.g. `(a+)`. */
private predicate simple_group_start(int start, int end) {
this.isGroupStart(start) and
this.getChar(start + 1) != "?" and
end = start + 1
}
/**
* Matches the start of a named group, such as:
* - `(?<name>\w+)`
* - `(?'name'\w+)`
*/
private predicate named_group_start(int start, int end) {
this.isGroupStart(start) and
this.getChar(start + 1) = "?" and
@@ -675,6 +696,7 @@ abstract class RegexString extends Expr {
)
}
/** Matches the start of a positive lookahead assertion, i.e. `(?=`. */
private predicate lookahead_assertion_start(int start, int end) {
this.isGroupStart(start) and
this.getChar(start + 1) = "?" and
@@ -682,6 +704,7 @@ abstract class RegexString extends Expr {
end = start + 3
}
/** Matches the start of a negative lookahead assertion, i.e. `(?!`. */
private predicate negative_lookahead_assertion_start(int start, int end) {
this.isGroupStart(start) and
this.getChar(start + 1) = "?" and
@@ -689,6 +712,7 @@ abstract class RegexString extends Expr {
end = start + 3
}
/** Matches the start of a positive lookbehind assertion, i.e. `(?<=`. */
private predicate lookbehind_assertion_start(int start, int end) {
this.isGroupStart(start) and
this.getChar(start + 1) = "?" and
@@ -697,6 +721,7 @@ abstract class RegexString extends Expr {
end = start + 4
}
/** Matches the start of a negative lookbehind assertion, i.e. `(?<!`. */
private predicate negative_lookbehind_assertion_start(int start, int end) {
this.isGroupStart(start) and
this.getChar(start + 1) = "?" and
@@ -705,6 +730,7 @@ abstract class RegexString extends Expr {
end = start + 4
}
/** Matches the start of a comment group, i.e. `(?#`. */
private predicate comment_group_start(int start, int end) {
this.isGroupStart(start) and
this.getChar(start + 1) = "?" and
@@ -712,6 +738,7 @@ abstract class RegexString extends Expr {
end = start + 3
}
/** Matches the contents of a group. */
predicate groupContents(int start, int end, int in_start, int in_end) {
this.group_start(start, in_start) and
end = in_end + 1 and
@@ -719,12 +746,14 @@ abstract class RegexString extends Expr {
this.isGroupEnd(in_end)
}
/** Matches a named backreference, e.g. `\k<foo>`. */
private predicate named_backreference(int start, int end, string name) {
this.named_backreference_start(start, start + 4) and
end = min(int i | i > start + 4 and this.getChar(i) = ")") + 1 and
name = this.getText().substring(start + 4, end - 2)
}
/** Matches a numbered backreference, e.g. `\1`. */
private predicate numbered_backreference(int start, int end, int value) {
this.escapingChar(start) and
// starting with 0 makes it an octal escape
@@ -749,7 +778,7 @@ abstract class RegexString extends Expr {
)
}
/** Whether the text in the range start,end is a back reference */
/** Whether the text in the range `start,end` is a back reference */
predicate backreference(int start, int end) {
this.numbered_backreference(start, end, _)
or

View File

@@ -1134,7 +1134,7 @@ class Import extends TImport, ModuleMember, ModuleRef {
*/
string getImportString() {
exists(string selec |
not exists(getQualifiedName(_)) and selec = ""
not exists(getSelectionName(_)) and selec = ""
or
selec =
"::" + strictconcat(int i, string q | q = this.getSelectionName(i) | q, "::" order by i)

View File

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

View File

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

View File

@@ -473,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.
*/
@@ -512,7 +482,7 @@ module API {
) {
exists(DataFlowDispatch::ArgumentPosition argPos |
argument.sourceArgumentOf(call.asExpr(), argPos) and
lbl = getLabelFromArgumentPosition(argPos)
lbl = Label::getLabelFromArgumentPosition(argPos)
)
}
@@ -525,7 +495,7 @@ module API {
) {
exists(DataFlowDispatch::ParameterPosition paramPos |
paramNode.isSourceParameterOf(callable.asExpr().getExpr(), paramPos) and
lbl = getLabelFromParameterPosition(paramPos)
lbl = Label::getLabelFromParameterPosition(paramPos)
)
}
@@ -803,5 +773,37 @@ module API {
/** Gets the label for the edge from the root node to a custom entry point of the given name. */
LabelEntryPoint entryPoint(API::EntryPoint name) { result.getName() = name }
/** Gets the API graph label corresponding to the given argument position. */
Label::ApiLabel getLabelFromArgumentPosition(DataFlowDispatch::ArgumentPosition pos) {
exists(int n |
pos.isPositional(n) and
result = Label::parameter(n)
)
or
exists(string name |
pos.isKeyword(name) and
result = Label::keywordParameter(name)
)
or
pos.isBlock() and
result = Label::blockParameter()
}
/** Gets the API graph label corresponding to the given parameter position. */
Label::ApiLabel getLabelFromParameterPosition(DataFlowDispatch::ParameterPosition pos) {
exists(int n |
pos.isPositional(n) and
result = Label::parameter(n)
)
or
exists(string name |
pos.isKeyword(name) and
result = Label::keywordParameter(name)
)
or
pos.isBlock() and
result = Label::blockParameter()
}
}
}

View File

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

View File

@@ -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
@@ -393,7 +393,7 @@ class RegExpLiteral extends StringlikeLiteral instanceof RegExpLiteralImpl {
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) }
}
/**

View File

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

View File

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

View File

@@ -24,7 +24,7 @@ private class Summaries extends ModelInput::SummaryModelCsv {
row =
[
"activestorage;;Member[ActiveStorage].Member[Filename].Method[new];Argument[0];ReturnValue;taint",
"activestorage;;Member[ActiveStorage].Member[Filename].Instance.Method[sanitized];Receiver;ReturnValue;taint",
"activestorage;;Member[ActiveStorage].Member[Filename].Instance.Method[sanitized];Argument[self];ReturnValue;taint",
]
}
}

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -413,10 +413,10 @@ predicate isValidNoArgumentTokenInIdentifyingAccessPath(string name) {
bindingset[name, argument]
predicate isValidTokenArgumentInIdentifyingAccessPath(string name, string argument) {
name = ["Argument", "Parameter"] and
argument.regexpMatch("(N-|-)?\\d+(\\.\\.(N-|-)?\\d+)?")
argument.regexpMatch("(N-|-)?\\d+(\\.\\.((N-|-)?\\d+)?)?")
or
name = "WithArity" and
argument.regexpMatch("\\d+(\\.\\.\\d+)?")
argument.regexpMatch("\\d+(\\.\\.(\\d+)?)?")
or
Specific::isExtraValidTokenArgumentInIdentifyingAccessPath(name, argument)
}

View File

@@ -26,6 +26,8 @@ class Unit = DataFlowPrivate::Unit;
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.
@@ -107,8 +109,10 @@ API::Node getExtraSuccessorFromNode(API::Node node, AccessPathToken token) {
token.getName() = "Instance" and
result = node.getInstance()
or
token.getName() = "BlockArgument" and
result = node.getBlock()
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.
@@ -118,7 +122,12 @@ API::Node getExtraSuccessorFromNode(API::Node node, AccessPathToken token) {
* Gets a Ruby-specific API graph successor of `node` reachable by resolving `token`.
*/
bindingset[token]
API::Node getExtraSuccessorFromInvoke(InvokeNode node, AccessPathToken token) { none() }
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`.
@@ -146,7 +155,7 @@ InvokeNode getAnInvocationOf(API::Node node) { result = node }
*/
bindingset[name]
predicate isExtraValidTokenNameInIdentifyingAccessPath(string name) {
name = ["Member", "Method", "Instance", "WithBlock", "WithoutBlock", "BlockArgument"]
name = ["Member", "Method", "Instance", "WithBlock", "WithoutBlock"]
}
/**
@@ -154,7 +163,7 @@ predicate isExtraValidTokenNameInIdentifyingAccessPath(string name) {
* in an identifying access path.
*/
predicate isExtraValidNoArgumentTokenInIdentifyingAccessPath(string name) {
name = ["Instance", "WithBlock", "WithoutBlock", "BlockArgument"]
name = ["Instance", "WithBlock", "WithoutBlock"]
}
/**
@@ -165,4 +174,11 @@ bindingset[name, argument]
predicate isExtraValidTokenArgumentInIdentifyingAccessPath(string name, string argument) {
name = ["Member", "Method"] and
exists(argument)
or
name = ["Argument", "Parameter"] and
(
argument = ["self", "block"]
or
argument.regexpMatch("\\w+:") // keyword argument
)
}

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -7,10 +7,6 @@
private import codeql.ruby.ast.Literal as AST
private import codeql.Locations
private import codeql.ruby.DataFlow
private import codeql.ruby.controlflow.CfgNodes
private import codeql.ruby.ApiGraphs
private import codeql.ruby.dataflow.internal.tainttrackingforlibraries.TaintTrackingImpl
/**
* 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
@@ -229,8 +256,10 @@ abstract class RegExp extends AST::StringlikeLiteral {
/** Gets the text of this regex */
string getText() { result = this.getConstantValue().getString() }
/** 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 +271,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 +363,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 +387,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 +436,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 +454,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
@@ -505,6 +559,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
@@ -538,24 +593,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, _)
@@ -661,6 +720,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
@@ -747,6 +807,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
@@ -774,6 +839,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
) {
@@ -781,6 +853,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
@@ -960,75 +1033,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) and
// exclude https://ruby-doc.org/core-2.4.0/Regexp.html#method-i-match
not mce.getReceiver().asExpr().getExpr() instanceof AST::RegExpLiteral
)
}
private class RegExpConfiguration extends Configuration {
RegExpConfiguration() { this = "RegExpConfiguration" }
override predicate isSource(DataFlow::Node source) {
source.asExpr() =
any(ExprCfgNode e |
e.getConstantValue().isString(_) and
not e instanceof ExprNodes::VariableReadAccessCfgNode and
not e instanceof ExprNodes::ConstantReadAccessCfgNode
)
}
override predicate isSink(DataFlow::Node sink) { isInterpretedAsRegExp(sink) }
override predicate isSanitizer(DataFlow::Node node) {
// stop flow if `node` is receiver of
// https://ruby-doc.org/core-2.4.0/String.html#method-i-match
exists(DataFlow::CallNode mce |
mce.getMethodName() = ["match", "match?"] and
node = mce.getReceiver() and
mce.getArgument(0).asExpr().getExpr() instanceof AST::RegExpLiteral
)
}
}
/**
* Gets a node whose value may flow (inter-procedurally) to `re`, where it is interpreted
* as a part of a regular expression.
*/
cached
DataFlow::Node regExpSource(DataFlow::Node re) {
exists(RegExpConfiguration c | c.hasFlow(result, re))
}

View File

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

View File

@@ -1,8 +1,10 @@
private import codeql.ruby.ast.Literal as AST
private import ParseRegExp
private import codeql.NumberUtils
/**
* This module should provide a class hierarchy corresponding to a parse tree of regular expressions.
*/
import codeql.ruby.Regexp
import codeql.Locations
private import codeql.ruby.DataFlow
private import codeql.ruby.ast.Literal as AST
/**
* Holds if `term` is an ecape class representing e.g. `\d`.
@@ -59,776 +61,3 @@ module RegExpFlags {
root.getLiteral().isDotAll()
}
}
/**
* 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])"
}
}
/**
* 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 {
string toString() { result = "RegExpParent" }
RegExpTerm getChild(int i) { none() }
final RegExpTerm getAChild() { result = this.getChild(_) }
int getNumChild() { result = count(this.getAChild()) }
/**
* Gets the name of a primary CodeQL class to which this regular
* expression term belongs.
*/
string getAPrimaryQlClass() { result = "RegExpParent" }
/**
* Gets a comma-separated list of the names of the primary CodeQL classes to
* which this regular expression term belongs.
*/
final string getPrimaryQlClasses() { result = concat(this.getAPrimaryQlClass(), ",") }
}
class RegExpLiteral extends TRegExpLiteral, RegExpParent {
RegExp re;
RegExpLiteral() { this = TRegExpLiteral(re) }
override RegExpTerm getChild(int i) { i = 0 and result.getRegExp() = re and result.isRootTerm() }
predicate isDotAll() { re.isDotAll() }
predicate isIgnoreCase() { re.isIgnoreCase() }
string getFlags() { result = re.getFlags() }
override string getAPrimaryQlClass() { result = "RegExpLiteral" }
}
class RegExpTerm extends RegExpParent {
RegExp re;
int start;
int end;
RegExpTerm() {
this = TRegExpAlt(re, start, end)
or
this = TRegExpBackRef(re, start, end)
or
this = TRegExpCharacterClass(re, start, end)
or
this = TRegExpCharacterRange(re, start, end)
or
this = TRegExpNormalChar(re, start, end)
or
this = TRegExpGroup(re, start, end)
or
this = TRegExpQuantifier(re, start, end)
or
this = TRegExpSequence(re, start, end) and
exists(seqChild(re, start, end, 1)) // if a sequence does not have more than one element, it should be treated as that element instead.
or
this = TRegExpSpecialChar(re, start, end)
or
this = TRegExpNamedCharacterProperty(re, start, end)
}
RegExpTerm getRootTerm() {
this.isRootTerm() and result = this
or
result = this.getParent().(RegExpTerm).getRootTerm()
}
predicate isUsedAsRegExp() { any() }
predicate isRootTerm() { start = 0 and end = re.getText().length() }
override RegExpTerm getChild(int i) {
result = this.(RegExpAlt).getChild(i)
or
result = this.(RegExpBackRef).getChild(i)
or
result = this.(RegExpCharacterClass).getChild(i)
or
result = this.(RegExpCharacterRange).getChild(i)
or
result = this.(RegExpNormalChar).getChild(i)
or
result = this.(RegExpGroup).getChild(i)
or
result = this.(RegExpQuantifier).getChild(i)
or
result = this.(RegExpSequence).getChild(i)
or
result = this.(RegExpSpecialChar).getChild(i)
or
result = this.(RegExpNamedCharacterProperty).getChild(i)
}
RegExpParent getParent() { result.getAChild() = this }
RegExp getRegExp() { result = re }
int getStart() { result = start }
int getEnd() { result = end }
override string toString() { result = re.getText().substring(start, end) }
override string getAPrimaryQlClass() { result = "RegExpTerm" }
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)
}
predicate hasLocationInfo(
string filepath, int startline, int startcolumn, int endline, int endcolumn
) {
exists(int re_start, int 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
)
}
File getFile() { result = this.getLocation().getFile() }
string getRawValue() { result = this.toString() }
RegExpLiteral getLiteral() { result = TRegExpLiteral(re) }
/** Gets the regular expression term that is matched (textually) before this one, if any. */
RegExpTerm getPredecessor() {
exists(RegExpTerm parent | parent = this.getParent() |
result = parent.(RegExpSequence).previousElement(this)
or
not exists(parent.(RegExpSequence).previousElement(this)) and
not parent instanceof RegExpSubPattern and
result = parent.getPredecessor()
)
}
/** Gets the regular expression term that is matched (textually) after this one, if any. */
RegExpTerm getSuccessor() {
exists(RegExpTerm parent | parent = this.getParent() |
result = parent.(RegExpSequence).nextElement(this)
or
not exists(parent.(RegExpSequence).nextElement(this)) and
not parent instanceof RegExpSubPattern and
result = parent.getSuccessor()
)
}
}
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, _)
}
class RegExpQuantifier extends RegExpTerm, TRegExpQuantifier {
int part_end;
boolean may_repeat_forever;
RegExpQuantifier() {
this = TRegExpQuantifier(re, start, end) and
re.qualifiedPart(start, part_end, end, _, may_repeat_forever)
}
override RegExpTerm getChild(int i) {
i = 0 and
result.getRegExp() = re and
result.getStart() = start and
result.getEnd() = part_end
}
predicate mayRepeatForever() { may_repeat_forever = true }
string getQualifier() { result = re.getText().substring(part_end, end) }
override string getAPrimaryQlClass() { result = "RegExpQuantifier" }
}
class InfiniteRepetitionQuantifier extends RegExpQuantifier {
InfiniteRepetitionQuantifier() { this.mayRepeatForever() }
override string getAPrimaryQlClass() { result = "InfiniteRepetitionQuantifier" }
}
class RegExpStar extends InfiniteRepetitionQuantifier {
RegExpStar() { this.getQualifier().charAt(0) = "*" }
override string getAPrimaryQlClass() { result = "RegExpStar" }
}
class RegExpPlus extends InfiniteRepetitionQuantifier {
RegExpPlus() { this.getQualifier().charAt(0) = "+" }
override string getAPrimaryQlClass() { result = "RegExpPlus" }
}
class RegExpOpt extends RegExpQuantifier {
RegExpOpt() { this.getQualifier().charAt(0) = "?" }
override string getAPrimaryQlClass() { result = "RegExpOpt" }
}
class RegExpRange extends RegExpQuantifier {
string upper;
string lower;
RegExpRange() { re.multiples(part_end, end, lower, upper) }
string getUpper() { result = upper }
string getLower() { result = lower }
/**
* Gets the upper bound of the range, if any.
*
* If there is no upper bound, any number of repetitions is allowed.
* For a term of the form `r{lo}`, both the lower and the upper bound
* are `lo`.
*/
int getUpperBound() { result = this.getUpper().toInt() }
/** Gets the lower bound of the range. */
int getLowerBound() { result = this.getLower().toInt() }
override string getAPrimaryQlClass() { result = "RegExpRange" }
}
class RegExpSequence extends RegExpTerm, TRegExpSequence {
RegExpSequence() {
this = TRegExpSequence(re, start, end) and
exists(seqChild(re, start, end, 1)) // if a sequence does not have more than one element, it should be treated as that element instead.
}
override RegExpTerm getChild(int i) { result = seqChild(re, start, end, i) }
/** Gets the element preceding `element` in this sequence. */
RegExpTerm previousElement(RegExpTerm element) { element = this.nextElement(result) }
/** Gets the element following `element` in this sequence. */
RegExpTerm nextElement(RegExpTerm element) {
exists(int i |
element = this.getChild(i) and
result = this.getChild(i + 1)
)
}
override string getAPrimaryQlClass() { result = "RegExpSequence" }
}
pragma[nomagic]
private int seqChildEnd(RegExp re, int start, int end, int i) {
result = seqChild(re, start, end, i).getEnd()
}
// moved out so we can use it in the charpred
private RegExpTerm seqChild(RegExp re, int start, int end, int i) {
re.sequence(start, end) and
(
i = 0 and
result.getRegExp() = re and
result.getStart() = start and
exists(int itemEnd |
re.item(start, itemEnd) and
result.getEnd() = itemEnd
)
or
i > 0 and
result.getRegExp() = re and
exists(int itemStart | itemStart = seqChildEnd(re, start, end, i - 1) |
result.getStart() = itemStart and
re.item(itemStart, result.getEnd())
)
)
}
class RegExpAlt extends RegExpTerm, TRegExpAlt {
RegExpAlt() { this = TRegExpAlt(re, start, end) }
override RegExpTerm getChild(int i) {
i = 0 and
result.getRegExp() = re and
result.getStart() = start and
exists(int part_end |
re.alternationOption(start, end, start, part_end) and
result.getEnd() = part_end
)
or
i > 0 and
result.getRegExp() = re and
exists(int part_start |
part_start = this.getChild(i - 1).getEnd() + 1 // allow for the |
|
result.getStart() = part_start and
re.alternationOption(start, end, part_start, result.getEnd())
)
}
override string getAPrimaryQlClass() { result = "RegExpAlt" }
}
class RegExpCharEscape = RegExpEscape;
class RegExpEscape extends RegExpNormalChar {
RegExpEscape() { re.escapedCharacter(start, end) }
/**
* Gets the name of the escaped; for example, `w` for `\w`.
* TODO: Handle named escapes.
*/
override string getValue() {
this.isIdentityEscape() and result = this.getUnescaped()
or
this.getUnescaped() = "n" and result = "\n"
or
this.getUnescaped() = "r" and result = "\r"
or
this.getUnescaped() = "t" and result = "\t"
or
this.isUnicode() and
result = this.getUnicode()
}
predicate isIdentityEscape() {
not this.getUnescaped() in ["n", "r", "t"] and not this.isUnicode()
}
/**
* Gets the text for this escape. That is e.g. "\w".
*/
private string getText() { result = re.getText().substring(start, end) }
/**
* Holds if this is a unicode escape.
*/
private predicate isUnicode() { this.getText().prefix(2) = ["\\u", "\\U"] }
/**
* Gets the unicode char for this escape.
* E.g. for `\u0061` this returns "a".
*/
private string getUnicode() {
this.isUnicode() and
result = parseHexInt(this.getText().suffix(2)).toUnicode()
}
string getUnescaped() { result = this.getText().suffix(1) }
override string getAPrimaryQlClass() { result = "RegExpEscape" }
}
/**
* A word boundary, that is, a regular expression term of the form `\b`.
*/
class RegExpWordBoundary extends RegExpSpecialChar {
RegExpWordBoundary() { this.getChar() = "\\b" }
}
/**
* A character class escape in a regular expression.
* That is, an escaped character that denotes multiple characters.
*
* Examples:
*
* ```
* \w
* \S
* ```
*/
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.
*
* Examples:
*
* ```rb
* /[a-fA-F0-9]/
* /[^abc]/
* ```
*/
class RegExpCharacterClass extends RegExpTerm, TRegExpCharacterClass {
RegExpCharacterClass() { this = TRegExpCharacterClass(re, start, end) }
predicate isInverted() { re.getChar(start + 1) = "^" }
predicate isUniversalClass() {
// [^]
this.isInverted() and not exists(this.getAChild())
or
// [\w\W] and similar
not this.isInverted() and
exists(string cce1, string cce2 |
cce1 = this.getAChild().(RegExpCharacterClassEscape).getValue() and
cce2 = this.getAChild().(RegExpCharacterClassEscape).getValue()
|
cce1 != cce2 and cce1.toLowerCase() = cce2.toLowerCase()
)
}
override RegExpTerm getChild(int i) {
i = 0 and
result.getRegExp() = re and
exists(int itemStart, int itemEnd |
result.getStart() = itemStart and
re.charSetStart(start, itemStart) and
re.charSetChild(start, itemStart, itemEnd) and
result.getEnd() = itemEnd
)
or
i > 0 and
result.getRegExp() = re and
exists(int itemStart | itemStart = this.getChild(i - 1).getEnd() |
result.getStart() = itemStart and
re.charSetChild(start, itemStart, result.getEnd())
)
}
override string getAPrimaryQlClass() { result = "RegExpCharacterClass" }
}
class RegExpCharacterRange extends RegExpTerm, TRegExpCharacterRange {
int lower_end;
int upper_start;
RegExpCharacterRange() {
this = TRegExpCharacterRange(re, start, end) and
re.charRange(_, start, lower_end, upper_start, end)
}
predicate isRange(string lo, string hi) {
lo = re.getText().substring(start, lower_end) and
hi = re.getText().substring(upper_start, end)
}
override RegExpTerm getChild(int i) {
i = 0 and
result.getRegExp() = re and
result.getStart() = start and
result.getEnd() = lower_end
or
i = 1 and
result.getRegExp() = re and
result.getStart() = upper_start and
result.getEnd() = end
}
override string getAPrimaryQlClass() { result = "RegExpCharacterRange" }
}
class RegExpNormalChar extends RegExpTerm, TRegExpNormalChar {
RegExpNormalChar() { this = TRegExpNormalChar(re, start, end) }
predicate isCharacter() { any() }
string getValue() { result = re.getText().substring(start, end) }
override RegExpTerm getChild(int i) { none() }
override string getAPrimaryQlClass() { result = "RegExpNormalChar" }
}
class RegExpConstant extends RegExpTerm {
string value;
RegExpConstant() {
this = TRegExpNormalChar(re, start, end) and
not this instanceof RegExpCharacterClassEscape and
// exclude chars in qualifiers
// TODO: push this into regex library
not exists(int qstart, int qend | re.qualifiedPart(_, qstart, qend, _, _) |
qstart <= start and end <= qend
) and
value = this.(RegExpNormalChar).getValue()
or
this = TRegExpSpecialChar(re, start, end) and
re.inCharSet(start) and
value = this.(RegExpSpecialChar).getChar()
}
predicate isCharacter() { any() }
string getValue() { result = value }
override RegExpTerm getChild(int i) { none() }
override string getAPrimaryQlClass() { result = "RegExpConstant" }
}
class RegExpGroup extends RegExpTerm, TRegExpGroup {
RegExpGroup() { this = TRegExpGroup(re, start, end) }
/**
* Gets the index of this capture group within the enclosing regular
* expression literal.
*
* For example, in the regular expression `/((a?).)(?:b)/`, the
* group `((a?).)` has index 1, the group `(a?)` nested inside it
* has index 2, and the group `(?:b)` has no index, since it is
* not a capture group.
*/
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
re.groupContents(start, end, result.getStart(), result.getEnd())
}
override string getAPrimaryQlClass() { result = "RegExpGroup" }
}
class RegExpSpecialChar extends RegExpTerm, TRegExpSpecialChar {
string char;
RegExpSpecialChar() {
this = TRegExpSpecialChar(re, start, end) and
re.specialCharacter(start, end, char)
}
predicate isCharacter() { any() }
string getChar() { result = char }
override RegExpTerm getChild(int i) { none() }
override string getAPrimaryQlClass() { result = "RegExpSpecialChar" }
}
class RegExpDot extends RegExpSpecialChar {
RegExpDot() { this.getChar() = "." }
override string getAPrimaryQlClass() { result = "RegExpDot" }
}
class RegExpDollar extends RegExpSpecialChar {
RegExpDollar() { this.getChar() = ["$", "\\Z", "\\z"] }
override string getAPrimaryQlClass() { result = "RegExpDollar" }
}
class RegExpCaret extends RegExpSpecialChar {
RegExpCaret() { this.getChar() = ["^", "\\A"] }
override string getAPrimaryQlClass() { result = "RegExpCaret" }
}
class RegExpZeroWidthMatch extends RegExpGroup {
RegExpZeroWidthMatch() { re.zeroWidthMatch(start, end) }
override predicate isCharacter() { any() }
override RegExpTerm getChild(int i) { none() }
override string getAPrimaryQlClass() { result = "RegExpZeroWidthMatch" }
}
/**
* A zero-width lookahead or lookbehind assertion.
*
* Examples:
*
* ```
* (?=\w)
* (?!\n)
* (?<=\.)
* (?<!\\)
* ```
*/
class RegExpSubPattern extends RegExpZeroWidthMatch {
RegExpSubPattern() { not re.emptyGroup(start, end) }
/** Gets the lookahead term. */
RegExpTerm getOperand() {
exists(int in_start, int in_end | re.groupContents(start, end, in_start, in_end) |
result.getRegExp() = re and
result.getStart() = in_start and
result.getEnd() = in_end
)
}
}
abstract class RegExpLookahead extends RegExpSubPattern { }
class RegExpPositiveLookahead extends RegExpLookahead {
RegExpPositiveLookahead() { re.positiveLookaheadAssertionGroup(start, end) }
override string getAPrimaryQlClass() { result = "RegExpPositiveLookahead" }
}
class RegExpNegativeLookahead extends RegExpLookahead {
RegExpNegativeLookahead() { re.negativeLookaheadAssertionGroup(start, end) }
override string getAPrimaryQlClass() { result = "RegExpNegativeLookahead" }
}
abstract class RegExpLookbehind extends RegExpSubPattern { }
class RegExpPositiveLookbehind extends RegExpLookbehind {
RegExpPositiveLookbehind() { re.positiveLookbehindAssertionGroup(start, end) }
override string getAPrimaryQlClass() { result = "RegExpPositiveLookbehind" }
}
class RegExpNegativeLookbehind extends RegExpLookbehind {
RegExpNegativeLookbehind() { re.negativeLookbehindAssertionGroup(start, end) }
override string getAPrimaryQlClass() { result = "RegExpNegativeLookbehind" }
}
class RegExpBackRef extends RegExpTerm, TRegExpBackRef {
RegExpBackRef() { this = TRegExpBackRef(re, start, end) }
/**
* Gets the number of the capture group this back reference refers to, if any.
*/
int getNumber() { result = re.getBackRefNumber(start, end) }
/**
* Gets the name of the capture group this back reference refers to, if any.
*/
string getName() { result = re.getBackRefName(start, end) }
/** Gets the capture group this back reference refers to. */
RegExpGroup getGroup() {
result.getLiteral() = this.getLiteral() and
(
result.getNumber() = this.getNumber() or
result.getName() = this.getName()
)
}
override RegExpTerm getChild(int i) { none() }
override string getAPrimaryQlClass() { result = "RegExpBackRef" }
}
/**
* A named character property. For example, the POSIX bracket expression
* `[[:digit:]]`.
*/
class RegExpNamedCharacterProperty extends RegExpTerm, TRegExpNamedCharacterProperty {
RegExpNamedCharacterProperty() { this = TRegExpNamedCharacterProperty(re, start, end) }
override RegExpTerm getChild(int i) { none() }
override string getAPrimaryQlClass() { result = "RegExpNamedCharacterProperty" }
/**
* Gets the property name. For example, in `\p{Space}`, the result is
* `"Space"`.
*/
string getName() { result = re.getCharacterPropertyName(start, end) }
/**
* Holds if the property is inverted. For example, it holds for `\p{^Digit}`,
* which matches non-digits.
*/
predicate isInverted() { re.namedCharacterPropertyIsInverted(start, end) }
}
RegExpTerm getParsedRegExp(AST::RegExpLiteral re) {
result.getRegExp() = re and result.isRootTerm()
}
/**
* A node whose value may flow to a position where it is interpreted
* as a part of a regular expression.
*/
abstract class RegExpPatternSource extends DataFlow::Node {
/**
* Gets a node where the pattern of this node is parsed as a part of
* a regular expression.
*/
abstract DataFlow::Node getAParse();
/**
* Gets the root term of the regular expression parsed from this pattern.
*/
abstract RegExpTerm getRegExpTerm();
}
/**
* A regular expression literal, viewed as the pattern source for itself.
*/
private class RegExpLiteralPatternSource extends RegExpPatternSource {
private AST::RegExpLiteral astNode;
RegExpLiteralPatternSource() { astNode = this.asExpr().getExpr() }
override DataFlow::Node getAParse() { result = this }
override RegExpTerm getRegExpTerm() { result = astNode.getParsed() }
}
/**
* A node whose string value may flow to a position where it is interpreted
* as a part of a regular expression.
*/
private class StringRegExpPatternSource extends RegExpPatternSource {
private DataFlow::Node parse;
StringRegExpPatternSource() { this = regExpSource(parse) }
override DataFlow::Node getAParse() { result = parse }
override RegExpTerm getRegExpTerm() { result.getRegExp() = this.asExpr().getExpr() }
}

View File

@@ -1,2 +1,2 @@
import codeql.ruby.security.performance.RegExpTreeView
import codeql.ruby.Regexp
import codeql.ruby.DataFlow

View File

@@ -16,7 +16,7 @@
import codeql.ruby.security.performance.ExponentialBackTracking
import codeql.ruby.security.performance.ReDoSUtil
import codeql.ruby.security.performance.RegExpTreeView
import codeql.ruby.Regexp
from RegExpTerm t, string pump, State s, string prefixMsg
where hasReDoSResult(t, pump, s, prefixMsg)

View File

@@ -1,23 +1,42 @@
failures
edges
| summaries.rb:1:11:1:26 | call to identity : | summaries.rb:2:6:2:12 | tainted |
| summaries.rb:1:11:1:26 | call to identity : | summaries.rb:4:24:4:30 | tainted : |
| summaries.rb:1:11:1:26 | call to identity : | summaries.rb:16:36:16:42 | tainted : |
| summaries.rb:1:11:1:26 | call to identity : | summaries.rb:20:25:20:31 | tainted : |
| summaries.rb:1:11:1:26 | call to identity : | summaries.rb:26:31:26:37 | tainted : |
| summaries.rb:1:11:1:26 | call to identity : | summaries.rb:30:24:30:30 | tainted : |
| summaries.rb:1:11:1:26 | call to identity : | summaries.rb:31:27:31:33 | tainted : |
| summaries.rb:1:11:1:26 | call to identity : | summaries.rb:34:16:34:22 | tainted |
| summaries.rb:1:11:1:26 | call to identity : | summaries.rb:35:16:35:22 | tainted |
| summaries.rb:1:11:1:26 | call to identity : | summaries.rb:36:21:36:27 | tainted |
| summaries.rb:1:11:1:26 | call to identity : | summaries.rb:37:36:37:42 | tainted |
| summaries.rb:1:20:1:26 | "taint" : | summaries.rb:1:11:1:26 | call to identity : |
| summaries.rb:1:11:1:36 | call to identity : | summaries.rb:2:6:2:12 | tainted |
| summaries.rb:1:11:1:36 | call to identity : | summaries.rb:2:6:2:12 | tainted |
| summaries.rb:1:11:1:36 | call to identity : | summaries.rb:4:24:4:30 | tainted : |
| summaries.rb:1:11:1:36 | call to identity : | summaries.rb:4:24:4:30 | tainted : |
| summaries.rb:1:11:1:36 | call to identity : | summaries.rb:16:36:16:42 | tainted : |
| summaries.rb:1:11:1:36 | call to identity : | summaries.rb:16:36:16:42 | tainted : |
| summaries.rb:1:11:1:36 | call to identity : | summaries.rb:20:25:20:31 | tainted : |
| summaries.rb:1:11:1:36 | call to identity : | summaries.rb:26:31:26:37 | tainted : |
| summaries.rb:1:11:1:36 | call to identity : | summaries.rb:30:24:30:30 | tainted : |
| summaries.rb:1:11:1:36 | call to identity : | summaries.rb:31:27:31:33 | tainted : |
| summaries.rb:1:11:1:36 | call to identity : | summaries.rb:34:16:34:22 | tainted |
| summaries.rb:1:11:1:36 | call to identity : | summaries.rb:34:16:34:22 | tainted |
| summaries.rb:1:11:1:36 | call to identity : | summaries.rb:35:16:35:22 | tainted |
| summaries.rb:1:11:1:36 | call to identity : | summaries.rb:35:16:35:22 | tainted |
| summaries.rb:1:11:1:36 | call to identity : | summaries.rb:36:21:36:27 | tainted |
| summaries.rb:1:11:1:36 | call to identity : | summaries.rb:36:21:36:27 | tainted |
| summaries.rb:1:11:1:36 | call to identity : | summaries.rb:37:36:37:42 | tainted |
| summaries.rb:1:11:1:36 | call to identity : | summaries.rb:37:36:37:42 | tainted |
| summaries.rb:1:11:1:36 | call to identity : | summaries.rb:51:24:51:30 | tainted : |
| summaries.rb:1:11:1:36 | call to identity : | summaries.rb:54:23:54:29 | tainted : |
| summaries.rb:1:20:1:36 | call to source : | summaries.rb:1:11:1:36 | call to identity : |
| summaries.rb:1:20:1:36 | call to source : | summaries.rb:1:11:1:36 | call to identity : |
| summaries.rb:4:12:7:3 | call to apply_block : | summaries.rb:9:6:9:13 | tainted2 |
| summaries.rb:4:12:7:3 | call to apply_block : | summaries.rb:9:6:9:13 | tainted2 |
| summaries.rb:4:24:4:30 | tainted : | summaries.rb:4:12:7:3 | call to apply_block : |
| summaries.rb:4:24:4:30 | tainted : | summaries.rb:4:12:7:3 | call to apply_block : |
| summaries.rb:4:24:4:30 | tainted : | summaries.rb:4:36:4:36 | x : |
| summaries.rb:4:24:4:30 | tainted : | summaries.rb:4:36:4:36 | x : |
| summaries.rb:4:36:4:36 | x : | summaries.rb:5:8:5:8 | x |
| summaries.rb:4:36:4:36 | x : | summaries.rb:5:8:5:8 | x |
| summaries.rb:11:17:11:17 | x : | summaries.rb:12:8:12:8 | x |
| summaries.rb:11:17:11:17 | x : | summaries.rb:12:8:12:8 | x |
| summaries.rb:16:12:16:43 | call to apply_lambda : | summaries.rb:18:6:18:13 | tainted3 |
| summaries.rb:16:12:16:43 | call to apply_lambda : | summaries.rb:18:6:18:13 | tainted3 |
| summaries.rb:16:36:16:42 | tainted : | summaries.rb:11:17:11:17 | x : |
| summaries.rb:16:36:16:42 | tainted : | summaries.rb:11:17:11:17 | x : |
| summaries.rb:16:36:16:42 | tainted : | summaries.rb:16:12:16:43 | call to apply_lambda : |
| summaries.rb:16:36:16:42 | tainted : | summaries.rb:16:12:16:43 | call to apply_lambda : |
| summaries.rb:20:12:20:32 | call to firstArg : | summaries.rb:21:6:21:13 | tainted4 |
| summaries.rb:20:25:20:31 | tainted : | summaries.rb:20:12:20:32 | call to firstArg : |
@@ -25,26 +44,44 @@ edges
| summaries.rb:26:31:26:37 | tainted : | summaries.rb:26:12:26:38 | call to secondArg : |
| summaries.rb:30:24:30:30 | tainted : | summaries.rb:30:6:30:42 | call to onlyWithBlock |
| summaries.rb:31:27:31:33 | tainted : | summaries.rb:31:6:31:34 | call to onlyWithoutBlock |
| summaries.rb:40:7:40:13 | "taint" : | summaries.rb:41:24:41:24 | t : |
| summaries.rb:40:7:40:13 | "taint" : | summaries.rb:42:24:42:24 | t : |
| summaries.rb:40:7:40:13 | "taint" : | summaries.rb:44:8:44:8 | t : |
| summaries.rb:40:7:40:17 | call to source : | summaries.rb:41:24:41:24 | t : |
| summaries.rb:40:7:40:17 | call to source : | summaries.rb:42:24:42:24 | t : |
| summaries.rb:40:7:40:17 | call to source : | summaries.rb:44:8:44:8 | t : |
| summaries.rb:41:24:41:24 | t : | summaries.rb:41:8:41:25 | call to matchedByName |
| summaries.rb:42:24:42:24 | t : | summaries.rb:42:8:42:25 | call to matchedByName |
| summaries.rb:44:8:44:8 | t : | summaries.rb:44:8:44:27 | call to matchedByNameRcv |
| summaries.rb:48:24:48:30 | "taint" : | summaries.rb:48:8:48:31 | call to preserveTaint |
| summaries.rb:48:24:48:41 | call to source : | summaries.rb:48:8:48:42 | call to preserveTaint |
| summaries.rb:51:24:51:30 | tainted : | summaries.rb:51:6:51:31 | call to namedArg |
| summaries.rb:54:23:54:29 | tainted : | summaries.rb:54:40:54:40 | x : |
| summaries.rb:54:40:54:40 | x : | summaries.rb:55:8:55:8 | x |
| summaries.rb:62:24:62:53 | call to source : | summaries.rb:62:8:62:54 | call to preserveTaint |
| summaries.rb:65:26:65:56 | call to source : | summaries.rb:65:8:65:57 | call to preserveTaint |
nodes
| summaries.rb:1:11:1:26 | call to identity : | semmle.label | call to identity : |
| summaries.rb:1:20:1:26 | "taint" : | semmle.label | "taint" : |
| summaries.rb:1:11:1:36 | call to identity : | semmle.label | call to identity : |
| summaries.rb:1:11:1:36 | call to identity : | semmle.label | call to identity : |
| summaries.rb:1:20:1:36 | call to source : | semmle.label | call to source : |
| summaries.rb:1:20:1:36 | call to source : | semmle.label | call to source : |
| summaries.rb:2:6:2:12 | tainted | semmle.label | tainted |
| summaries.rb:2:6:2:12 | tainted | semmle.label | tainted |
| summaries.rb:4:12:7:3 | call to apply_block : | semmle.label | call to apply_block : |
| summaries.rb:4:12:7:3 | call to apply_block : | semmle.label | call to apply_block : |
| summaries.rb:4:24:4:30 | tainted : | semmle.label | tainted : |
| summaries.rb:4:24:4:30 | tainted : | semmle.label | tainted : |
| summaries.rb:4:36:4:36 | x : | semmle.label | x : |
| summaries.rb:4:36:4:36 | x : | semmle.label | x : |
| summaries.rb:5:8:5:8 | x | semmle.label | x |
| summaries.rb:5:8:5:8 | x | semmle.label | x |
| summaries.rb:9:6:9:13 | tainted2 | semmle.label | tainted2 |
| summaries.rb:9:6:9:13 | tainted2 | semmle.label | tainted2 |
| summaries.rb:11:17:11:17 | x : | semmle.label | x : |
| summaries.rb:11:17:11:17 | x : | semmle.label | x : |
| summaries.rb:12:8:12:8 | x | semmle.label | x |
| summaries.rb:12:8:12:8 | x | semmle.label | x |
| summaries.rb:16:12:16:43 | call to apply_lambda : | semmle.label | call to apply_lambda : |
| summaries.rb:16:12:16:43 | call to apply_lambda : | semmle.label | call to apply_lambda : |
| summaries.rb:16:36:16:42 | tainted : | semmle.label | tainted : |
| summaries.rb:16:36:16:42 | tainted : | semmle.label | tainted : |
| summaries.rb:18:6:18:13 | tainted3 | semmle.label | tainted3 |
| summaries.rb:18:6:18:13 | tainted3 | semmle.label | tainted3 |
| summaries.rb:20:12:20:32 | call to firstArg : | semmle.label | call to firstArg : |
| summaries.rb:20:25:20:31 | tainted : | semmle.label | tainted : |
@@ -57,39 +94,65 @@ nodes
| summaries.rb:31:6:31:34 | call to onlyWithoutBlock | semmle.label | call to onlyWithoutBlock |
| summaries.rb:31:27:31:33 | tainted : | semmle.label | tainted : |
| summaries.rb:34:16:34:22 | tainted | semmle.label | tainted |
| summaries.rb:34:16:34:22 | tainted | semmle.label | tainted |
| summaries.rb:35:16:35:22 | tainted | semmle.label | tainted |
| summaries.rb:35:16:35:22 | tainted | semmle.label | tainted |
| summaries.rb:36:21:36:27 | tainted | semmle.label | tainted |
| summaries.rb:36:21:36:27 | tainted | semmle.label | tainted |
| summaries.rb:37:36:37:42 | tainted | semmle.label | tainted |
| summaries.rb:40:7:40:13 | "taint" : | semmle.label | "taint" : |
| summaries.rb:37:36:37:42 | tainted | semmle.label | tainted |
| summaries.rb:40:7:40:17 | call to source : | semmle.label | call to source : |
| summaries.rb:41:8:41:25 | call to matchedByName | semmle.label | call to matchedByName |
| summaries.rb:41:24:41:24 | t : | semmle.label | t : |
| summaries.rb:42:8:42:25 | call to matchedByName | semmle.label | call to matchedByName |
| summaries.rb:42:24:42:24 | t : | semmle.label | t : |
| summaries.rb:44:8:44:8 | t : | semmle.label | t : |
| summaries.rb:44:8:44:27 | call to matchedByNameRcv | semmle.label | call to matchedByNameRcv |
| summaries.rb:48:8:48:31 | call to preserveTaint | semmle.label | call to preserveTaint |
| summaries.rb:48:24:48:30 | "taint" : | semmle.label | "taint" : |
| summaries.rb:48:8:48:42 | call to preserveTaint | semmle.label | call to preserveTaint |
| summaries.rb:48:24:48:41 | call to source : | semmle.label | call to source : |
| summaries.rb:51:6:51:31 | call to namedArg | semmle.label | call to namedArg |
| summaries.rb:51:24:51:30 | tainted : | semmle.label | tainted : |
| summaries.rb:54:23:54:29 | tainted : | semmle.label | tainted : |
| summaries.rb:54:40:54:40 | x : | semmle.label | x : |
| summaries.rb:55:8:55:8 | x | semmle.label | x |
| summaries.rb:62:8:62:54 | call to preserveTaint | semmle.label | call to preserveTaint |
| summaries.rb:62:24:62:53 | call to source : | semmle.label | call to source : |
| summaries.rb:65:8:65:57 | call to preserveTaint | semmle.label | call to preserveTaint |
| summaries.rb:65:26:65:56 | call to source : | semmle.label | call to source : |
subpaths
invalidSpecComponent
invalidOutputSpecComponent
#select
| summaries.rb:2:6:2:12 | tainted | summaries.rb:1:20:1:26 | "taint" : | summaries.rb:2:6:2:12 | tainted | $@ | summaries.rb:1:20:1:26 | "taint" : | "taint" : |
| summaries.rb:5:8:5:8 | x | summaries.rb:1:20:1:26 | "taint" : | summaries.rb:5:8:5:8 | x | $@ | summaries.rb:1:20:1:26 | "taint" : | "taint" : |
| summaries.rb:9:6:9:13 | tainted2 | summaries.rb:1:20:1:26 | "taint" : | summaries.rb:9:6:9:13 | tainted2 | $@ | summaries.rb:1:20:1:26 | "taint" : | "taint" : |
| summaries.rb:12:8:12:8 | x | summaries.rb:1:20:1:26 | "taint" : | summaries.rb:12:8:12:8 | x | $@ | summaries.rb:1:20:1:26 | "taint" : | "taint" : |
| summaries.rb:18:6:18:13 | tainted3 | summaries.rb:1:20:1:26 | "taint" : | summaries.rb:18:6:18:13 | tainted3 | $@ | summaries.rb:1:20:1:26 | "taint" : | "taint" : |
| summaries.rb:21:6:21:13 | tainted4 | summaries.rb:1:20:1:26 | "taint" : | summaries.rb:21:6:21:13 | tainted4 | $@ | summaries.rb:1:20:1:26 | "taint" : | "taint" : |
| summaries.rb:27:6:27:13 | tainted5 | summaries.rb:1:20:1:26 | "taint" : | summaries.rb:27:6:27:13 | tainted5 | $@ | summaries.rb:1:20:1:26 | "taint" : | "taint" : |
| summaries.rb:30:6:30:42 | call to onlyWithBlock | summaries.rb:1:20:1:26 | "taint" : | summaries.rb:30:6:30:42 | call to onlyWithBlock | $@ | summaries.rb:1:20:1:26 | "taint" : | "taint" : |
| summaries.rb:31:6:31:34 | call to onlyWithoutBlock | summaries.rb:1:20:1:26 | "taint" : | summaries.rb:31:6:31:34 | call to onlyWithoutBlock | $@ | summaries.rb:1:20:1:26 | "taint" : | "taint" : |
| summaries.rb:34:16:34:22 | tainted | summaries.rb:1:20:1:26 | "taint" : | summaries.rb:34:16:34:22 | tainted | $@ | summaries.rb:1:20:1:26 | "taint" : | "taint" : |
| summaries.rb:35:16:35:22 | tainted | summaries.rb:1:20:1:26 | "taint" : | summaries.rb:35:16:35:22 | tainted | $@ | summaries.rb:1:20:1:26 | "taint" : | "taint" : |
| summaries.rb:36:21:36:27 | tainted | summaries.rb:1:20:1:26 | "taint" : | summaries.rb:36:21:36:27 | tainted | $@ | summaries.rb:1:20:1:26 | "taint" : | "taint" : |
| summaries.rb:37:36:37:42 | tainted | summaries.rb:1:20:1:26 | "taint" : | summaries.rb:37:36:37:42 | tainted | $@ | summaries.rb:1:20:1:26 | "taint" : | "taint" : |
| summaries.rb:41:8:41:25 | call to matchedByName | summaries.rb:40:7:40:13 | "taint" : | summaries.rb:41:8:41:25 | call to matchedByName | $@ | summaries.rb:40:7:40:13 | "taint" : | "taint" : |
| summaries.rb:42:8:42:25 | call to matchedByName | summaries.rb:40:7:40:13 | "taint" : | summaries.rb:42:8:42:25 | call to matchedByName | $@ | summaries.rb:40:7:40:13 | "taint" : | "taint" : |
| summaries.rb:44:8:44:27 | call to matchedByNameRcv | summaries.rb:40:7:40:13 | "taint" : | summaries.rb:44:8:44:27 | call to matchedByNameRcv | $@ | summaries.rb:40:7:40:13 | "taint" : | "taint" : |
| summaries.rb:48:8:48:31 | call to preserveTaint | summaries.rb:48:24:48:30 | "taint" : | summaries.rb:48:8:48:31 | call to preserveTaint | $@ | summaries.rb:48:24:48:30 | "taint" : | "taint" : |
| summaries.rb:2:6:2:12 | tainted | summaries.rb:1:20:1:36 | call to source : | summaries.rb:2:6:2:12 | tainted | $@ | summaries.rb:1:20:1:36 | call to source : | call to source : |
| summaries.rb:2:6:2:12 | tainted | summaries.rb:1:20:1:36 | call to source : | summaries.rb:2:6:2:12 | tainted | $@ | summaries.rb:1:20:1:36 | call to source : | call to source : |
| summaries.rb:5:8:5:8 | x | summaries.rb:1:20:1:36 | call to source : | summaries.rb:5:8:5:8 | x | $@ | summaries.rb:1:20:1:36 | call to source : | call to source : |
| summaries.rb:5:8:5:8 | x | summaries.rb:1:20:1:36 | call to source : | summaries.rb:5:8:5:8 | x | $@ | summaries.rb:1:20:1:36 | call to source : | call to source : |
| summaries.rb:9:6:9:13 | tainted2 | summaries.rb:1:20:1:36 | call to source : | summaries.rb:9:6:9:13 | tainted2 | $@ | summaries.rb:1:20:1:36 | call to source : | call to source : |
| summaries.rb:9:6:9:13 | tainted2 | summaries.rb:1:20:1:36 | call to source : | summaries.rb:9:6:9:13 | tainted2 | $@ | summaries.rb:1:20:1:36 | call to source : | call to source : |
| summaries.rb:12:8:12:8 | x | summaries.rb:1:20:1:36 | call to source : | summaries.rb:12:8:12:8 | x | $@ | summaries.rb:1:20:1:36 | call to source : | call to source : |
| summaries.rb:12:8:12:8 | x | summaries.rb:1:20:1:36 | call to source : | summaries.rb:12:8:12:8 | x | $@ | summaries.rb:1:20:1:36 | call to source : | call to source : |
| summaries.rb:18:6:18:13 | tainted3 | summaries.rb:1:20:1:36 | call to source : | summaries.rb:18:6:18:13 | tainted3 | $@ | summaries.rb:1:20:1:36 | call to source : | call to source : |
| summaries.rb:18:6:18:13 | tainted3 | summaries.rb:1:20:1:36 | call to source : | summaries.rb:18:6:18:13 | tainted3 | $@ | summaries.rb:1:20:1:36 | call to source : | call to source : |
| summaries.rb:21:6:21:13 | tainted4 | summaries.rb:1:20:1:36 | call to source : | summaries.rb:21:6:21:13 | tainted4 | $@ | summaries.rb:1:20:1:36 | call to source : | call to source : |
| summaries.rb:27:6:27:13 | tainted5 | summaries.rb:1:20:1:36 | call to source : | summaries.rb:27:6:27:13 | tainted5 | $@ | summaries.rb:1:20:1:36 | call to source : | call to source : |
| summaries.rb:30:6:30:42 | call to onlyWithBlock | summaries.rb:1:20:1:36 | call to source : | summaries.rb:30:6:30:42 | call to onlyWithBlock | $@ | summaries.rb:1:20:1:36 | call to source : | call to source : |
| summaries.rb:31:6:31:34 | call to onlyWithoutBlock | summaries.rb:1:20:1:36 | call to source : | summaries.rb:31:6:31:34 | call to onlyWithoutBlock | $@ | summaries.rb:1:20:1:36 | call to source : | call to source : |
| summaries.rb:34:16:34:22 | tainted | summaries.rb:1:20:1:36 | call to source : | summaries.rb:34:16:34:22 | tainted | $@ | summaries.rb:1:20:1:36 | call to source : | call to source : |
| summaries.rb:34:16:34:22 | tainted | summaries.rb:1:20:1:36 | call to source : | summaries.rb:34:16:34:22 | tainted | $@ | summaries.rb:1:20:1:36 | call to source : | call to source : |
| summaries.rb:35:16:35:22 | tainted | summaries.rb:1:20:1:36 | call to source : | summaries.rb:35:16:35:22 | tainted | $@ | summaries.rb:1:20:1:36 | call to source : | call to source : |
| summaries.rb:35:16:35:22 | tainted | summaries.rb:1:20:1:36 | call to source : | summaries.rb:35:16:35:22 | tainted | $@ | summaries.rb:1:20:1:36 | call to source : | call to source : |
| summaries.rb:36:21:36:27 | tainted | summaries.rb:1:20:1:36 | call to source : | summaries.rb:36:21:36:27 | tainted | $@ | summaries.rb:1:20:1:36 | call to source : | call to source : |
| summaries.rb:36:21:36:27 | tainted | summaries.rb:1:20:1:36 | call to source : | summaries.rb:36:21:36:27 | tainted | $@ | summaries.rb:1:20:1:36 | call to source : | call to source : |
| summaries.rb:37:36:37:42 | tainted | summaries.rb:1:20:1:36 | call to source : | summaries.rb:37:36:37:42 | tainted | $@ | summaries.rb:1:20:1:36 | call to source : | call to source : |
| summaries.rb:37:36:37:42 | tainted | summaries.rb:1:20:1:36 | call to source : | summaries.rb:37:36:37:42 | tainted | $@ | summaries.rb:1:20:1:36 | call to source : | call to source : |
| summaries.rb:41:8:41:25 | call to matchedByName | summaries.rb:40:7:40:17 | call to source : | summaries.rb:41:8:41:25 | call to matchedByName | $@ | summaries.rb:40:7:40:17 | call to source : | call to source : |
| summaries.rb:42:8:42:25 | call to matchedByName | summaries.rb:40:7:40:17 | call to source : | summaries.rb:42:8:42:25 | call to matchedByName | $@ | summaries.rb:40:7:40:17 | call to source : | call to source : |
| summaries.rb:44:8:44:27 | call to matchedByNameRcv | summaries.rb:40:7:40:17 | call to source : | summaries.rb:44:8:44:27 | call to matchedByNameRcv | $@ | summaries.rb:40:7:40:17 | call to source : | call to source : |
| summaries.rb:48:8:48:42 | call to preserveTaint | summaries.rb:48:24:48:41 | call to source : | summaries.rb:48:8:48:42 | call to preserveTaint | $@ | summaries.rb:48:24:48:41 | call to source : | call to source : |
| summaries.rb:51:6:51:31 | call to namedArg | summaries.rb:1:20:1:36 | call to source : | summaries.rb:51:6:51:31 | call to namedArg | $@ | summaries.rb:1:20:1:36 | call to source : | call to source : |
| summaries.rb:55:8:55:8 | x | summaries.rb:1:20:1:36 | call to source : | summaries.rb:55:8:55:8 | x | $@ | summaries.rb:1:20:1:36 | call to source : | call to source : |
| summaries.rb:62:8:62:54 | call to preserveTaint | summaries.rb:62:24:62:53 | call to source : | summaries.rb:62:8:62:54 | call to preserveTaint | $@ | summaries.rb:62:24:62:53 | call to source : | call to source : |
| summaries.rb:65:8:65:57 | call to preserveTaint | summaries.rb:65:26:65:56 | call to source : | summaries.rb:65:8:65:57 | call to preserveTaint | $@ | summaries.rb:65:26:65:56 | call to source : | call to source : |
warning
| CSV type row should have 5 columns but has 2: test;TooFewColumns |
| CSV type row should have 5 columns but has 8: test;TooManyColumns;;;Member[Foo].Instance;too;many;columns |

View File

@@ -4,11 +4,12 @@
import ruby
import codeql.ruby.dataflow.FlowSummary
import DataFlow::PathGraph
import codeql.ruby.TaintTracking
import codeql.ruby.dataflow.internal.FlowSummaryImpl
import codeql.ruby.dataflow.internal.AccessPathSyntax
import codeql.ruby.frameworks.data.ModelsAsData
import TestUtilities.InlineFlowTest
import DataFlow::PathGraph
query predicate invalidSpecComponent(SummarizedCallable sc, string s, string c) {
(sc.propagatesFlowExt(s, _, _) or sc.propagatesFlowExt(_, s, _)) and
@@ -42,10 +43,10 @@ private class SummarizedCallableApplyBlock extends SummarizedCallable {
override predicate propagatesFlowExt(string input, string output, boolean preservesValue) {
input = "Argument[0]" and
output = "BlockArgument.Parameter[0]" and
output = "Argument[block].Parameter[0]" and
preservesValue = true
or
input = "BlockArgument.ReturnValue" and
input = "Argument[block].ReturnValue" and
output = "ReturnValue" and
preservesValue = true
}
@@ -75,9 +76,14 @@ private class StepsFromModel extends ModelInput::SummaryModelCsv {
";;Member[Foo].Method[secondArg];Argument[1];ReturnValue;taint",
";;Member[Foo].Method[onlyWithoutBlock].WithoutBlock;Argument[0];ReturnValue;taint",
";;Member[Foo].Method[onlyWithBlock].WithBlock;Argument[0];ReturnValue;taint",
";;Member[Foo].Method[blockArg].BlockArgument.Parameter[0].Method[preserveTaint];Argument[0];ReturnValue;taint",
";;Member[Foo].Method[blockArg].Argument[block].Parameter[0].Method[preserveTaint];Argument[0];ReturnValue;taint",
";;Member[Foo].Method[namedArg];Argument[foo:];ReturnValue;taint",
";;Member[Foo].Method[intoNamedCallback];Argument[0];Argument[foo:].Parameter[0];taint",
";;Member[Foo].Method[intoNamedParameter];Argument[0];Argument[0].Parameter[foo:];taint",
";;Member[Foo].Method[startInNamedCallback].Argument[foo:].Parameter[0].Method[preserveTaint];Argument[0];ReturnValue;taint",
";;Member[Foo].Method[startInNamedParameter].Argument[0].Parameter[foo:].Method[preserveTaint];Argument[0];ReturnValue;taint",
";any;Method[matchedByName];Argument[0];ReturnValue;taint",
";any;Method[matchedByNameRcv];Receiver;ReturnValue;taint",
";any;Method[matchedByNameRcv];Argument[self];ReturnValue;taint",
]
}
}
@@ -112,23 +118,22 @@ private class SinkFromModel extends ModelInput::SinkModelCsv {
override predicate row(string row) { row = "test;FooOrBar;Method[method].Argument[0];test-sink" }
}
class Conf extends TaintTracking::Configuration {
Conf() { this = "FlowSummaries" }
override predicate isSource(DataFlow::Node src) {
src.asExpr().getExpr().(StringLiteral).getConstantValue().isString("taint")
}
class CustomValueSink extends DefaultValueFlowConf {
override predicate isSink(DataFlow::Node sink) {
exists(MethodCall mc |
mc.getMethodName() = "sink" and
mc.getAnArgument() = sink.asExpr().getExpr()
)
super.isSink(sink)
or
sink = ModelOutput::getASinkNode("test-sink").getARhs()
}
}
from DataFlow::PathNode source, DataFlow::PathNode sink, Conf conf
class CustomTaintSink extends DefaultTaintFlowConf {
override predicate isSink(DataFlow::Node sink) {
super.isSink(sink)
or
sink = ModelOutput::getASinkNode("test-sink").getARhs()
}
}
from DataFlow::PathNode source, DataFlow::PathNode sink, DataFlow::Configuration conf
where conf.hasFlowPath(source, sink)
select sink, source, sink, "$@", source, source.toString()

View File

@@ -1,49 +1,66 @@
tainted = identity "taint"
sink tainted
tainted = identity source("tainted")
sink tainted # $ hasValueFlow=tainted
tainted2 = apply_block tainted do |x|
sink x
sink x # $ hasValueFlow=tainted
x
end
sink tainted2
sink tainted2 # $ hasValueFlow=tainted
my_lambda = -> (x) {
sink x
sink x # $ hasValueFlow=tainted
x
}
tainted3 = apply_lambda(my_lambda, tainted)
sink(tainted3)
sink(tainted3) # $ hasValueFlow=tainted
tainted4 = Foo.firstArg(tainted)
sink(tainted4)
sink(tainted4) # $ hasTaintFlow=tainted
notTainted = Foo.firstArg(nil, tainted))
sink(notTainted)
tainted5 = Foo.secondArg(nil, tainted)
sink(tainted5)
sink(tainted5) # $ hasTaintFlow=tainted
sink(Foo.onlyWithBlock(tainted))
sink(Foo.onlyWithBlock(tainted) do |x| end)
sink(Foo.onlyWithoutBlock(tainted))
sink(Foo.onlyWithBlock(tainted) do |x| end) # $ hasTaintFlow=tainted
sink(Foo.onlyWithoutBlock(tainted)) # $ hasTaintFlow=tainted
sink(Foo.onlyWithoutBlock(tainted) do |x| end)
Foo.new.method(tainted)
Bar.new.method(tainted)
Bar.new.next.method(tainted)
Bar.new.next.next.next.next.method(tainted)
Foo.new.method(tainted) # $ hasValueFlow=tainted
Bar.new.method(tainted) # $ hasValueFlow=tainted
Bar.new.next.method(tainted) # $ hasValueFlow=tainted
Bar.new.next.next.next.next.method(tainted) # $ hasValueFlow=tainted
def userDefinedFunction(x, y)
t = "taint"
sink(x.matchedByName(t))
sink(y.matchedByName(t))
t = source("t")
sink(x.matchedByName(t)) # $ hasTaintFlow=t
sink(y.matchedByName(t)) # $ hasTaintFlow=t
sink(x.unmatchedName(t))
sink(t.matchedByNameRcv())
sink(t.matchedByNameRcv()) # $ hasTaintFlow=t
end
Foo.blockArg do |x|
sink(x.preserveTaint("taint"))
sink(x.preserveTaint(source("blockArg"))) # $ hasTaintFlow=blockArg
end
sink(Foo.namedArg(foo: tainted)) # $ hasTaintFlow=tainted
sink(Foo.namedArg(tainted))
Foo.intoNamedCallback(tainted, foo: ->(x) {
sink(x) # $ hasTaintFlow=tainted
})
Foo.intoNamedParameter(tainted, ->(foo:) {
sink(foo) # $ MISSING: hasTaintFlow=tainted
})
Foo.startInNamedCallback(foo: ->(x) {
sink(x.preserveTaint(source("startInNamedCallback"))) # $ hasTaintFlow=startInNamedCallback
})
Foo.startInNamedParameter(->(foo:) {
sink(foo.preserveTaint(source("startInNamedParameter"))) # $ hasTaintFlow=startInNamedParameter
})

View File

@@ -3,9 +3,9 @@
*/
import codeql.Locations
import codeql.ruby.security.performance.RegExpTreeView as RETV
import codeql.ruby.Regexp as RE
query predicate nodes(RETV::RegExpTerm n, string attr, string val) {
query predicate nodes(RE::RegExpTerm n, string attr, string val) {
attr = "semmle.label" and
val = "[" + concat(n.getAPrimaryQlClass(), ", ") + "] " + n.toString()
or
@@ -13,7 +13,7 @@ query predicate nodes(RETV::RegExpTerm n, string attr, string val) {
val =
any(int i |
n =
rank[i](RETV::RegExpTerm t, string fp, int sl, int sc, int el, int ec |
rank[i](RE::RegExpTerm t, string fp, int sl, int sc, int el, int ec |
t.hasLocationInfo(fp, sl, sc, el, ec)
|
t order by fp, sl, sc, el, ec, t.toString()
@@ -21,7 +21,7 @@ query predicate nodes(RETV::RegExpTerm n, string attr, string val) {
).toString()
}
query predicate edges(RETV::RegExpTerm pred, RETV::RegExpTerm succ, string attr, string val) {
query predicate edges(RE::RegExpTerm pred, RE::RegExpTerm succ, string attr, string val) {
attr in ["semmle.label", "semmle.order"] and
val = any(int i | succ = pred.getChild(i)).toString()
}

View File

@@ -1,4 +1,4 @@
import codeql.ruby.security.performance.RegExpTreeView
import codeql.ruby.Regexp
query predicate groupName(RegExpGroup g, string name) { name = g.getName() }

View File

@@ -33,7 +33,9 @@
| tst.rb:137:11:137:17 | (\\w\|G)* | This part of the regular expression may cause exponential backtracking on strings containing many repetitions of 'G'. |
| tst.rb:143:11:143:18 | (\\d\|\\w)* | This part of the regular expression may cause exponential backtracking on strings containing many repetitions of '0'. |
| tst.rb:146:11:146:17 | (\\d\|5)* | This part of the regular expression may cause exponential backtracking on strings containing many repetitions of '5'. |
| tst.rb:155:11:155:20 | (\\f\|[\\f])* | This part of the regular expression may cause exponential backtracking on strings containing many repetitions of 'f'. |
| tst.rb:149:11:149:20 | (\\s\|[\\f])* | This part of the regular expression may cause exponential backtracking on strings containing many repetitions of '\u000c'. |
| tst.rb:152:11:152:24 | (\\s\|[\\v]\|\\\\v)* | This part of the regular expression may cause exponential backtracking on strings containing many repetitions of '\u000b'. |
| tst.rb:155:11:155:20 | (\\f\|[\\f])* | This part of the regular expression may cause exponential backtracking on strings containing many repetitions of '\u000c'. |
| tst.rb:158:11:158:18 | (\\W\|\\D)* | This part of the regular expression may cause exponential backtracking on strings containing many repetitions of ' '. |
| tst.rb:161:11:161:18 | (\\S\|\\w)* | This part of the regular expression may cause exponential backtracking on strings containing many repetitions of '0'. |
| tst.rb:164:11:164:20 | (\\S\|[\\w])* | This part of the regular expression may cause exponential backtracking on strings containing many repetitions of '0'. |