Skip to content

Commit a62aa2b

Browse files
authored
Merge pull request #269 from github/polynomial_redos
Polynomial ReDoS query
2 parents 8fbe5c0 + 414362d commit a62aa2b

22 files changed

+853
-3
lines changed

ql/lib/codeql/ruby/controlflow/CfgNodes.qll

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -381,11 +381,25 @@ module ExprNodes {
381381
final override StringLiteral getExpr() { result = super.getExpr() }
382382
}
383383

384+
/** A control-flow node that wraps a `RegExpLiteral` AST expression. */
385+
class RegExpLiteralCfgNode extends ExprCfgNode {
386+
override RegExpLiteral e;
387+
388+
final override RegExpLiteral getExpr() { result = super.getExpr() }
389+
}
390+
384391
/** A control-flow node that wraps a `ComparisonOperation` AST expression. */
385392
class ComparisonOperationCfgNode extends BinaryOperationCfgNode {
386393
ComparisonOperationCfgNode() { e instanceof ComparisonOperation }
387394

388-
final override ComparisonOperation getExpr() { result = super.getExpr() }
395+
override ComparisonOperation getExpr() { result = super.getExpr() }
396+
}
397+
398+
/** A control-flow node that wraps a `RelationalOperation` AST expression. */
399+
class RelationalOperationCfgNode extends ComparisonOperationCfgNode {
400+
RelationalOperationCfgNode() { e instanceof RelationalOperation }
401+
402+
final override RelationalOperation getExpr() { result = super.getExpr() }
389403
}
390404

391405
/** A control-flow node that wraps an `ElementReference` AST expression. */
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
/**
2+
* Provides default sources, sinks and sanitizers for reasoning about
3+
* polynomial regular expression denial-of-service attacks, as well
4+
* as extension points for adding your own.
5+
*/
6+
7+
private import codeql.ruby.AST as AST
8+
private import codeql.ruby.CFG
9+
private import codeql.ruby.DataFlow
10+
private import codeql.ruby.dataflow.RemoteFlowSources
11+
private import codeql.ruby.regexp.ParseRegExp as RegExp
12+
private import codeql.ruby.regexp.RegExpTreeView
13+
private import codeql.ruby.regexp.SuperlinearBackTracking
14+
15+
module PolynomialReDoS {
16+
/**
17+
* A data flow source node for polynomial regular expression denial-of-service vulnerabilities.
18+
*/
19+
abstract class Source extends DataFlow::Node { }
20+
21+
/**
22+
* A data flow sink node for polynomial regular expression denial-of-service vulnerabilities.
23+
*/
24+
abstract class Sink extends DataFlow::Node {
25+
/** Gets the regex that is being executed by this node. */
26+
abstract RegExpTerm getRegExp();
27+
28+
/** Gets the node to highlight in the alert message. */
29+
DataFlow::Node getHighlight() { result = this }
30+
}
31+
32+
/**
33+
* A sanitizer for polynomial regular expression denial-of-service vulnerabilities.
34+
*/
35+
abstract class Sanitizer extends DataFlow::Node { }
36+
37+
/**
38+
* A sanitizer guard for polynomial regular expression denial of service
39+
* vulnerabilities.
40+
*/
41+
abstract class SanitizerGuard extends DataFlow::BarrierGuard { }
42+
43+
/**
44+
* A source of remote user input, considered as a flow source.
45+
*/
46+
class RemoteFlowSourceAsSource extends Source, RemoteFlowSource { }
47+
48+
/**
49+
* Gets the AST of a regular expression object that can flow to `node`.
50+
*/
51+
RegExpTerm getRegExpObjectFromNode(DataFlow::Node node) {
52+
exists(DataFlow::LocalSourceNode regexp |
53+
regexp.flowsTo(node) and
54+
result = regexp.asExpr().(CfgNodes::ExprNodes::RegExpLiteralCfgNode).getExpr().getParsed()
55+
)
56+
}
57+
58+
/**
59+
* A regexp match against a superlinear backtracking term, seen as a sink for
60+
* polynomial regular expression denial-of-service vulnerabilities.
61+
*/
62+
class PolynomialBackTrackingTermMatch extends Sink {
63+
PolynomialBackTrackingTerm term;
64+
DataFlow::ExprNode matchNode;
65+
66+
PolynomialBackTrackingTermMatch() {
67+
exists(DataFlow::Node regexp |
68+
term.getRootTerm() = getRegExpObjectFromNode(regexp) and
69+
(
70+
// `=~` or `!~`
71+
exists(CfgNodes::ExprNodes::BinaryOperationCfgNode op |
72+
matchNode.asExpr() = op and
73+
(
74+
op.getExpr() instanceof AST::RegExpMatchExpr or
75+
op.getExpr() instanceof AST::NoRegExpMatchExpr
76+
) and
77+
(
78+
this.asExpr() = op.getLeftOperand() and regexp.asExpr() = op.getRightOperand()
79+
or
80+
this.asExpr() = op.getRightOperand() and regexp.asExpr() = op.getLeftOperand()
81+
)
82+
)
83+
or
84+
// Any of the methods on `String` that take a regexp.
85+
exists(CfgNodes::ExprNodes::MethodCallCfgNode call |
86+
matchNode.asExpr() = call and
87+
call.getExpr().getMethodName() =
88+
[
89+
"[]", "gsub", "gsub!", "index", "match", "match?", "partition", "rindex",
90+
"rpartition", "scan", "slice!", "split", "sub", "sub!"
91+
] and
92+
this.asExpr() = call.getReceiver() and
93+
regexp.asExpr() = call.getArgument(0)
94+
)
95+
or
96+
// A call to `match` or `match?` where the regexp is the receiver.
97+
exists(CfgNodes::ExprNodes::MethodCallCfgNode call |
98+
matchNode.asExpr() = call and
99+
call.getExpr().getMethodName() = ["match", "match?"] and
100+
regexp.asExpr() = call.getReceiver() and
101+
this.asExpr() = call.getArgument(0)
102+
)
103+
)
104+
)
105+
}
106+
107+
override RegExpTerm getRegExp() { result = term }
108+
109+
override DataFlow::Node getHighlight() { result = matchNode }
110+
}
111+
112+
/**
113+
* A check on the length of a string, seen as a sanitizer guard.
114+
*/
115+
class LengthGuard extends SanitizerGuard, CfgNodes::ExprNodes::RelationalOperationCfgNode {
116+
private DataFlow::Node input;
117+
118+
LengthGuard() {
119+
exists(DataFlow::CallNode length, DataFlow::ExprNode operand |
120+
length.asExpr().getExpr().(AST::MethodCall).getMethodName() = "length" and
121+
length.getReceiver() = input and
122+
length.flowsTo(operand) and
123+
operand.getExprNode() = this.getAnOperand()
124+
)
125+
}
126+
127+
override DataFlow::Node getAGuardedNode() { result = input }
128+
}
129+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/**
2+
* Provides a taint tracking configuration for reasoning about polynomial
3+
* regular expression denial-of-service attacks.
4+
*
5+
* Note, for performance reasons: only import this file if `Configuration` is
6+
* needed. Otherwise, `PolynomialReDoSCustomizations` should be imported
7+
* instead.
8+
*/
9+
10+
private import codeql.ruby.DataFlow
11+
private import codeql.ruby.TaintTracking
12+
13+
/**
14+
* Provides a taint-tracking configuration for detecting polynomial regular
15+
* expression denial of service vulnerabilities.
16+
*/
17+
module PolynomialReDoS {
18+
import PolynomialReDoSCustomizations::PolynomialReDoS
19+
20+
/**
21+
* A taint-tracking configuration for detecting polynomial regular expression
22+
* denial of service vulnerabilities.
23+
*/
24+
class Configuration extends TaintTracking::Configuration {
25+
Configuration() { this = "PolynomialReDoS" }
26+
27+
override predicate isSource(DataFlow::Node source) { source instanceof Source }
28+
29+
override predicate isSink(DataFlow::Node sink) { sink instanceof Sink }
30+
31+
override predicate isSanitizer(DataFlow::Node node) { node instanceof Sanitizer }
32+
33+
override predicate isSanitizerGuard(DataFlow::BarrierGuard node) {
34+
node instanceof SanitizerGuard
35+
}
36+
}
37+
}

ql/lib/codeql/ruby/regexp/ReDoSUtil.qll

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
* states that will cause backtracking (a rejecting suffix exists).
1313
*/
1414

15-
private import RegExpTreeView
15+
import RegExpTreeView
1616
private import codeql.Locations
1717

1818
/**

0 commit comments

Comments
 (0)