Skip to content

Commit 1d693d3

Browse files
committed
Ruby: Model javascript_include_tag and friends
1 parent 35a05f6 commit 1d693d3

File tree

5 files changed

+126
-92
lines changed

5 files changed

+126
-92
lines changed

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

Lines changed: 109 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -218,114 +218,134 @@ class FileSystemResolverAccess extends DataFlow::CallNode, FileSystemAccess::Ran
218218
}
219219

220220
// TODO: model flow in/out of template files properly,
221-
//
222-
/**
223-
* Action view helper methods which are XSS sinks.
224-
*/
225-
module ActionViewHelpers {
221+
// TODO: Move the classes and predicates above inside this module.
222+
/** Modeling for `ActionView`. */
223+
module ActionView {
226224
/**
227-
* Calls to ActionView helpers which render their argument without escaping.
228-
* These arguments should be treated as XSS sinks.
229-
* In the documentation for classes in this module, the vulnerable argument is
230-
* named `x`.
225+
* Action view helper methods which are XSS sinks.
231226
*/
232-
abstract class RawHelperCall extends MethodCall {
233-
abstract Expr getRawArgument();
234-
}
235-
236-
/**
237-
* `ActionView::Helpers::TextHelper#simple_format`.
238-
*
239-
* `simple_format(x, y, sanitize: false)`.
240-
*/
241-
private class SimpleFormat extends ActionViewContextCall, RawHelperCall {
242-
SimpleFormat() {
243-
this.getMethodName() = "simple_format" and
244-
this.getKeywordArgument("sanitize").getConstantValue().isBoolean(false)
227+
module Helpers {
228+
/**
229+
* Calls to ActionView helpers which render their argument without escaping.
230+
* These arguments should be treated as XSS sinks.
231+
* In the documentation for classes in this module, the vulnerable argument is
232+
* named `x`.
233+
*/
234+
abstract class RawHelperCall extends MethodCall {
235+
abstract Expr getRawArgument();
245236
}
246237

247-
override Expr getRawArgument() { result = this.getArgument(0) }
248-
}
249-
250-
/**
251-
* `ActionView::Helpers::TextHelper#truncate`.
252-
*
253-
* `truncate(x, escape: false)`.
254-
*/
255-
private class Truncate extends ActionViewContextCall, RawHelperCall {
256-
Truncate() {
257-
this.getMethodName() = "truncate" and
258-
this.getKeywordArgument("escape").getConstantValue().isBoolean(false)
238+
/**
239+
* `ActionView::Helpers::TextHelper#simple_format`.
240+
*
241+
* `simple_format(x, y, sanitize: false)`.
242+
*/
243+
private class SimpleFormat extends ActionViewContextCall, RawHelperCall {
244+
SimpleFormat() {
245+
this.getMethodName() = "simple_format" and
246+
this.getKeywordArgument("sanitize").getConstantValue().isBoolean(false)
247+
}
248+
249+
override Expr getRawArgument() { result = this.getArgument(0) }
259250
}
260251

261-
override Expr getRawArgument() { result = this.getArgument(0) }
262-
}
263-
264-
/**
265-
* `ActionView::Helpers::TextHelper#highlight`.
266-
*
267-
* `truncate(x, y, sanitize: false)`.
268-
*/
269-
private class Highlight extends ActionViewContextCall, RawHelperCall {
270-
Highlight() {
271-
this.getMethodName() = "highlight" and
272-
this.getKeywordArgument("sanitize").getConstantValue().isBoolean(false)
252+
/**
253+
* `ActionView::Helpers::TextHelper#truncate`.
254+
*
255+
* `truncate(x, escape: false)`.
256+
*/
257+
private class Truncate extends ActionViewContextCall, RawHelperCall {
258+
Truncate() {
259+
this.getMethodName() = "truncate" and
260+
this.getKeywordArgument("escape").getConstantValue().isBoolean(false)
261+
}
262+
263+
override Expr getRawArgument() { result = this.getArgument(0) }
273264
}
274265

275-
override Expr getRawArgument() { result = this.getArgument(0) }
276-
}
277-
278-
/**
279-
* `ActionView::Helpers::JavascriptHelper#javascript_tag`.
280-
*
281-
* `javascript_tag(x)`.
282-
*/
283-
private class JavascriptTag extends ActionViewContextCall, RawHelperCall {
284-
JavascriptTag() { this.getMethodName() = "javascript_tag" }
266+
/**
267+
* `ActionView::Helpers::TextHelper#highlight`.
268+
*
269+
* `truncate(x, y, sanitize: false)`.
270+
*/
271+
private class Highlight extends ActionViewContextCall, RawHelperCall {
272+
Highlight() {
273+
this.getMethodName() = "highlight" and
274+
this.getKeywordArgument("sanitize").getConstantValue().isBoolean(false)
275+
}
276+
277+
override Expr getRawArgument() { result = this.getArgument(0) }
278+
}
285279

286-
override Expr getRawArgument() { result = this.getArgument(0) }
287-
}
280+
/**
281+
* `ActionView::Helpers::JavascriptHelper#javascript_tag`.
282+
*
283+
* `javascript_tag(x)`.
284+
*/
285+
private class JavascriptTag extends ActionViewContextCall, RawHelperCall {
286+
JavascriptTag() { this.getMethodName() = "javascript_tag" }
288287

289-
/**
290-
* `ActionView::Helpers::TagHelper#tag`.
291-
*
292-
* `tag(x, x, y, false)`.
293-
*/
294-
private class ContentTag extends ActionViewContextCall, RawHelperCall {
295-
ContentTag() {
296-
this.getMethodName() = "content_tag" and
297-
this.getArgument(3).getConstantValue().isBoolean(false)
288+
override Expr getRawArgument() { result = this.getArgument(0) }
298289
}
299290

300-
override Expr getRawArgument() { result = this.getArgument(1) }
301-
}
291+
/**
292+
* `ActionView::Helpers::TagHelper#tag`.
293+
*
294+
* `tag(x, x, y, false)`.
295+
*/
296+
private class ContentTag extends ActionViewContextCall, RawHelperCall {
297+
ContentTag() {
298+
this.getMethodName() = "content_tag" and
299+
this.getArgument(3).getConstantValue().isBoolean(false)
300+
}
301+
302+
override Expr getRawArgument() { result = this.getArgument(1) }
303+
}
302304

303-
/**
304-
* `ActionView::Helpers::TagHelper#tag`.
305-
*
306-
* `tag(x, x, y, false)`.
307-
*/
308-
private class Tag extends ActionViewContextCall, RawHelperCall {
309-
Tag() {
310-
this.getMethodName() = "tag" and
311-
this.getArgument(3).getConstantValue().isBoolean(false)
305+
/**
306+
* `ActionView::Helpers::TagHelper#tag`.
307+
*
308+
* `tag(x, x, y, false)`.
309+
*/
310+
private class Tag extends ActionViewContextCall, RawHelperCall {
311+
Tag() {
312+
this.getMethodName() = "tag" and
313+
this.getArgument(3).getConstantValue().isBoolean(false)
314+
}
315+
316+
override Expr getRawArgument() { result = this.getArgument(0) }
312317
}
313318

314-
override Expr getRawArgument() { result = this.getArgument(0) }
319+
/**
320+
* `ActionView::Helpers::TagHelper#tag.<tag name>`.
321+
*
322+
* `tag.h1(x, escape: false)`.
323+
*/
324+
private class TagMethod extends MethodCall, RawHelperCall {
325+
TagMethod() {
326+
inActionViewContext(this) and
327+
this.getReceiver().(MethodCall).getMethodName() = "tag" and
328+
this.getKeywordArgument("escape").getConstantValue().isBoolean(false)
329+
}
330+
331+
override Expr getRawArgument() { result = this.getArgument(0) }
332+
}
315333
}
316334

317335
/**
318-
* `ActionView::Helpers::TagHelper#tag.<tag name>`.
319-
*
320-
* `tag.h1(x, escape: false)`.
336+
* An argument to a method call which constructs a script tag, interpreting the
337+
* argument as a URL. Remote input flowing to this argument may allow loading of
338+
* arbitrary javascript.
321339
*/
322-
private class TagMethod extends MethodCall, RawHelperCall {
323-
TagMethod() {
324-
inActionViewContext(this) and
325-
this.getReceiver().(MethodCall).getMethodName() = "tag" and
326-
this.getKeywordArgument("escape").getConstantValue().isBoolean(false)
340+
class ArgumentInterpretedAsUrl extends DataFlow::Node {
341+
ArgumentInterpretedAsUrl() {
342+
exists(DataFlow::CallNode call |
343+
call.getMethodName() = ["javascript_include_tag", "javascript_path", "path_to_javascript"] and
344+
this = call.getArgument(0)
345+
or
346+
call.getMethodName() = "javascript_url" and
347+
this = call.getKeywordArgument("host")
348+
)
327349
}
328-
329-
override Expr getRawArgument() { result = this.getArgument(0) }
330350
}
331351
}

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

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,14 +79,21 @@ private module Shared {
7979
* An argument to an ActionView helper method which is not escaped,
8080
* considered as a flow sink.
8181
*/
82-
class RawHelperCallArgumentAsSink extends Sink, ErbOutputMethodCallArgumentNode {
82+
class RawHelperCallArgumentAsSink extends Sink {
8383
RawHelperCallArgumentAsSink() {
84-
exists(ErbOutputDirective d, ActionViewHelpers::RawHelperCall c |
84+
exists(ErbOutputDirective d, ActionView::Helpers::RawHelperCall c |
8585
d.getTerminalStmt() = c and this.asExpr().getExpr() = c.getRawArgument()
8686
)
8787
}
8888
}
8989

90+
/**
91+
* An argument that is used to construct the `src` attribute of a `<script>`
92+
* tag.
93+
*/
94+
class ArgumentInterpretedAsUrlAsSink extends Sink, ErbOutputMethodCallArgumentNode,
95+
ActionView::ArgumentInterpretedAsUrl { }
96+
9097
/**
9198
* A argument to a call to the `link_to` method, which does not expect
9299
* unsanitized user-input, considered as a flow sink.

ruby/ql/test/library-tests/frameworks/ActionView.ql

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,6 @@ query predicate httpResponses(Http::Server::HttpResponse r, DataFlow::Node body,
1818
r.getBody() = body and r.getMimetype() = mimeType
1919
}
2020

21-
query predicate rawHelperCalls(ActionViewHelpers::RawHelperCall c, Expr arg) {
21+
query predicate rawHelperCalls(ActionView::Helpers::RawHelperCall c, Expr arg) {
2222
arg = c.getRawArgument()
2323
}

ruby/ql/test/query-tests/security/cwe-079/ReflectedXSS.expected

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ edges
2323
| app/views/foo/bars/show.html.erb:54:29:54:34 | call to params : | app/views/foo/bars/show.html.erb:54:29:54:44 | ...[...] |
2424
| app/views/foo/bars/show.html.erb:57:13:57:18 | call to params : | app/views/foo/bars/show.html.erb:57:13:57:28 | ...[...] |
2525
| app/views/foo/bars/show.html.erb:74:19:74:24 | call to params : | app/views/foo/bars/show.html.erb:74:19:74:34 | ...[...] |
26+
| app/views/foo/bars/show.html.erb:77:28:77:33 | call to params : | app/views/foo/bars/show.html.erb:77:28:77:39 | ...[...] |
2627
nodes
2728
| app/controllers/foo/bars_controller.rb:9:12:9:17 | call to params : | semmle.label | call to params : |
2829
| app/controllers/foo/bars_controller.rb:9:12:9:29 | ...[...] : | semmle.label | ...[...] : |
@@ -53,6 +54,8 @@ nodes
5354
| app/views/foo/bars/show.html.erb:57:13:57:28 | ...[...] | semmle.label | ...[...] |
5455
| app/views/foo/bars/show.html.erb:74:19:74:24 | call to params : | semmle.label | call to params : |
5556
| app/views/foo/bars/show.html.erb:74:19:74:34 | ...[...] | semmle.label | ...[...] |
57+
| app/views/foo/bars/show.html.erb:77:28:77:33 | call to params : | semmle.label | call to params : |
58+
| app/views/foo/bars/show.html.erb:77:28:77:39 | ...[...] | semmle.label | ...[...] |
5659
subpaths
5760
#select
5861
| app/views/foo/bars/_widget.html.erb:5:9:5:20 | call to display_text | app/controllers/foo/bars_controller.rb:18:10:18:15 | call to params : | app/views/foo/bars/_widget.html.erb:5:9:5:20 | call to display_text | Cross-site scripting vulnerability due to a $@. | app/controllers/foo/bars_controller.rb:18:10:18:15 | call to params | user-provided value |
@@ -68,3 +71,4 @@ subpaths
6871
| app/views/foo/bars/show.html.erb:54:29:54:44 | ...[...] | app/views/foo/bars/show.html.erb:54:29:54:34 | call to params : | app/views/foo/bars/show.html.erb:54:29:54:44 | ...[...] | Cross-site scripting vulnerability due to a $@. | app/views/foo/bars/show.html.erb:54:29:54:34 | call to params | user-provided value |
6972
| app/views/foo/bars/show.html.erb:57:13:57:28 | ...[...] | app/views/foo/bars/show.html.erb:57:13:57:18 | call to params : | app/views/foo/bars/show.html.erb:57:13:57:28 | ...[...] | Cross-site scripting vulnerability due to a $@. | app/views/foo/bars/show.html.erb:57:13:57:18 | call to params | user-provided value |
7073
| app/views/foo/bars/show.html.erb:74:19:74:34 | ...[...] | app/views/foo/bars/show.html.erb:74:19:74:24 | call to params : | app/views/foo/bars/show.html.erb:74:19:74:34 | ...[...] | Cross-site scripting vulnerability due to a $@. | app/views/foo/bars/show.html.erb:74:19:74:24 | call to params | user-provided value |
74+
| app/views/foo/bars/show.html.erb:77:28:77:39 | ...[...] | app/views/foo/bars/show.html.erb:77:28:77:33 | call to params : | app/views/foo/bars/show.html.erb:77:28:77:39 | ...[...] | Cross-site scripting vulnerability due to a $@. | app/views/foo/bars/show.html.erb:77:28:77:33 | call to params | user-provided value |

ruby/ql/test/query-tests/security/cwe-079/app/views/foo/bars/show.html.erb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,3 +72,6 @@
7272

7373
<%# BAD: simple_format called with sanitize: false %>
7474
<%= simple_format(params[:comment], sanitize: false) %>
75+
76+
<%# BAD: javasript_include_tag called with remote input %>
77+
<%= javascript_include_tag params[:url] %>

0 commit comments

Comments
 (0)