Skip to content

Commit 7d23170

Browse files
authored
Merge pull request github#10602 from hmac/hmac/actiondispatch-request
Ruby: Model ActionDispatch::Request
2 parents 5ce4483 + e6dc27a commit 7d23170

File tree

14 files changed

+424
-21
lines changed

14 files changed

+424
-21
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
category: minorAnalysis
3+
---
4+
* More sources of remote input arising from methods on `ActionDispatch::Request`
5+
are now recognised.

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

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,26 @@ module Http {
290290
}
291291
}
292292

293+
/** A kind of request input. */
294+
class RequestInputKind extends string {
295+
RequestInputKind() { this = ["parameter", "header", "body", "url", "cookie"] }
296+
}
297+
298+
/** Input from the parameters of a request. */
299+
RequestInputKind parameterInputKind() { result = "parameter" }
300+
301+
/** Input from the headers of a request. */
302+
RequestInputKind headerInputKind() { result = "header" }
303+
304+
/** Input from the body of a request. */
305+
RequestInputKind bodyInputKind() { result = "body" }
306+
307+
/** Input from the URL of a request. */
308+
RequestInputKind urlInputKind() { result = "url" }
309+
310+
/** Input from the cookies of a request. */
311+
RequestInputKind cookieInputKind() { result = "cookie" }
312+
293313
/**
294314
* An access to a user-controlled HTTP request input. For example, the URL or body of a request.
295315
* Instances of this class automatically become `RemoteFlowSource`s.
@@ -304,6 +324,32 @@ module Http {
304324
* This is typically the name of the method that gives rise to this input.
305325
*/
306326
string getSourceType() { result = super.getSourceType() }
327+
328+
/**
329+
* Gets the kind of the accessed input,
330+
* Can be one of "parameter", "header", "body", "url", "cookie".
331+
*/
332+
RequestInputKind getKind() { result = super.getKind() }
333+
334+
/**
335+
* Holds if this part of the request may be controlled by a third party,
336+
* that is, an agent other than the one who sent the request.
337+
*
338+
* This is true for the URL, query parameters, and request body.
339+
* These can be controlled by a malicious third party in the following scenarios:
340+
*
341+
* - The user clicks a malicious link or is otherwise redirected to a malicious URL.
342+
* - The user visits a web site that initiates a form submission or AJAX request on their behalf.
343+
*
344+
* In these cases, the request is technically sent from the user's browser, but
345+
* the user is not in direct control of the URL or POST body.
346+
*
347+
* Headers are never considered third-party controllable by this predicate, although the
348+
* third party does have some control over the the Referer and Origin headers.
349+
*/
350+
predicate isThirdPartyControllable() {
351+
this.getKind() = [parameterInputKind(), urlInputKind(), bodyInputKind()]
352+
}
307353
}
308354

309355
/** Provides a class for modeling new HTTP request inputs. */
@@ -321,6 +367,12 @@ module Http {
321367
* This is typically the name of the method that gives rise to this input.
322368
*/
323369
abstract string getSourceType();
370+
371+
/**
372+
* Gets the kind of the accessed input,
373+
* Can be one of "parameter", "header", "body", "url", "cookie".
374+
*/
375+
abstract RequestInputKind getKind();
324376
}
325377
}
326378

@@ -387,6 +439,8 @@ module Http {
387439
RoutedParameter() { this.getParameter() = handler.getARoutedParameter() }
388440

389441
override string getSourceType() { result = handler.getFramework() + " RoutedParameter" }
442+
443+
override RequestInputKind getKind() { result = parameterInputKind() }
390444
}
391445

392446
/**

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

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,8 @@ class ParamsSource extends Http::Server::RequestInputAccess::Range {
139139
ParamsSource() { this.asExpr().getExpr() instanceof Rails::ParamsCall }
140140

141141
override string getSourceType() { result = "ActionController::Metal#params" }
142+
143+
override Http::Server::RequestInputKind getKind() { result = Http::Server::parameterInputKind() }
142144
}
143145

144146
/**
@@ -149,6 +151,8 @@ class CookiesSource extends Http::Server::RequestInputAccess::Range {
149151
CookiesSource() { this.asExpr().getExpr() instanceof Rails::CookiesCall }
150152

151153
override string getSourceType() { result = "ActionController::Metal#cookies" }
154+
155+
override Http::Server::RequestInputKind getKind() { result = Http::Server::cookieInputKind() }
152156
}
153157

154158
/** A call to `cookies` from within a controller. */
@@ -161,6 +165,140 @@ private class ActionControllerParamsCall extends ActionControllerContextCall, Pa
161165
ActionControllerParamsCall() { this.getMethodName() = "params" }
162166
}
163167

168+
/** Modeling for `ActionDispatch::Request`. */
169+
private module Request {
170+
/**
171+
* A call to `request` from within a controller. This is an instance of
172+
* `ActionDispatch::Request`.
173+
*/
174+
private class RequestNode extends DataFlow::CallNode {
175+
RequestNode() {
176+
this.asExpr().getExpr() instanceof ActionControllerContextCall and
177+
this.getMethodName() = "request"
178+
}
179+
}
180+
181+
/**
182+
* A method call on `request`.
183+
*/
184+
private class RequestMethodCall extends DataFlow::CallNode {
185+
RequestMethodCall() {
186+
any(RequestNode r).(DataFlow::LocalSourceNode).flowsTo(this.getReceiver())
187+
}
188+
}
189+
190+
abstract private class RequestInputAccess extends RequestMethodCall,
191+
Http::Server::RequestInputAccess::Range {
192+
override string getSourceType() { result = "ActionDispatch::Request#" + this.getMethodName() }
193+
}
194+
195+
/**
196+
* A method call on `request` which returns request parameters.
197+
*/
198+
private class ParametersCall extends RequestInputAccess {
199+
ParametersCall() {
200+
this.getMethodName() =
201+
[
202+
"parameters", "params", "GET", "POST", "query_parameters", "request_parameters",
203+
"filtered_parameters"
204+
]
205+
}
206+
207+
override Http::Server::RequestInputKind getKind() {
208+
result = Http::Server::parameterInputKind()
209+
}
210+
}
211+
212+
/** A method call on `request` which returns part or all of the request path. */
213+
private class PathCall extends RequestInputAccess {
214+
PathCall() {
215+
this.getMethodName() =
216+
["path", "filtered_path", "fullpath", "original_fullpath", "original_url", "url"]
217+
}
218+
219+
override Http::Server::RequestInputKind getKind() { result = Http::Server::urlInputKind() }
220+
}
221+
222+
/** A method call on `request` which returns a specific request header. */
223+
private class HeadersCall extends RequestInputAccess {
224+
HeadersCall() {
225+
this.getMethodName() =
226+
[
227+
"authorization", "script_name", "path_info", "user_agent", "referer", "referrer",
228+
"host_authority", "content_type", "host", "hostname", "accept_encoding",
229+
"accept_language", "if_none_match", "if_none_match_etags", "content_mime_type"
230+
]
231+
or
232+
// Request headers are prefixed with `HTTP_` to distinguish them from
233+
// "headers" supplied by Rack middleware.
234+
this.getMethodName() = ["get_header", "fetch_header"] and
235+
this.getArgument(0).asExpr().getExpr().getConstantValue().getString().regexpMatch("^HTTP_.+")
236+
}
237+
238+
override Http::Server::RequestInputKind getKind() { result = Http::Server::headerInputKind() }
239+
}
240+
241+
// TODO: each_header
242+
/**
243+
* A method call on `request` which returns part or all of the host.
244+
* This can be influenced by headers such as Host and X-Forwarded-Host.
245+
*/
246+
private class HostCall extends RequestInputAccess {
247+
HostCall() {
248+
this.getMethodName() =
249+
[
250+
"authority", "host", "host_authority", "host_with_port", "hostname", "forwarded_for",
251+
"forwarded_host", "port", "forwarded_port"
252+
]
253+
}
254+
255+
override Http::Server::RequestInputKind getKind() { result = Http::Server::headerInputKind() }
256+
}
257+
258+
/**
259+
* A method call on `request` which is influenced by one or more request
260+
* headers.
261+
*/
262+
private class HeaderTaintedCall extends RequestInputAccess {
263+
HeaderTaintedCall() {
264+
this.getMethodName() = ["media_type", "media_type_params", "content_charset", "base_url"]
265+
}
266+
267+
override Http::Server::RequestInputKind getKind() { result = Http::Server::headerInputKind() }
268+
}
269+
270+
/** A method call on `request` which returns the request body. */
271+
private class BodyCall extends RequestInputAccess {
272+
BodyCall() { this.getMethodName() = ["body", "raw_post"] }
273+
274+
override Http::Server::RequestInputKind getKind() { result = Http::Server::bodyInputKind() }
275+
}
276+
277+
/**
278+
* A method call on `request` which returns the rack env.
279+
* This is a hash containing all the information about the request. Values
280+
* under keys starting with `HTTP_` are user-controlled.
281+
*/
282+
private class EnvCall extends RequestMethodCall {
283+
EnvCall() { this.getMethodName() = ["env", "filtered_env"] }
284+
}
285+
286+
/**
287+
* A read of a user-controlled parameter from the request env.
288+
*/
289+
private class EnvHttpAccess extends DataFlow::CallNode, Http::Server::RequestInputAccess::Range {
290+
EnvHttpAccess() {
291+
any(EnvCall c).(DataFlow::LocalSourceNode).flowsTo(this.getReceiver()) and
292+
this.getMethodName() = "[]" and
293+
this.getArgument(0).asExpr().getExpr().getConstantValue().getString().regexpMatch("^HTTP_.+")
294+
}
295+
296+
override Http::Server::RequestInputKind getKind() { result = Http::Server::headerInputKind() }
297+
298+
override string getSourceType() { result = "ActionDispatch::Request#env[]" }
299+
}
300+
}
301+
164302
/** A call to `render` from within a controller. */
165303
private class ActionControllerRenderCall extends ActionControllerContextCall, RenderCallImpl {
166304
ActionControllerRenderCall() { this.getMethodName() = "render" }

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,9 @@ module UrlRedirect {
5050
/**
5151
* A source of remote user input, considered as a flow source.
5252
*/
53-
class RemoteFlowSourceAsSource extends Source, RemoteFlowSource { }
53+
class HttpRequestInputAccessAsSource extends Source, Http::Server::RequestInputAccess {
54+
HttpRequestInputAccessAsSource() { this.isThirdPartyControllable() }
55+
}
5456

5557
/**
5658
* A HTTP redirect response, considered as a flow sink.

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -312,9 +312,11 @@ module ReflectedXss {
312312
deprecated predicate isAdditionalXSSTaintStep = isAdditionalXssTaintStep/2;
313313

314314
/**
315-
* A source of remote user input, considered as a flow source.
315+
* A HTTP request input, considered as a flow source.
316316
*/
317-
class RemoteFlowSourceAsSource extends Source, RemoteFlowSource { }
317+
class HttpRequestInputAccessAsSource extends Source, Http::Server::RequestInputAccess {
318+
HttpRequestInputAccessAsSource() { this.isThirdPartyControllable() }
319+
}
318320
}
319321

320322
/** DEPRECATED: Alias for ReflectedXss */

0 commit comments

Comments
 (0)