Skip to content

Commit bf139a0

Browse files
authored
Merge pull request #341 from github/rc/3.3
Rc/3.3 mergeback
2 parents a78ee53 + 8531174 commit bf139a0

34 files changed

+1015
-176
lines changed

ql/lib/codeql/ruby/ApiGraphs.qll

Lines changed: 31 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,7 @@ module API {
188188
/** A node corresponding to the use of an API component. */
189189
class Use extends Node, Impl::MkUse {
190190
override string toString() {
191-
exists(string type | this = Impl::MkUse(_) and type = "Use " |
191+
exists(string type | type = "Use " |
192192
result = type + getPath()
193193
or
194194
not exists(this.getPath()) and result = type + "with no path"
@@ -239,20 +239,19 @@ module API {
239239
/** The root of the API graph. */
240240
MkRoot() or
241241
/** A use of an API member at the node `nd`. */
242-
MkUse(DataFlow::Node nd) { use(_, _, nd) }
242+
MkUse(DataFlow::Node nd) { isUse(nd) }
243243

244244
private string resolveTopLevel(ConstantReadAccess read) {
245245
TResolved(result) = resolveScopeExpr(read) and
246246
not result.matches("%::%")
247247
}
248248

249249
/**
250-
* Holds if `ref` is a use of a node that should have an incoming edge from `base` labeled
251-
* `lbl` in the API graph.
250+
* Holds if `ref` is a use of a node that should have an incoming edge from the root
251+
* node labeled `lbl` in the API graph.
252252
*/
253253
cached
254-
predicate use(TApiNode base, string lbl, DataFlow::Node ref) {
255-
base = MkRoot() and
254+
predicate useRoot(string lbl, DataFlow::Node ref) {
256255
exists(string name, ExprNodes::ConstantAccessCfgNode access, ConstantReadAccess read |
257256
access = ref.asExpr() and
258257
lbl = Label::member(read.getName()) and
@@ -264,7 +263,14 @@ module API {
264263
not exists(resolveTopLevel(read)) and
265264
not exists(read.getScopeExpr())
266265
)
267-
or
266+
}
267+
268+
/**
269+
* Holds if `ref` is a use of a node that should have an incoming edge from use node
270+
* `base` labeled `lbl` in the API graph.
271+
*/
272+
cached
273+
predicate useUse(DataFlow::LocalSourceNode base, string lbl, DataFlow::Node ref) {
268274
exists(ExprCfgNode node |
269275
// First, we find a predecessor of the node `ref` that we want to determine. The predecessor
270276
// is any node that is a type-tracked use of a data flow node (`src`), which is itself a
@@ -307,9 +313,15 @@ module API {
307313
}
308314

309315
pragma[nomagic]
310-
private predicate useExpr(ExprCfgNode node, TApiNode base) {
311-
exists(DataFlow::LocalSourceNode src, DataFlow::LocalSourceNode pred |
312-
use(base, src) and
316+
private predicate isUse(DataFlow::Node nd) {
317+
useRoot(_, nd)
318+
or
319+
useUse(_, _, nd)
320+
}
321+
322+
pragma[nomagic]
323+
private predicate useExpr(ExprCfgNode node, DataFlow::LocalSourceNode src) {
324+
exists(DataFlow::LocalSourceNode pred |
313325
pred = trackUseNode(src) and
314326
pred.flowsTo(any(DataFlow::ExprNode n | n.getExprNode() = node))
315327
)
@@ -331,7 +343,7 @@ module API {
331343
// recursive case, so instead we check it explicitly here.
332344
src instanceof DataFlow::LocalSourceNode and
333345
t.start() and
334-
use(_, src) and
346+
isUse(src) and
335347
result = src
336348
or
337349
exists(TypeTracker t2 | result = trackUseNode(src, t2).track(t2, t))
@@ -353,9 +365,14 @@ module API {
353365
cached
354366
predicate edge(TApiNode pred, string lbl, TApiNode succ) {
355367
/* Every node that is a use of an API component is itself added to the API graph. */
356-
exists(DataFlow::LocalSourceNode ref |
357-
use(pred, lbl, ref) and
358-
succ = MkUse(ref)
368+
exists(DataFlow::LocalSourceNode ref | succ = MkUse(ref) |
369+
pred = MkRoot() and
370+
useRoot(lbl, ref)
371+
or
372+
exists(DataFlow::Node nd |
373+
pred = MkUse(nd) and
374+
useUse(nd, lbl, ref)
375+
)
359376
)
360377
}
361378

ql/lib/codeql/ruby/Concepts.qll

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -419,6 +419,15 @@ module HTTP {
419419

420420
/** Gets a string that identifies the framework used for this request. */
421421
string getFramework() { result = super.getFramework() }
422+
423+
/**
424+
* Holds if this request is made using a mode that disables SSL/TLS
425+
* certificate validation, where `disablingNode` represents the point at
426+
* which the validation was disabled.
427+
*/
428+
predicate disablesCertificateValidation(DataFlow::Node disablingNode) {
429+
super.disablesCertificateValidation(disablingNode)
430+
}
422431
}
423432

424433
/** Provides a class for modeling new HTTP requests. */
@@ -435,6 +444,13 @@ module HTTP {
435444

436445
/** Gets a string that identifies the framework used for this request. */
437446
abstract string getFramework();
447+
448+
/**
449+
* Holds if this request is made using a mode that disables SSL/TLS
450+
* certificate validation, where `disablingNode` represents the point at
451+
* which the validation was disabled.
452+
*/
453+
abstract predicate disablesCertificateValidation(DataFlow::Node disablingNode);
438454
}
439455
}
440456

ql/lib/codeql/ruby/ast/Erb.qll

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,8 +260,28 @@ class ErbFile extends File {
260260
*/
261261
predicate isPartial() { this.getStem().charAt(0) = "_" }
262262

263+
/**
264+
* Gets the base template name associated with this ERB file.
265+
* For instance, a file named `foo.html.erb` has a template name of `foo`.
266+
* A partial template file named `_item.html.erb` has a template name of `item`.
267+
*/
268+
string getTemplateName() { none() }
269+
263270
/**
264271
* Gets the erb template contained within this file.
265272
*/
266273
ErbTemplate getTemplate() { result = template }
267274
}
275+
276+
private class PartialErbFile extends ErbFile {
277+
PartialErbFile() { this.isPartial() }
278+
279+
// Drop the leading underscore
280+
override string getTemplateName() { result = this.getStem().splitAt(".", 0).suffix(1) }
281+
}
282+
283+
private class FullErbFile extends ErbFile {
284+
FullErbFile() { not this.isPartial() }
285+
286+
override string getTemplateName() { result = this.getStem().splitAt(".", 0) }
287+
}

ql/lib/codeql/ruby/ast/Literal.qll

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,13 @@ class BooleanLiteral extends Literal, TBooleanLiteral {
194194

195195
/** Holds if the Boolean literal is `false` or `FALSE`. */
196196
predicate isFalse() { none() }
197+
198+
/** Gets the value of this Boolean literal. */
199+
boolean getValue() {
200+
this.isTrue() and result = true
201+
or
202+
this.isFalse() and result = false
203+
}
197204
}
198205

199206
private class TrueLiteral extends BooleanLiteral, TTrueLiteral {
@@ -750,7 +757,7 @@ class HashLiteral extends Literal, THashLiteral {
750757
final override string getAPrimaryQlClass() { result = "HashLiteral" }
751758

752759
/**
753-
* Gets the `n`th element in this array literal.
760+
* Gets the `n`th element in this hash literal.
754761
*
755762
* In the following example, the 0th element is a `Pair`, and the 1st element
756763
* is a `HashSplatExpr`.
@@ -761,7 +768,7 @@ class HashLiteral extends Literal, THashLiteral {
761768
*/
762769
final Expr getElement(int n) { toGenerated(result) = g.getChild(n) }
763770

764-
/** Gets an element in this array literal. */
771+
/** Gets an element in this hash literal. */
765772
final Expr getAnElement() { result = this.getElement(_) }
766773

767774
/** Gets a key-value `Pair` in this hash literal. */

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

Lines changed: 16 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -73,48 +73,30 @@ private class ActionViewParamsCall extends ActionViewContextCall, ParamsCall { }
7373
abstract class RenderCall extends MethodCall {
7474
RenderCall() { this.getMethodName() = "render" }
7575

76-
private string getWorkingDirectory() {
77-
result = this.getLocation().getFile().getParentContainer().getAbsolutePath()
76+
private Expr getTemplatePathArgument() {
77+
// TODO: support other ways of specifying paths (e.g. `file`)
78+
result = [this.getKeywordArgument(["partial", "template", "action"]), this.getArgument(0)]
7879
}
7980

80-
bindingset[templatePath]
81-
private string templatePathPattern(string templatePath) {
82-
exists(string basename, string relativeRoot |
83-
// everything after the final slash, or the whole string if there is no slash
84-
basename = templatePath.regexpCapture("^(?:.*/)?([^/]*)$", 1) and
85-
// everything up to and including the final slash
86-
relativeRoot = templatePath.regexpCapture("^(.*/)?(?:[^/]*?)$", 1)
87-
|
88-
(
89-
// path relative to <source_prefix>/app/views/
90-
result = "%/app/views/" + relativeRoot + "%" + basename + "%"
91-
or
92-
// relative to file containing call
93-
result = this.getWorkingDirectory() + "%" + templatePath + "%"
94-
)
95-
)
81+
private string getTemplatePathValue() { result = this.getTemplatePathArgument().getValueText() }
82+
83+
// everything up to and including the final slash, but ignoring any leading slash
84+
private string getSubPath() {
85+
result = this.getTemplatePathValue().regexpCapture("^/?(.*/)?(?:[^/]*?)$", 1)
9686
}
9787

98-
private string getTemplatePathPatterns() {
99-
exists(string templatePath |
100-
exists(Expr arg |
101-
// TODO: support other ways of specifying paths (e.g. `file`)
102-
arg = this.getKeywordArgument("partial") or
103-
arg = this.getKeywordArgument("template") or
104-
arg = this.getKeywordArgument("action") or
105-
arg = this.getArgument(0)
106-
|
107-
templatePath = arg.(StringlikeLiteral).getValueText()
108-
)
109-
|
110-
result = this.templatePathPattern(templatePath)
111-
)
88+
// everything after the final slash, or the whole string if there is no slash
89+
private string getBaseName() {
90+
result = this.getTemplatePathValue().regexpCapture("^/?(?:.*/)?([^/]*?)$", 1)
11291
}
11392

11493
/**
115-
* Get the template file to be rendered by this call, if any.
94+
* Gets the template file to be rendered by this call, if any.
11695
*/
117-
ErbFile getTemplateFile() { result.getAbsolutePath().matches(this.getTemplatePathPatterns()) }
96+
ErbFile getTemplateFile() {
97+
result.getTemplateName() = this.getBaseName() and
98+
result.getRelativePath().matches("%app/views/" + this.getSubPath() + "%")
99+
}
118100

119101
/**
120102
* Get the local variables passed as context to the renderer

ql/lib/codeql/ruby/frameworks/http_clients/Excon.qll

Lines changed: 103 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -18,29 +18,113 @@ private import codeql.ruby.ApiGraphs
1818
* https://github.com/excon/excon/blob/master/README.md
1919
*/
2020
class ExconHttpRequest extends HTTP::Client::Request::Range {
21-
DataFlow::Node request;
22-
DataFlow::CallNode responseBody;
21+
DataFlow::Node requestUse;
22+
API::Node requestNode;
23+
API::Node connectionNode;
2324

2425
ExconHttpRequest() {
25-
exists(API::Node requestNode | request = requestNode.getAnImmediateUse() |
26-
requestNode =
27-
[
28-
// one-off requests
29-
API::getTopLevelMember("Excon"),
30-
// connection re-use
31-
API::getTopLevelMember("Excon").getInstance()
32-
]
33-
.getReturn([
34-
// Excon#request exists but Excon.request doesn't.
35-
// This shouldn't be a problem - in real code the latter would raise NoMethodError anyway.
36-
"get", "head", "delete", "options", "post", "put", "patch", "trace", "request"
37-
]) and
38-
responseBody = requestNode.getAMethodCall("body") and
39-
this = request.asExpr().getExpr()
40-
)
26+
requestUse = requestNode.getAnImmediateUse() and
27+
connectionNode =
28+
[
29+
// one-off requests
30+
API::getTopLevelMember("Excon"),
31+
// connection re-use
32+
API::getTopLevelMember("Excon").getInstance(),
33+
API::getTopLevelMember("Excon").getMember("Connection").getInstance()
34+
] and
35+
requestNode =
36+
connectionNode
37+
.getReturn([
38+
// Excon#request exists but Excon.request doesn't.
39+
// This shouldn't be a problem - in real code the latter would raise NoMethodError anyway.
40+
"get", "head", "delete", "options", "post", "put", "patch", "trace", "request"
41+
]) and
42+
this = requestUse.asExpr().getExpr()
4143
}
4244

43-
override DataFlow::Node getResponseBody() { result = responseBody }
45+
override DataFlow::Node getResponseBody() { result = requestNode.getAMethodCall("body") }
46+
47+
override predicate disablesCertificateValidation(DataFlow::Node disablingNode) {
48+
// Check for `ssl_verify_peer: false` in the options hash.
49+
exists(DataFlow::Node arg, int i |
50+
i > 0 and arg = connectionNode.getAUse().(DataFlow::CallNode).getArgument(i)
51+
|
52+
argSetsVerifyPeer(arg, false, disablingNode)
53+
)
54+
or
55+
// Or we see a call to `Excon.defaults[:ssl_verify_peer] = false` before the
56+
// request, and no `ssl_verify_peer: true` in the explicit options hash for
57+
// the request call.
58+
exists(DataFlow::CallNode disableCall |
59+
setsDefaultVerification(disableCall, false) and
60+
disableCall.asExpr().getASuccessor+() = requestUse.asExpr() and
61+
disablingNode = disableCall and
62+
not exists(DataFlow::Node arg, int i |
63+
i > 0 and arg = connectionNode.getAUse().(DataFlow::CallNode).getArgument(i)
64+
|
65+
argSetsVerifyPeer(arg, true, _)
66+
)
67+
)
68+
}
4469

4570
override string getFramework() { result = "Excon" }
4671
}
72+
73+
/**
74+
* Holds if `arg` represents an options hash that contains the key
75+
* `:ssl_verify_peer` with `value`, where `kvNode` is the data-flow node for
76+
* this key-value pair.
77+
*/
78+
predicate argSetsVerifyPeer(DataFlow::Node arg, boolean value, DataFlow::Node kvNode) {
79+
// Either passed as an individual key:value argument, e.g.:
80+
// Excon.get(..., ssl_verify_peer: false)
81+
isSslVerifyPeerPair(arg.asExpr().getExpr(), value) and
82+
kvNode = arg
83+
or
84+
// Or as a single hash argument, e.g.:
85+
// Excon.get(..., { ssl_verify_peer: false, ... })
86+
exists(DataFlow::LocalSourceNode optionsNode, Pair p |
87+
p = optionsNode.asExpr().getExpr().(HashLiteral).getAKeyValuePair() and
88+
isSslVerifyPeerPair(p, value) and
89+
optionsNode.flowsTo(arg) and
90+
kvNode.asExpr().getExpr() = p
91+
)
92+
}
93+
94+
/**
95+
* Holds if `callNode` sets `Excon.defaults[:ssl_verify_peer]` or
96+
* `Excon.ssl_verify_peer` to `value`.
97+
*/
98+
private predicate setsDefaultVerification(DataFlow::CallNode callNode, boolean value) {
99+
callNode = API::getTopLevelMember("Excon").getReturn("defaults").getAMethodCall("[]=") and
100+
isSslVerifyPeerLiteral(callNode.getArgument(0)) and
101+
hasBooleanValue(callNode.getArgument(1), value)
102+
or
103+
callNode = API::getTopLevelMember("Excon").getAMethodCall("ssl_verify_peer=") and
104+
hasBooleanValue(callNode.getArgument(0), value)
105+
}
106+
107+
private predicate isSslVerifyPeerLiteral(DataFlow::Node node) {
108+
exists(DataFlow::LocalSourceNode literal |
109+
literal.asExpr().getExpr().(SymbolLiteral).getValueText() = "ssl_verify_peer" and
110+
literal.flowsTo(node)
111+
)
112+
}
113+
114+
/** Holds if `node` can contain `value`. */
115+
private predicate hasBooleanValue(DataFlow::Node node, boolean value) {
116+
exists(DataFlow::LocalSourceNode literal |
117+
literal.asExpr().getExpr().(BooleanLiteral).getValue() = value and
118+
literal.flowsTo(node)
119+
)
120+
}
121+
122+
/** Holds if `p` is the pair `ssl_verify_peer: <value>`. */
123+
private predicate isSslVerifyPeerPair(Pair p, boolean value) {
124+
exists(DataFlow::Node key, DataFlow::Node valueNode |
125+
key.asExpr().getExpr() = p.getKey() and valueNode.asExpr().getExpr() = p.getValue()
126+
|
127+
isSslVerifyPeerLiteral(key) and
128+
hasBooleanValue(valueNode, value)
129+
)
130+
}

0 commit comments

Comments
 (0)