Skip to content

Commit 6f22867

Browse files
authored
Merge pull request #7015 from github/hmac/ssrf
Ruby: Add Server-Side Request Forgery query
2 parents ddeb700 + 0600078 commit 6f22867

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+752
-129
lines changed
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
lgtm,codescanning
2+
* A new query (`rb/request-forgery`) has been added. The query finds HTTP requests made with user-controlled URLs.

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -417,6 +417,12 @@ module HTTP {
417417
/** Gets a node which returns the body of the response */
418418
DataFlow::Node getResponseBody() { result = super.getResponseBody() }
419419

420+
/**
421+
* Gets a node that contributes to the URL of the request.
422+
* Depending on the framework, a request may have multiple nodes which contribute to the URL.
423+
*/
424+
DataFlow::Node getURL() { result = super.getURL() }
425+
420426
/** Gets a string that identifies the framework used for this request. */
421427
string getFramework() { result = super.getFramework() }
422428

@@ -442,6 +448,12 @@ module HTTP {
442448
/** Gets a node which returns the body of the response */
443449
abstract DataFlow::Node getResponseBody();
444450

451+
/**
452+
* Gets a node that contributes to the URL of the request.
453+
* Depending on the framework, a request may have multiple nodes which contribute to the URL.
454+
*/
455+
abstract DataFlow::Node getURL();
456+
445457
/** Gets a string that identifies the framework used for this request. */
446458
abstract string getFramework();
447459

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

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -320,7 +320,11 @@ module ExprNodes {
320320

321321
/** Gets the the keyword argument whose key is `keyword` of this call. */
322322
final ExprCfgNode getKeywordArgument(string keyword) {
323-
e.hasCfgChild(e.getKeywordArgument(keyword), this, result)
323+
exists(PairCfgNode n |
324+
e.hasCfgChild(e.getAnArgument(), this, n) and
325+
n.getKey().getExpr().(SymbolLiteral).getValueText() = keyword and
326+
result = n.getValue()
327+
)
324328
}
325329

326330
/** Gets the number of arguments of this call. */
@@ -426,6 +430,27 @@ module ExprNodes {
426430
ParenthesizedExprCfgNode() { this.getExpr() instanceof ParenthesizedExpr }
427431
}
428432

433+
private class PairChildMapping extends ExprChildMapping, Pair {
434+
override predicate relevantChild(Expr e) { e = this.getKey() or e = this.getValue() }
435+
}
436+
437+
/** A control-flow node that wraps a `Pair` AST expression. */
438+
class PairCfgNode extends ExprCfgNode {
439+
override PairChildMapping e;
440+
441+
final override Pair getExpr() { result = ExprCfgNode.super.getExpr() }
442+
443+
/**
444+
* Gets the key expression of this pair.
445+
*/
446+
final ExprCfgNode getKey() { e.hasCfgChild(e.getKey(), this, result) }
447+
448+
/**
449+
* Gets the value expression of this pair.
450+
*/
451+
final ExprCfgNode getValue() { e.hasCfgChild(e.getValue(), this, result) }
452+
}
453+
429454
/** A control-flow node that wraps a `VariableReadAccess` AST expression. */
430455
class VariableReadAccessCfgNode extends ExprCfgNode {
431456
override VariableReadAccess e;
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
/** Provides commonly used dataflow sanitizers */
2+
3+
private import codeql.ruby.AST
4+
private import codeql.ruby.DataFlow
5+
6+
/**
7+
* A sanitizer for flow into a string interpolation component,
8+
* provided that component does not form a prefix of the string.
9+
*
10+
* This is useful for URLs and paths, where the fixed prefix prevents the user from controlling the target.
11+
*/
12+
class PrefixedStringInterpolation extends DataFlow::Node {
13+
PrefixedStringInterpolation() {
14+
exists(StringlikeLiteral str, int n | str.getComponent(n) = this.asExpr().getExpr() and n > 0)
15+
}
16+
}

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

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,14 @@ 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 requestUse;
21+
DataFlow::CallNode requestUse;
2222
API::Node requestNode;
2323
API::Node connectionNode;
24+
DataFlow::Node connectionUse;
2425

2526
ExconHttpRequest() {
2627
requestUse = requestNode.getAnImmediateUse() and
28+
connectionUse = connectionNode.getAnImmediateUse() and
2729
connectionNode =
2830
[
2931
// one-off requests
@@ -44,6 +46,17 @@ class ExconHttpRequest extends HTTP::Client::Request::Range {
4446

4547
override DataFlow::Node getResponseBody() { result = requestNode.getAMethodCall("body") }
4648

49+
override DataFlow::Node getURL() {
50+
// For one-off requests, the URL is in the first argument of the request method call.
51+
// For connection re-use, the URL is split between the first argument of the `new` call
52+
// and the `path` keyword argument of the request method call.
53+
result = requestUse.getArgument(0) and not result.asExpr().getExpr() instanceof Pair
54+
or
55+
result = requestUse.getKeywordArgument("path")
56+
or
57+
result = connectionUse.(DataFlow::CallNode).getArgument(0)
58+
}
59+
4760
override predicate disablesCertificateValidation(DataFlow::Node disablingNode) {
4861
// Check for `ssl_verify_peer: false` in the options hash.
4962
exists(DataFlow::Node arg, int i |

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

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,16 @@ private import codeql.ruby.ApiGraphs
1111
* # connection re-use
1212
* connection = Faraday.new("http://example.com")
1313
* connection.get("/").body
14+
*
15+
* connection = Faraday.new(url: "http://example.com")
16+
* connection.get("/").body
1417
* ```
1518
*/
1619
class FaradayHttpRequest extends HTTP::Client::Request::Range {
17-
DataFlow::Node requestUse;
1820
API::Node requestNode;
1921
API::Node connectionNode;
22+
DataFlow::Node connectionUse;
23+
DataFlow::CallNode requestUse;
2024

2125
FaradayHttpRequest() {
2226
connectionNode =
@@ -29,11 +33,18 @@ class FaradayHttpRequest extends HTTP::Client::Request::Range {
2933
requestNode =
3034
connectionNode.getReturn(["get", "head", "delete", "post", "put", "patch", "trace"]) and
3135
requestUse = requestNode.getAnImmediateUse() and
36+
connectionUse = connectionNode.getAnImmediateUse() and
3237
this = requestUse.asExpr().getExpr()
3338
}
3439

3540
override DataFlow::Node getResponseBody() { result = requestNode.getAMethodCall("body") }
3641

42+
override DataFlow::Node getURL() {
43+
result = requestUse.getArgument(0) or
44+
result = connectionUse.(DataFlow::CallNode).getArgument(0) or
45+
result = connectionUse.(DataFlow::CallNode).getKeywordArgument("url")
46+
}
47+
3748
override predicate disablesCertificateValidation(DataFlow::Node disablingNode) {
3849
// `Faraday::new` takes an options hash as its second argument, and we're
3950
// looking for

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ private import codeql.ruby.ApiGraphs
1212
class HttpClientRequest extends HTTP::Client::Request::Range {
1313
API::Node requestNode;
1414
API::Node connectionNode;
15-
DataFlow::Node requestUse;
15+
DataFlow::CallNode requestUse;
1616
string method;
1717

1818
HttpClientRequest() {
@@ -31,6 +31,8 @@ class HttpClientRequest extends HTTP::Client::Request::Range {
3131
this = requestUse.asExpr().getExpr()
3232
}
3333

34+
override DataFlow::Node getURL() { result = requestUse.getArgument(0) }
35+
3436
override DataFlow::Node getResponseBody() {
3537
// The `get_content` and `post_content` methods return the response body as
3638
// a string. The other methods return a `HTTPClient::Message` object which

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,15 @@ private import codeql.ruby.ApiGraphs
1111
* # TODO: module inclusion
1212
* class MyClass
1313
* include HTTParty
14+
* base_uri "http://example.com"
1415
* end
1516
*
1617
* MyClass.new("http://example.com")
1718
* ```
1819
*/
1920
class HttpartyRequest extends HTTP::Client::Request::Range {
2021
API::Node requestNode;
21-
DataFlow::Node requestUse;
22+
DataFlow::CallNode requestUse;
2223

2324
HttpartyRequest() {
2425
requestUse = requestNode.getAnImmediateUse() and
@@ -28,6 +29,8 @@ class HttpartyRequest extends HTTP::Client::Request::Range {
2829
this = requestUse.asExpr().getExpr()
2930
}
3031

32+
override DataFlow::Node getURL() { result = requestUse.getArgument(0) }
33+
3134
override DataFlow::Node getResponseBody() {
3235
// If HTTParty can recognise the response type, it will parse and return it
3336
// directly from the request call. Otherwise, it will return a `HTTParty::Response`

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ class NetHttpRequest extends HTTP::Client::Request::Range {
4646
* Gets the node representing the URL of the request.
4747
* Currently unused, but may be useful in future, e.g. to filter out certain requests.
4848
*/
49-
DataFlow::Node getURLArgument() { result = request.getArgument(0) }
49+
override DataFlow::Node getURL() { result = request.getArgument(0) }
5050

5151
override DataFlow::Node getResponseBody() { result = responseBody }
5252

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

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ private import codeql.ruby.frameworks.StandardLibrary
1414
*/
1515
class OpenUriRequest extends HTTP::Client::Request::Range {
1616
API::Node requestNode;
17-
DataFlow::Node requestUse;
17+
DataFlow::CallNode requestUse;
1818

1919
OpenUriRequest() {
2020
requestNode =
@@ -24,6 +24,8 @@ class OpenUriRequest extends HTTP::Client::Request::Range {
2424
this = requestUse.asExpr().getExpr()
2525
}
2626

27+
override DataFlow::Node getURL() { result = requestUse.getArgument(0) }
28+
2729
override DataFlow::Node getResponseBody() {
2830
result = requestNode.getAMethodCall(["read", "readlines"])
2931
}
@@ -48,14 +50,16 @@ class OpenUriRequest extends HTTP::Client::Request::Range {
4850
* ```
4951
*/
5052
class OpenUriKernelOpenRequest extends HTTP::Client::Request::Range {
51-
DataFlow::Node requestUse;
53+
DataFlow::CallNode requestUse;
5254

5355
OpenUriKernelOpenRequest() {
5456
requestUse instanceof KernelMethodCall and
5557
this.getMethodName() = "open" and
5658
this = requestUse.asExpr().getExpr()
5759
}
5860

61+
override DataFlow::Node getURL() { result = requestUse.getArgument(0) }
62+
5963
override DataFlow::CallNode getResponseBody() {
6064
result.asExpr().getExpr().(MethodCall).getMethodName() in ["read", "readlines"] and
6165
requestUse.(DataFlow::LocalSourceNode).flowsTo(result.getReceiver())

0 commit comments

Comments
 (0)