Skip to content

Commit 2c5129e

Browse files
authored
Merge pull request github#10369 from alexrford/rb/sensitive-get-query
Ruby: add `rb/sensitive-get-query` query
2 parents b88b2f1 + b29bf82 commit 2c5129e

File tree

16 files changed

+421
-0
lines changed

16 files changed

+421
-0
lines changed

ruby/ql/lib/codeql/ruby/Concepts.qll

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,11 @@ module Http {
250250

251251
/** Gets a string that identifies the framework used for this route setup. */
252252
string getFramework() { result = super.getFramework() }
253+
254+
/**
255+
* Gets the HTTP method name, in lowercase, that this handler will respond to.
256+
*/
257+
string getHttpMethod() { result = super.getHttpMethod() }
253258
}
254259

255260
/** Provides a class for modeling new HTTP routing APIs. */
@@ -287,6 +292,11 @@ module Http {
287292

288293
/** Gets a string that identifies the framework used for this route setup. */
289294
abstract string getFramework();
295+
296+
/**
297+
* Gets the HTTP method name, in all caps, that this handler will respond to.
298+
*/
299+
abstract string getHttpMethod();
290300
}
291301
}
292302

@@ -395,6 +405,12 @@ module Http {
395405

396406
/** Gets a string that identifies the framework used for this route setup. */
397407
string getFramework() { result = super.getFramework() }
408+
409+
/**
410+
* Gets an HTTP method name, in all caps, that this handler will respond to.
411+
* Handlers can potentially respond to multiple HTTP methods.
412+
*/
413+
string getAnHttpMethod() { result = super.getAnHttpMethod() }
398414
}
399415

400416
/** Provides a class for modeling new HTTP request handlers. */
@@ -416,6 +432,12 @@ module Http {
416432

417433
/** Gets a string that identifies the framework used for this request handler. */
418434
abstract string getFramework();
435+
436+
/**
437+
* Gets an HTTP method name, in all caps, that this handler will respond to.
438+
* Handlers can potentially respond to multiple HTTP methods.
439+
*/
440+
abstract string getAnHttpMethod();
419441
}
420442
}
421443

@@ -430,6 +452,8 @@ module Http {
430452
}
431453

432454
override string getFramework() { result = rs.getFramework() }
455+
456+
override string getAnHttpMethod() { result = rs.getHttpMethod() }
433457
}
434458

435459
/** A parameter that will receive parts of the url when handling an incoming request. */

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -687,6 +687,15 @@ module ExprNodes {
687687
final ExprCfgNode getValue() { e.hasCfgChild(e.getValue(), this, result) }
688688
}
689689

690+
/** A control-flow node that wraps a `VariableAccess` AST expression. */
691+
class VariableAccessCfgNode extends ExprCfgNode {
692+
override string getAPrimaryQlClass() { result = "VariableAccessCfgNode" }
693+
694+
override VariableAccess e;
695+
696+
final override VariableAccess getExpr() { result = ExprCfgNode.super.getExpr() }
697+
}
698+
690699
/** A control-flow node that wraps a `VariableReadAccess` AST expression. */
691700
class VariableReadAccessCfgNode extends ExprCfgNode {
692701
override string getAPrimaryQlClass() { result = "VariableReadAccessCfgNode" }

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,8 @@ class ActionControllerActionMethod extends Method, Http::Server::RequestHandler:
8383

8484
override string getFramework() { result = "ActionController" }
8585

86+
override string getAnHttpMethod() { result = this.getARoute().getHttpMethod() }
87+
8688
/** Gets a call to render from within this method. */
8789
Rails::RenderCall getARenderCall() { result.getParent+() = this }
8890

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,9 @@ private class GraphqlSchemaResolverClass extends ClassDeclaration {
8484
}
8585
}
8686

87+
/** Gets an HTTP method that is supported for querying a GraphQL server. */
88+
private string getASupportedHttpMethod() { result = ["get", "post"] }
89+
8790
/**
8891
* A `ClassDeclaration` for a class that extends `GraphQL::Schema::Object`.
8992
* For example,
@@ -172,6 +175,8 @@ class GraphqlResolveMethod extends Method, Http::Server::RequestHandler::Range {
172175

173176
override string getFramework() { result = "GraphQL" }
174177

178+
override string getAnHttpMethod() { result = getASupportedHttpMethod() }
179+
175180
/** Gets the mutation class containing this method. */
176181
GraphqlResolvableClass getMutationClass() { result = resolvableClass }
177182
}
@@ -219,6 +224,8 @@ class GraphqlLoadMethod extends Method, Http::Server::RequestHandler::Range {
219224

220225
override string getFramework() { result = "GraphQL" }
221226

227+
override string getAnHttpMethod() { result = getASupportedHttpMethod() }
228+
222229
/** Gets the mutation class containing this method. */
223230
GraphqlResolvableClass getMutationClass() { result = resolvableClass }
224231
}
@@ -388,6 +395,8 @@ class GraphqlFieldResolutionMethod extends Method, Http::Server::RequestHandler:
388395

389396
override string getFramework() { result = "GraphQL" }
390397

398+
override string getAnHttpMethod() { result = getASupportedHttpMethod() }
399+
391400
/** Gets the class containing this method. */
392401
GraphqlSchemaObjectClass getGraphqlClass() { result = schemaObjectClass }
393402
}

ruby/ql/lib/codeql/ruby/security/SensitiveActions.qll

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,144 @@
1111

1212
private import codeql.ruby.AST
1313
private import codeql.ruby.DataFlow
14+
import codeql.ruby.security.internal.SensitiveDataHeuristics
15+
private import HeuristicNames
16+
private import codeql.ruby.CFG
17+
18+
/** An expression that might contain sensitive data. */
19+
cached
20+
abstract class SensitiveNode extends DataFlow::Node {
21+
/** Gets a human-readable description of this expression for use in alert messages. */
22+
cached
23+
abstract string describe();
24+
25+
/** Gets a classification of the kind of sensitive data this expression might contain. */
26+
cached
27+
abstract SensitiveDataClassification getClassification();
28+
}
29+
30+
/** A method call that might produce sensitive data. */
31+
class SensitiveCall extends SensitiveNode instanceof DataFlow::CallNode {
32+
SensitiveDataClassification classification;
33+
34+
SensitiveCall() {
35+
classification = this.getMethodName().(SensitiveDataMethodName).getClassification()
36+
or
37+
// This is particularly to pick up methods with an argument like "password", which
38+
// may indicate a lookup.
39+
exists(string s | super.getArgument(_).asExpr().getConstantValue().isStringlikeValue(s) |
40+
nameIndicatesSensitiveData(s, classification)
41+
)
42+
}
43+
44+
override string describe() { result = "a call to " + super.getMethodName() }
45+
46+
override SensitiveDataClassification getClassification() { result = classification }
47+
}
48+
49+
/** An access to a variable or hash value that might contain sensitive data. */
50+
abstract class SensitiveVariableAccess extends SensitiveNode {
51+
string name;
52+
53+
SensitiveVariableAccess() {
54+
this.asExpr().(CfgNodes::ExprNodes::VariableAccessCfgNode).getExpr().getVariable().hasName(name)
55+
or
56+
this.asExpr()
57+
.(CfgNodes::ExprNodes::ElementReferenceCfgNode)
58+
.getAnArgument()
59+
.getConstantValue()
60+
.isStringlikeValue(name)
61+
}
62+
63+
override string describe() { result = "an access to " + name }
64+
}
65+
66+
/** A write to a location that might contain sensitive data. */
67+
abstract class SensitiveWrite extends DataFlow::Node { }
68+
69+
/**
70+
* Holds if `node` is a write to a variable or hash value named `name`.
71+
*
72+
* Helper predicate factored out for performance,
73+
* to filter `name` as much as possible before using it in
74+
* regex matching.
75+
*/
76+
pragma[nomagic]
77+
private predicate writesProperty(DataFlow::Node node, string name) {
78+
exists(VariableWriteAccess vwa | vwa.getVariable().getName() = name |
79+
node.asExpr().getExpr() = vwa
80+
)
81+
or
82+
// hash value assignment
83+
node.(DataFlow::CallNode).getMethodName() = "[]=" and
84+
node.(DataFlow::CallNode).getArgument(0).asExpr().getConstantValue().isStringlikeValue(name)
85+
}
86+
87+
/**
88+
* Instance and class variable names are reported with their respective `@`
89+
* and `@@` prefixes. This predicate strips these prefixes.
90+
*/
91+
bindingset[name]
92+
private string unprefixedVariableName(string name) { result = name.regexpReplaceAll("^@*", "") }
93+
94+
/** A write to a variable or property that might contain sensitive data. */
95+
private class BasicSensitiveWrite extends SensitiveWrite {
96+
SensitiveDataClassification classification;
97+
98+
BasicSensitiveWrite() {
99+
exists(string name |
100+
/*
101+
* PERFORMANCE OPTIMISATION:
102+
* `nameIndicatesSensitiveData` performs a `regexpMatch` on `name`.
103+
* To carry out a regex match, we must first compute the Cartesian product
104+
* of all possible `name`s and regexes, then match.
105+
* To keep this product as small as possible,
106+
* we want to filter `name` as much as possible before the product.
107+
*
108+
* Do this by factoring out a helper predicate containing the filtering
109+
* logic that restricts `name`. This helper predicate will get picked first
110+
* in the join order, since it is the only call here that binds `name`.
111+
*/
112+
113+
writesProperty(this, name) and
114+
nameIndicatesSensitiveData(unprefixedVariableName(name), classification)
115+
)
116+
}
117+
118+
/** Gets a classification of the kind of sensitive data the write might handle. */
119+
SensitiveDataClassification getClassification() { result = classification }
120+
}
121+
122+
/** An access to a variable or hash value that might contain sensitive data. */
123+
private class BasicSensitiveVariableAccess extends SensitiveVariableAccess {
124+
SensitiveDataClassification classification;
125+
126+
BasicSensitiveVariableAccess() {
127+
nameIndicatesSensitiveData(unprefixedVariableName(name), classification)
128+
}
129+
130+
override SensitiveDataClassification getClassification() { result = classification }
131+
}
132+
133+
/** A method name that suggests it may be sensitive. */
134+
abstract class SensitiveMethodName extends string {
135+
SensitiveMethodName() { this = any(MethodBase m).getName() }
136+
}
137+
138+
/** A method name that suggests it may produce sensitive data. */
139+
abstract class SensitiveDataMethodName extends SensitiveMethodName {
140+
/** Gets a classification of the kind of sensitive data this method may produce. */
141+
abstract SensitiveDataClassification getClassification();
142+
}
143+
144+
/** A method name that might return sensitive credential data. */
145+
class CredentialsMethodName extends SensitiveDataMethodName {
146+
SensitiveDataClassification classification;
147+
148+
CredentialsMethodName() { nameIndicatesSensitiveData(this, classification) }
149+
150+
override SensitiveDataClassification getClassification() { result = classification }
151+
}
14152

15153
/**
16154
* A sensitive action, such as transfer of sensitive data.
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/**
2+
* Provides default sources and sinks for reasoning about sensitive data sourced
3+
* from the query string of a GET request, as well as extension points for
4+
* adding your own.
5+
*/
6+
7+
private import codeql.ruby.security.SensitiveActions
8+
private import codeql.ruby.Concepts
9+
private import codeql.ruby.DataFlow
10+
11+
/**
12+
* Provides default sources and sinks for reasoning about sensitive data sourced
13+
* from the query string of a GET request, as well as extension points for
14+
* adding your own.
15+
*/
16+
module SensitiveGetQuery {
17+
/**
18+
* A data flow source representing data sourced from the query string in a
19+
* GET request handler.
20+
*/
21+
abstract class Source extends DataFlow::Node {
22+
/** Gets the request handler corresponding to this data source. */
23+
abstract Http::Server::RequestHandler getHandler();
24+
}
25+
26+
/**
27+
* An access to data from the query string of a GET request as a data flow
28+
* source.
29+
*/
30+
private class RequestInputAccessSource extends Source instanceof Http::Server::RequestInputAccess {
31+
private Http::Server::RequestHandler handler;
32+
33+
RequestInputAccessSource() {
34+
handler = this.asExpr().getExpr().getEnclosingMethod() and
35+
handler.getAnHttpMethod() = "get" and
36+
this.getKind() = "parameter"
37+
}
38+
39+
override Http::Server::RequestHandler getHandler() { result = handler }
40+
}
41+
42+
/**
43+
* A data flow sink suggesting a use of sensitive data.
44+
*/
45+
abstract class Sink extends DataFlow::Node { }
46+
47+
/** A sensitive data node as a data flow sink. */
48+
private class SensitiveNodeSink extends Sink instanceof SensitiveNode {
49+
SensitiveNodeSink() {
50+
// User names and other similar information is not sensitive in this context.
51+
not this.getClassification() = SensitiveDataClassification::id()
52+
}
53+
}
54+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/**
2+
* Provides a taint-tracking configuration for detecting flow of query string
3+
* data to sensitive actions in GET query request handlers.
4+
*
5+
* Note, for performance reasons: only import this file if `Configuration` is
6+
* needed, otherwise `SensitiveGetQueryCustomizations` should be imported
7+
* instead.
8+
*/
9+
10+
private import ruby
11+
private import codeql.ruby.TaintTracking
12+
13+
/**
14+
* Provides a taint-tracking configuration for detecting flow of query string
15+
* data to sensitive actions in GET query request handlers.
16+
*/
17+
module SensitiveGetQuery {
18+
import SensitiveGetQueryCustomizations::SensitiveGetQuery
19+
20+
/**
21+
* A taint-tracking configuration for reasoning about use of sensitive data
22+
* from a GET request query string.
23+
*/
24+
class Configuration extends TaintTracking::Configuration {
25+
Configuration() { this = "SensitiveGetQuery" }
26+
27+
override predicate isSource(DataFlow::Node source) { source instanceof Source }
28+
29+
override predicate isSink(DataFlow::Node sink) { sink instanceof Sink }
30+
}
31+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
category: newQuery
3+
---
4+
* Added a new query, `rb/sensitive-get-query`, to detect cases where sensitive data is read from the query parameters of an HTTP `GET` request.

0 commit comments

Comments
 (0)