Skip to content

Commit a9abba5

Browse files
authored
Merge pull request #15520 from hmac/hmac-erb-raw-output-directive
Ruby: Recognise raw Erb output as XSS sink
2 parents babae65 + 6cc5c09 commit a9abba5

File tree

5 files changed

+44
-2
lines changed

5 files changed

+44
-2
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
category: minorAnalysis
3+
---
4+
* Raw output ERB tags of the form `<%== ... %>` are now recognised as cross-site scripting sinks.

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

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -256,10 +256,30 @@ class ErbOutputDirective extends ErbDirective {
256256

257257
override ErbCode getToken() { toGenerated(result) = g.getChild() }
258258

259+
/**
260+
* Holds if this is a raw Erb output directive.
261+
* ```erb
262+
* <%== foo %>
263+
* ```
264+
*/
265+
predicate isRaw() {
266+
exists(Erb::Token t | t.getParentIndex() = 0 and t.getParent() = g and t.getValue() = "<%==")
267+
}
268+
259269
final override string toString() {
260-
result = "<%=" + this.getToken().toString() + "%>"
270+
this.isRaw() and
271+
(
272+
result = "<%==" + this.getToken().toString() + "%>"
273+
or
274+
not exists(this.getToken()) and result = "<%==%>"
275+
)
261276
or
262-
not exists(this.getToken()) and result = "<%=%>"
277+
not this.isRaw() and
278+
(
279+
result = "<%=" + this.getToken().toString() + "%>"
280+
or
281+
not exists(this.getToken()) and result = "<%=%>"
282+
)
263283
}
264284

265285
final override string getAPrimaryQlClass() { result = "ErbOutputDirective" }

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,18 @@ private module Shared {
4848
MethodCall getCall() { result = call }
4949
}
5050

51+
/**
52+
* A value interpolated using a raw erb output directive, which does not perform HTML escaping.
53+
* ```erb
54+
* <%== sink %>
55+
* ```
56+
*/
57+
class ErbRawOutputDirective extends Sink {
58+
ErbRawOutputDirective() {
59+
exists(ErbOutputDirective d | d.isRaw() | this.asExpr().getExpr() = d.getTerminalStmt())
60+
}
61+
}
62+
5163
/**
5264
* An `html_safe` call marking the output as not requiring HTML escaping,
5365
* considered as a flow sink.

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ edges
2121
| app/controllers/foo/bars_controller.rb:26:53:26:54 | dt | app/views/foo/bars/show.html.erb:17:15:17:27 | call to local_assigns [element :display_text] | provenance | |
2222
| app/controllers/foo/bars_controller.rb:26:53:26:54 | dt | app/views/foo/bars/show.html.erb:35:3:35:14 | call to display_text | provenance | |
2323
| app/controllers/foo/bars_controller.rb:26:53:26:54 | dt | app/views/foo/bars/show.html.erb:43:76:43:87 | call to display_text | provenance | |
24+
| app/controllers/foo/bars_controller.rb:26:53:26:54 | dt | app/views/foo/bars/show.html.erb:82:6:82:17 | call to display_text | provenance | |
2425
| app/controllers/foo/bars_controller.rb:30:5:30:7 | str | app/controllers/foo/bars_controller.rb:31:5:31:7 | str | provenance | |
2526
| app/controllers/foo/bars_controller.rb:30:11:30:16 | call to params | app/controllers/foo/bars_controller.rb:30:11:30:28 | ...[...] | provenance | |
2627
| app/controllers/foo/bars_controller.rb:30:11:30:28 | ...[...] | app/controllers/foo/bars_controller.rb:30:5:30:7 | str | provenance | |
@@ -90,6 +91,7 @@ nodes
9091
| app/views/foo/bars/show.html.erb:73:19:73:34 | ...[...] | semmle.label | ...[...] |
9192
| app/views/foo/bars/show.html.erb:76:28:76:33 | call to params | semmle.label | call to params |
9293
| app/views/foo/bars/show.html.erb:76:28:76:39 | ...[...] | semmle.label | ...[...] |
94+
| app/views/foo/bars/show.html.erb:82:6:82:17 | call to display_text | semmle.label | call to display_text |
9395
subpaths
9496
#select
9597
| app/controllers/foo/bars_controller.rb:24:39:24:59 | ... = ... | app/controllers/foo/bars_controller.rb:24:39:24:44 | call to params | app/controllers/foo/bars_controller.rb:24:39:24:59 | ... = ... | Cross-site scripting vulnerability due to a $@. | app/controllers/foo/bars_controller.rb:24:39:24:44 | call to params | user-provided value |
@@ -109,3 +111,4 @@ subpaths
109111
| app/views/foo/bars/show.html.erb:56:13:56:28 | ...[...] | app/views/foo/bars/show.html.erb:56:13:56:18 | call to params | app/views/foo/bars/show.html.erb:56:13:56:28 | ...[...] | Cross-site scripting vulnerability due to a $@. | app/views/foo/bars/show.html.erb:56:13:56:18 | call to params | user-provided value |
110112
| app/views/foo/bars/show.html.erb:73:19:73:34 | ...[...] | app/views/foo/bars/show.html.erb:73:19:73:24 | call to params | app/views/foo/bars/show.html.erb:73:19:73:34 | ...[...] | Cross-site scripting vulnerability due to a $@. | app/views/foo/bars/show.html.erb:73:19:73:24 | call to params | user-provided value |
111113
| app/views/foo/bars/show.html.erb:76:28:76:39 | ...[...] | app/views/foo/bars/show.html.erb:76:28:76:33 | call to params | app/views/foo/bars/show.html.erb:76:28:76:39 | ...[...] | Cross-site scripting vulnerability due to a $@. | app/views/foo/bars/show.html.erb:76:28:76:33 | call to params | user-provided value |
114+
| app/views/foo/bars/show.html.erb:82:6:82:17 | call to display_text | app/controllers/foo/bars_controller.rb:18:10:18:15 | call to params | app/views/foo/bars/show.html.erb:82:6:82:17 | 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 |

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
@@ -77,3 +77,6 @@
7777

7878
<%# GOOD: input is sanitized %>
7979
<%= sanitize(params[:comment]).html_safe %>
80+
81+
<%# BAD: A local rendered raw as a local variable %>
82+
<%== display_text %>

0 commit comments

Comments
 (0)