Skip to content

Commit 11154a9

Browse files
committed
Ruby: add regex injection query
1 parent dc24361 commit 11154a9

File tree

8 files changed

+278
-0
lines changed

8 files changed

+278
-0
lines changed

ruby/ql/lib/codeql/ruby/frameworks/StandardLibrary.qll

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ private import codeql.ruby.AST
22
private import codeql.ruby.Concepts
33
private import codeql.ruby.DataFlow
44
private import codeql.ruby.ApiGraphs
5+
private import codeql.ruby.dataflow.FlowSummary
56

67
/**
78
* The `Kernel` module is included by the `Object` class, so its methods are available
@@ -335,3 +336,18 @@ class ModuleEvalCallCodeExecution extends CodeExecution::Range, DataFlow::CallNo
335336

336337
override DataFlow::Node getCode() { result = this.getArgument(0) }
337338
}
339+
340+
/** Flow summary for `Regexp.escape`. */
341+
class RegexpEscapeSummary extends SummarizedCallable {
342+
RegexpEscapeSummary() { this = "Regexp.escape" }
343+
344+
override MethodCall getACall() {
345+
result = API::getTopLevelMember("Regexp").getAMethodCall("escape").asExpr().getExpr()
346+
}
347+
348+
override predicate propagatesFlowExt(string input, string output, boolean preservesValue) {
349+
input = "Argument[0]" and
350+
output = "ReturnValue" and
351+
preservesValue = false
352+
}
353+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
private import ruby
2+
private import codeql.ruby.DataFlow
3+
private import codeql.ruby.Concepts
4+
private import codeql.ruby.Frameworks
5+
private import codeql.ruby.dataflow.RemoteFlowSources
6+
private import codeql.ruby.dataflow.BarrierGuards
7+
private import codeql.ruby.ApiGraphs
8+
9+
/**
10+
* Provides default sources, sinks and sanitizers for detecting
11+
* regexp injection vulnerabilities, as well as extension points for
12+
* adding your own.
13+
*/
14+
module RegExpInjection {
15+
/**
16+
* A data flow source for regexp injection vulnerabilities.
17+
*/
18+
abstract class Source extends DataFlow::Node { }
19+
20+
/**
21+
* A data flow sink for regexp injection vulnerabilities.
22+
*/
23+
abstract class Sink extends DataFlow::Node { }
24+
25+
/**
26+
* A sanitizer guard for regexp injection vulnerabilities.
27+
*/
28+
abstract class SanitizerGuard extends DataFlow::BarrierGuard { }
29+
30+
/**
31+
* A data flow sanitized for regexp injection vulnerabilities.
32+
*/
33+
abstract class Sanitizer extends DataFlow::Node { }
34+
35+
/**
36+
* A source of remote user input, considered as a flow source.
37+
*/
38+
class RemoteFlowSourceAsSource extends Source, RemoteFlowSource { }
39+
40+
/** A regexp literal, considered as a flow sink. */
41+
class RegExpLiteralAsSink extends Sink {
42+
RegExpLiteralAsSink() { this.asExpr().getExpr() instanceof RegExpLiteral }
43+
}
44+
45+
/**
46+
* The first argument of a call to `Regexp.new`, considered as a flow sink.
47+
*/
48+
class ConstructedRegExpAsSink extends Sink {
49+
ConstructedRegExpAsSink() {
50+
this =
51+
API::getTopLevelMember("Regexp").getAnInstantiation().(DataFlow::CallNode).getArgument(0)
52+
}
53+
}
54+
55+
/**
56+
* A comparison with a constant string, considered as a sanitizer-guard.
57+
*/
58+
class StringConstCompareAsSanitizerGuard extends SanitizerGuard, StringConstCompare { }
59+
60+
/**
61+
* An inclusion check against an array of constant strings, considered as a
62+
* sanitizer-guard.
63+
*/
64+
class StringConstArrayInclusionCallAsSanitizerGuard extends SanitizerGuard,
65+
StringConstArrayInclusionCall { }
66+
67+
/**
68+
* A call to `Regexp.escape`, considered as a sanitizer.
69+
*/
70+
class RegexpEscapeSanitization extends Sanitizer {
71+
RegexpEscapeSanitization() { this = API::getTopLevelMember("Regexp").getAMethodCall("escape") }
72+
}
73+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/**
2+
* Provides a taint-tracking configuration for detecting regexp injection vulnerabilities.
3+
*
4+
* Note, for performance reasons: only import this file if `Configuration` is needed,
5+
* otherwise `RegExpInjectionCustomizations` should be imported instead.
6+
*/
7+
8+
import codeql.ruby.DataFlow::DataFlow::PathGraph
9+
import codeql.ruby.DataFlow
10+
import codeql.ruby.TaintTracking
11+
import RegExpInjectionCustomizations
12+
import codeql.ruby.dataflow.BarrierGuards
13+
14+
/**
15+
* A taint-tracking configuration for detecting regexp injection vulnerabilities.
16+
*/
17+
class Configuration extends TaintTracking::Configuration {
18+
Configuration() { this = "RegExpInjection" }
19+
20+
override predicate isSource(DataFlow::Node source) { source instanceof RegExpInjection::Source }
21+
22+
override predicate isSink(DataFlow::Node sink) { sink instanceof RegExpInjection::Sink }
23+
24+
override predicate isSanitizerGuard(DataFlow::BarrierGuard guard) {
25+
guard instanceof RegExpInjection::SanitizerGuard
26+
}
27+
28+
override predicate isSanitizer(DataFlow::Node node) { node instanceof RegExpInjection::Sanitizer }
29+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
<!DOCTYPE qhelp PUBLIC
2+
"-//Semmle//qhelp//EN"
3+
"qhelp.dtd">
4+
<qhelp>
5+
6+
<overview>
7+
<p>
8+
Constructing a regular expression with unsanitized user input is dangerous,
9+
since a malicious user may be able to modify the meaning of the expression. In
10+
particular, such a user may be able to provide a regular expression fragment
11+
that takes exponential time in the worst case, and use that to perform a Denial
12+
of Service attack.
13+
</p>
14+
</overview>
15+
16+
<recommendation>
17+
<p>
18+
Before embedding user input into a regular expression, use a sanitization
19+
function such as <code>Regexp.escape</code> to escape meta-characters that have
20+
special meaning.
21+
</p>
22+
</recommendation>
23+
24+
<example>
25+
<p>
26+
The following examples construct regular expressions from an HTTP request
27+
parameter without sanitizing it first:
28+
</p>
29+
<sample language="ruby">
30+
class UsersController < ActionController::Base
31+
def first_example
32+
# BAD: Unsanitized user input is used to construct a regular expression
33+
regex = /#{ params[:key] }/
34+
end
35+
36+
def second_example
37+
# BAD: Unsanitized user input is used to construct a regular expression
38+
regex = Regexp.new(params[:key])
39+
end
40+
end
41+
</sample>
42+
<p>
43+
Instead, the request parameter should be sanitized first. This ensures that the
44+
user cannot insert characters that have special meanings in regular expressions.
45+
</p>
46+
<sample language="ruby">
47+
class UsersController < ActionController::Base
48+
def example
49+
# GOOD: User input is sanitized before constructing the regular expression
50+
regex = Regexp.new(Regex.escape(params[:key]))
51+
end
52+
end
53+
</sample>
54+
</example>
55+
56+
<references>
57+
<li>
58+
OWASP:
59+
<a href="https://www.owasp.org/index.php/Regular_expression_Denial_of_Service_-_ReDoS">Regular expression Denial of Service - ReDoS</a>.
60+
</li>
61+
<li>
62+
Wikipedia: <a href="https://en.wikipedia.org/wiki/ReDoS">ReDoS</a>.
63+
</li>
64+
<li>
65+
Ruby: <a href="https://ruby-doc.org/core-3.0.2/Regexp.html#method-c-escape">Regexp.escape</a>.
66+
</li>
67+
</references>
68+
</qhelp>
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/**
2+
* @name Regular expression injection
3+
* @description User input should not be used in regular expressions without
4+
* first being escaped. Otherwise, a malicious user may be able to
5+
* inject an expression that could require exponential time on
6+
* certain inputs.
7+
* @kind path-problem
8+
* @problem.severity error
9+
* @security-severity 7.5
10+
* @precision high
11+
* @id rb/regexp-injection
12+
* @tags security
13+
* external/cwe/cwe-1333
14+
* external/cwe/cwe-730
15+
* external/cwe/cwe-400
16+
*/
17+
18+
import ruby
19+
import DataFlow::PathGraph
20+
import codeql.ruby.DataFlow
21+
import codeql.ruby.regexp.RegExpInjectionQuery
22+
23+
from Configuration cfg, DataFlow::PathNode source, DataFlow::PathNode sink
24+
where cfg.hasFlowPath(source, sink)
25+
select sink.getNode(), source, sink, "This regular expression is constructed from a $@.",
26+
source.getNode(), "user-provided value"
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
edges
2+
| RegExpInjection.rb:4:12:4:17 | call to params : | RegExpInjection.rb:5:13:5:21 | /#{...}/ |
3+
| RegExpInjection.rb:10:12:10:17 | call to params : | RegExpInjection.rb:11:13:11:27 | /foo#{...}bar/ |
4+
| RegExpInjection.rb:16:12:16:17 | call to params : | RegExpInjection.rb:17:24:17:27 | name |
5+
| RegExpInjection.rb:22:12:22:17 | call to params : | RegExpInjection.rb:23:24:23:33 | ... + ... |
6+
nodes
7+
| RegExpInjection.rb:4:12:4:17 | call to params : | semmle.label | call to params : |
8+
| RegExpInjection.rb:5:13:5:21 | /#{...}/ | semmle.label | /#{...}/ |
9+
| RegExpInjection.rb:10:12:10:17 | call to params : | semmle.label | call to params : |
10+
| RegExpInjection.rb:11:13:11:27 | /foo#{...}bar/ | semmle.label | /foo#{...}bar/ |
11+
| RegExpInjection.rb:16:12:16:17 | call to params : | semmle.label | call to params : |
12+
| RegExpInjection.rb:17:24:17:27 | name | semmle.label | name |
13+
| RegExpInjection.rb:22:12:22:17 | call to params : | semmle.label | call to params : |
14+
| RegExpInjection.rb:23:24:23:33 | ... + ... | semmle.label | ... + ... |
15+
subpaths
16+
#select
17+
| RegExpInjection.rb:5:13:5:21 | /#{...}/ | RegExpInjection.rb:4:12:4:17 | call to params : | RegExpInjection.rb:5:13:5:21 | /#{...}/ | This regular expression is constructed from a $@. | RegExpInjection.rb:4:12:4:17 | call to params | user-provided value |
18+
| RegExpInjection.rb:11:13:11:27 | /foo#{...}bar/ | RegExpInjection.rb:10:12:10:17 | call to params : | RegExpInjection.rb:11:13:11:27 | /foo#{...}bar/ | This regular expression is constructed from a $@. | RegExpInjection.rb:10:12:10:17 | call to params | user-provided value |
19+
| RegExpInjection.rb:17:24:17:27 | name | RegExpInjection.rb:16:12:16:17 | call to params : | RegExpInjection.rb:17:24:17:27 | name | This regular expression is constructed from a $@. | RegExpInjection.rb:16:12:16:17 | call to params | user-provided value |
20+
| RegExpInjection.rb:23:24:23:33 | ... + ... | RegExpInjection.rb:22:12:22:17 | call to params : | RegExpInjection.rb:23:24:23:33 | ... + ... | This regular expression is constructed from a $@. | RegExpInjection.rb:22:12:22:17 | call to params | user-provided value |
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
queries/security/cwe-1333/RegExpInjection.ql
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
class FooController < ActionController::Base
2+
# BAD
3+
def route0
4+
name = params[:name]
5+
regex = /#{name}/
6+
end
7+
8+
# BAD
9+
def route1
10+
name = params[:name]
11+
regex = /foo#{name}bar/
12+
end
13+
14+
# BAD
15+
def route2
16+
name = params[:name]
17+
regex = Regexp.new(name)
18+
end
19+
20+
# BAD
21+
def route3
22+
name = params[:name]
23+
regex = Regexp.new("@" + name)
24+
end
25+
26+
# GOOD - string is compared against a constant string
27+
def route4
28+
name = params[:name]
29+
regex = Regexp.new("@" + name) if name == "foo"
30+
end
31+
32+
# GOOD - string is compared against a constant string array
33+
def route5
34+
name = params[:name]
35+
if ["John", "Paul", "George", "Ringo"].include?(name)
36+
regex = /@#{name}/
37+
end
38+
end
39+
40+
# GOOD - string is explicitly escaped
41+
def route6
42+
name = params[:name]
43+
regex = Regexp.new(Regexp.escape(name))
44+
end
45+
end

0 commit comments

Comments
 (0)