Skip to content

Commit d71dd3f

Browse files
committed
rb/reflected-xss
1 parent d3a1d0a commit d71dd3f

11 files changed

Lines changed: 449 additions & 0 deletions

File tree

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
/**
2+
* Provides default sources, sinks and sanitizers for detecting
3+
* "reflected server-side cross-site scripting"
4+
* vulnerabilities, as well as extension points for adding your own.
5+
*/
6+
7+
private import ruby
8+
private import codeql.ruby.DataFlow
9+
private import codeql.ruby.CFG
10+
private import codeql.ruby.Concepts
11+
private import codeql.ruby.Frameworks
12+
private import codeql.ruby.frameworks.ActionController
13+
private import codeql.ruby.frameworks.ActionView
14+
private import codeql.ruby.dataflow.RemoteFlowSources
15+
private import codeql.ruby.dataflow.BarrierGuards
16+
private import codeql.ruby.typetracking.TypeTracker
17+
18+
/**
19+
* Provides default sources, sinks and sanitizers for detecting
20+
* "reflected server-side cross-site scripting"
21+
* vulnerabilities, as well as extension points for adding your own.
22+
*/
23+
module ReflectedXSS {
24+
/**
25+
* A data flow source for "reflected server-side cross-site scripting" vulnerabilities.
26+
*/
27+
abstract class Source extends DataFlow::Node { }
28+
29+
/**
30+
* A data flow sink for "reflected server-side cross-site scripting" vulnerabilities.
31+
*/
32+
abstract class Sink extends DataFlow::Node { }
33+
34+
/**
35+
* A sanitizer for "reflected server-side cross-site scripting" vulnerabilities.
36+
*/
37+
abstract class Sanitizer extends DataFlow::Node { }
38+
39+
/**
40+
* A sanitizer guard for "reflected server-side cross-site scripting" vulnerabilities.
41+
*/
42+
abstract class SanitizerGuard extends DataFlow::BarrierGuard { }
43+
44+
/**
45+
* A source of remote user input, considered as a flow source.
46+
*/
47+
class RemoteFlowSourceAsSource extends Source, RemoteFlowSource { }
48+
49+
private class ErbOutputMethodCallArgumentNode extends DataFlow::Node {
50+
private MethodCall call;
51+
52+
ErbOutputMethodCallArgumentNode() {
53+
exists(ErbOutputDirective d |
54+
call = d.getTerminalStmt() and
55+
this.asExpr().getExpr() = call.getAnArgument()
56+
)
57+
}
58+
59+
MethodCall getCall() { result = call }
60+
}
61+
62+
/**
63+
* A node on which `html_safe` has been called to mark it as not requiring
64+
* HTML escaping, considered as a flow sink.
65+
*/
66+
class HtmlSafeCallAsSink extends Sink {
67+
// TODO: extend this to track strings that have been marked as html_safe
68+
// earlier in the control flow graph
69+
HtmlSafeCallAsSink() {
70+
exists(HtmlSafeCall c, ErbOutputDirective d |
71+
this.asExpr().getExpr() = c.getReceiver() and
72+
c = d.getTerminalStmt()
73+
)
74+
}
75+
}
76+
77+
/**
78+
* An argument to a call to the `raw` method, considered as a flow sink.
79+
*/
80+
class RawCallArgumentAsSink extends Sink, ErbOutputMethodCallArgumentNode {
81+
RawCallArgumentAsSink() { this.getCall() instanceof RawCall }
82+
}
83+
84+
/**
85+
* A argument to a call to the `link_to` method, which does not expect
86+
* unsanitized user-input, considered as a flow sink a flow sink.
87+
*/
88+
class LinkToCallArgumentAsSink extends Sink, ErbOutputMethodCallArgumentNode {
89+
LinkToCallArgumentAsSink() {
90+
this.asExpr().getExpr() = this.getCall().(LinkToCall).getPathArgument()
91+
}
92+
}
93+
94+
/**
95+
* An HTML escaping, considered as a sanitizer.
96+
*/
97+
class HtmlEscapingAsSanitizer extends Sanitizer {
98+
HtmlEscapingAsSanitizer() { this = any(HtmlEscaping esc).getOutput() }
99+
}
100+
101+
/**
102+
* A comparison with a constant string, considered as a sanitizer-guard.
103+
*/
104+
class StringConstCompareAsSanitizerGuard extends SanitizerGuard, StringConstCompare { }
105+
106+
/**
107+
* An inclusion check against an array of constant strings, considered as a sanitizer-guard.
108+
*/
109+
class StringConstArrayInclusionCallAsSanitizerGuard extends SanitizerGuard,
110+
StringConstArrayInclusionCall { }
111+
112+
/**
113+
* An additional step that is taint-preserving in the context of reflected XSS.
114+
*/
115+
predicate isAdditionalXSSTaintStep(DataFlow::Node node1, DataFlow::Node node2) {
116+
// node1 is a `locals` argument to a render call...
117+
exists(RenderCall call, Pair kvPair, string hashKey |
118+
call.getLocals().getAKeyValuePair() = kvPair and
119+
kvPair.getValue() = node1.asExpr().getExpr() and
120+
kvPair.getKey().(StringlikeLiteral).getValueText() = hashKey and
121+
// `node2` appears in the rendered template file
122+
call.getTemplateFile() = node2.getLocation().getFile() and
123+
(
124+
// ...node2 is a variable read
125+
exists(CfgNodes::ExprNodes::VariableReadAccessCfgNode readNode |
126+
readNode = node2.asExpr() and
127+
readNode.getExpr().getVariable().getName() = "@" + hashKey
128+
)
129+
or
130+
// ...node2 is an element reference against `locals`
131+
exists(
132+
CfgNodes::ExprNodes::ElementReferenceCfgNode refNode, DataFlow::Node argNode,
133+
CfgNodes::ExprNodes::StringlikeLiteralCfgNode strNode
134+
|
135+
refNode = node2.asExpr() and
136+
argNode.asExpr() = refNode.getArgument(0) and
137+
refNode.getReceiver().getExpr().(MethodCall).getMethodName() = "locals" and
138+
argNode.getALocalSource() = DataFlow::exprNode(strNode) and
139+
strNode.getExpr().getValueText() = hashKey
140+
)
141+
or
142+
// ...node2 is a "method call" to a "method" with `hashKey` as its name
143+
// TODO: This may be a variable read in reality that we interpret as a method call
144+
exists(MethodCall varAcc |
145+
varAcc = node2.asExpr().(CfgNodes::ExprNodes::MethodCallCfgNode).getExpr() and
146+
varAcc.getMethodName() = hashKey
147+
)
148+
)
149+
)
150+
or
151+
// instance variables in the controller
152+
exists(
153+
ActionControllerActionMethod action, VariableReadAccess viewVarRead,
154+
VariableWriteAccess controllerVarWrite
155+
|
156+
viewVarRead = node2.asExpr().(CfgNodes::ExprNodes::VariableReadAccessCfgNode).getExpr() and
157+
action.getDefaultTemplateFile() = viewVarRead.getLocation().getFile() and
158+
controllerVarWrite.getVariable() instanceof InstanceVariable and
159+
viewVarRead.getVariable().getName() = controllerVarWrite.getVariable().getName() and
160+
// TODO: include only final assignment along a path
161+
node1.asExpr().getExpr() = controllerVarWrite and
162+
controllerVarWrite.getParent+() = action
163+
)
164+
or
165+
// flow from template into controller helper method
166+
exists(
167+
ErbFile template, ActionControllerHelperMethod helperMethod,
168+
CfgNodes::ExprNodes::MethodCallCfgNode helperMethodCall, int argIdx
169+
|
170+
template = node1.getLocation().getFile() and
171+
helperMethod.getName() = helperMethodCall.getExpr().getMethodName() and
172+
helperMethod.getControllerClass() = getAssociatedControllerClass(template) and
173+
helperMethodCall.getArgument(argIdx) = node1.asExpr() and
174+
helperMethod.getParameter(argIdx) = node2.asExpr().getExpr()
175+
)
176+
or
177+
// flow out of controller helper method into template
178+
exists(
179+
ErbFile template, ActionControllerHelperMethod helperMethod,
180+
CfgNodes::ExprNodes::MethodCallCfgNode helperMethodCall, ReturnStmt ret
181+
|
182+
template = node2.getLocation().getFile() and
183+
helperMethod.getName() = helperMethodCall.getExpr().getMethodName() and
184+
helperMethod.getControllerClass() = getAssociatedControllerClass(template) and
185+
// `node1` is a returned value
186+
// TODO: we don't pick up implicit returns with this approach
187+
node1.asExpr().getExpr().getParent() = ret and
188+
ret.getParent+() = helperMethod and
189+
node2.asExpr() = helperMethodCall
190+
)
191+
}
192+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/**
2+
* Provides a taint-tracking configuration for detecting "reflected server-side cross-site scripting" vulnerabilities.
3+
*
4+
* Note, for performance reasons: only import this file if
5+
* `ReflectedXSS::Configuration` is needed, otherwise
6+
* `ReflectedXSSCustomizations` should be imported instead.
7+
*/
8+
9+
private import ruby
10+
import codeql.ruby.DataFlow
11+
import codeql.ruby.TaintTracking
12+
13+
/**
14+
* Provides a taint-tracking configuration for detecting "reflected server-side cross-site scripting" vulnerabilities.
15+
*/
16+
module ReflectedXSS {
17+
import ReflectedXSSCustomizations::ReflectedXSS
18+
19+
/**
20+
* A taint-tracking configuration for detecting "reflected server-side cross-site scripting" vulnerabilities.
21+
*/
22+
class Configuration extends TaintTracking::Configuration {
23+
Configuration() { this = "ReflectedXSS" }
24+
25+
override predicate isSource(DataFlow::Node source) { source instanceof Source }
26+
27+
override predicate isSink(DataFlow::Node sink) { sink instanceof Sink }
28+
29+
override predicate isSanitizer(DataFlow::Node node) { node instanceof Sanitizer }
30+
31+
override predicate isSanitizerGuard(DataFlow::BarrierGuard guard) {
32+
guard instanceof SanitizerGuard
33+
}
34+
35+
override predicate isAdditionalTaintStep(DataFlow::Node node1, DataFlow::Node node2) {
36+
isAdditionalXSSTaintStep(node1, node2)
37+
}
38+
}
39+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
<!DOCTYPE qhelp PUBLIC
2+
"-//Semmle//qhelp//EN"
3+
"qhelp.dtd">
4+
<qhelp>
5+
6+
<overview>
7+
<p>
8+
Directly writing user input (for example, an HTTP request parameter) to a webpage
9+
without properly sanitizing the input first, allows for a cross-site scripting
10+
vulnerability.
11+
</p>
12+
</overview>
13+
14+
<recommendation>
15+
<p>
16+
To guard against cross-site scripting, consider escaping the input before
17+
writing user input to the page. In some frameworks, such as Rails, escaping will
18+
be performed implicitly and by default.
19+
</p>
20+
</recommendation>
21+
22+
<example>
23+
<p>
24+
For instance, the following example is safe because the
25+
<code>params[:user_name]</code> content within the output tags will be
26+
automatically HTML escaped before being output.
27+
</p>
28+
<sample src="examples/safe.html.erb" />
29+
</example>
30+
31+
<recommendation>
32+
<p>
33+
Care should be taken when using methods such as <code>html_safe</code> or
34+
<code>raw</code>. These methods can be used to output a string without escaping
35+
it. As such, they should only be used when the string has already been manually
36+
escaped (for example, with the Rails <code>html_escape</code> method), or when
37+
the content is otherwise guaranteed to be safe (such as a hard-coded string).
38+
</p>
39+
</recommendation>
40+
41+
<example>
42+
<p>
43+
The following example is unsafe because user-controlled input is output without
44+
escaping due to being marked as <code>html_safe</code>.
45+
</p>
46+
<sample src="examples/reflective_xss.html.erb" />
47+
</example>
48+
49+
<references>
50+
<li>
51+
OWASP:
52+
<a href="https://cheatsheetseries.owasp.org/cheatsheets/Ruby_on_Rails_Cheat_Sheet.html#cross-site-scripting-xss">XSS
53+
Ruby on Rails Cheatsheet</a>.
54+
</li>
55+
<li>
56+
Wikipedia: <a href="http://en.wikipedia.org/wiki/Cross-site_scripting">Cross-site scripting</a>.
57+
</li>
58+
</references>
59+
</qhelp>
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/**
2+
* @name Reflected server-side cross-site scripting
3+
* @description Writing user input directly to a web page
4+
* allows for a cross-site scripting vulnerability.
5+
* @kind path-problem
6+
* @problem.severity error
7+
* @sub-severity high
8+
* @precision high
9+
* @id rb/reflected-xss
10+
* @tags security
11+
* external/cwe/cwe-079
12+
* external/cwe/cwe-116
13+
*/
14+
15+
import ruby
16+
import codeql.ruby.security.ReflectedXSSQuery
17+
import codeql.ruby.DataFlow
18+
import DataFlow::PathGraph
19+
20+
from ReflectedXSS::Configuration config, DataFlow::PathNode source, DataFlow::PathNode sink
21+
where config.hasFlowPath(source, sink)
22+
select sink.getNode(), source, sink, "Cross-site scripting vulnerability due to $@.",
23+
source.getNode(), "a user-provided value"
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<p>Hello <%= params[:user_name].html_safe %>!</p>
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<p>Hello <%= params[:user_name] %>!</p>
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
edges
2+
| app/controllers/foo/bars_controller.rb:10:12:10:17 | call to params : | app/controllers/foo/bars_controller.rb:10:12:10:29 | ...[...] : |
3+
| app/controllers/foo/bars_controller.rb:10:12:10:29 | ...[...] : | app/views/foo/bars/show.html.erb:49:5:49:13 | call to user_name |
4+
| app/controllers/foo/bars_controller.rb:19:10:19:15 | call to params : | app/controllers/foo/bars_controller.rb:20:53:20:54 | dt : |
5+
| app/controllers/foo/bars_controller.rb:20:53:20:54 | dt : | app/views/foo/bars/show.html.erb:5:9:5:21 | @display_text |
6+
| app/controllers/foo/bars_controller.rb:20:53:20:54 | dt : | app/views/foo/bars/show.html.erb:8:9:8:20 | call to display_text |
7+
| app/controllers/foo/bars_controller.rb:20:53:20:54 | dt : | app/views/foo/bars/show.html.erb:11:9:11:29 | ...[...] |
8+
| app/controllers/foo/bars_controller.rb:20:53:20:54 | dt : | app/views/foo/bars/show.html.erb:15:9:15:19 | ...[...] |
9+
| app/controllers/foo/bars_controller.rb:20:53:20:54 | dt : | app/views/foo/bars/show.html.erb:36:3:36:15 | @display_text |
10+
| app/controllers/foo/bars_controller.rb:20:53:20:54 | dt : | app/views/foo/bars/show.html.erb:46:76:46:87 | call to display_text : |
11+
| app/views/foo/bars/show.html.erb:46:64:46:87 | ... + ... : | app/views/foo/bars/_widget.html.erb:2:9:2:21 | @display_text |
12+
| app/views/foo/bars/show.html.erb:46:64:46:87 | ... + ... : | app/views/foo/bars/_widget.html.erb:5:9:5:20 | call to display_text |
13+
| app/views/foo/bars/show.html.erb:46:64:46:87 | ... + ... : | app/views/foo/bars/_widget.html.erb:8:9:8:29 | ...[...] |
14+
| app/views/foo/bars/show.html.erb:46:76:46:87 | call to display_text : | app/views/foo/bars/show.html.erb:46:64:46:87 | ... + ... : |
15+
| app/views/foo/bars/show.html.erb:56:29:56:34 | call to params : | app/views/foo/bars/show.html.erb:56:29:56:44 | ...[...] |
16+
nodes
17+
| app/controllers/foo/bars_controller.rb:10:12:10:17 | call to params : | semmle.label | call to params : |
18+
| app/controllers/foo/bars_controller.rb:10:12:10:29 | ...[...] : | semmle.label | ...[...] : |
19+
| app/controllers/foo/bars_controller.rb:19:10:19:15 | call to params : | semmle.label | call to params : |
20+
| app/controllers/foo/bars_controller.rb:20:53:20:54 | dt : | semmle.label | dt : |
21+
| app/views/foo/bars/_widget.html.erb:2:9:2:21 | @display_text | semmle.label | @display_text |
22+
| app/views/foo/bars/_widget.html.erb:5:9:5:20 | call to display_text | semmle.label | call to display_text |
23+
| app/views/foo/bars/_widget.html.erb:8:9:8:29 | ...[...] | semmle.label | ...[...] |
24+
| app/views/foo/bars/show.html.erb:5:9:5:21 | @display_text | semmle.label | @display_text |
25+
| app/views/foo/bars/show.html.erb:8:9:8:20 | call to display_text | semmle.label | call to display_text |
26+
| app/views/foo/bars/show.html.erb:11:9:11:29 | ...[...] | semmle.label | ...[...] |
27+
| app/views/foo/bars/show.html.erb:15:9:15:19 | ...[...] | semmle.label | ...[...] |
28+
| app/views/foo/bars/show.html.erb:36:3:36:15 | @display_text | semmle.label | @display_text |
29+
| app/views/foo/bars/show.html.erb:46:64:46:87 | ... + ... : | semmle.label | ... + ... : |
30+
| app/views/foo/bars/show.html.erb:46:76:46:87 | call to display_text : | semmle.label | call to display_text : |
31+
| app/views/foo/bars/show.html.erb:49:5:49:13 | call to user_name | semmle.label | call to user_name |
32+
| app/views/foo/bars/show.html.erb:56:29:56:34 | call to params : | semmle.label | call to params : |
33+
| app/views/foo/bars/show.html.erb:56:29:56:44 | ...[...] | semmle.label | ...[...] |
34+
#select
35+
| app/views/foo/bars/_widget.html.erb:2:9:2:21 | @display_text | app/controllers/foo/bars_controller.rb:19:10:19:15 | call to params : | app/views/foo/bars/_widget.html.erb:2:9:2:21 | @display_text | Cross-site scripting vulnerability due to $@. | app/controllers/foo/bars_controller.rb:19:10:19:15 | call to params | a user-provided value |
36+
| app/views/foo/bars/_widget.html.erb:5:9:5:20 | call to display_text | app/controllers/foo/bars_controller.rb:19:10:19: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 $@. | app/controllers/foo/bars_controller.rb:19:10:19:15 | call to params | a user-provided value |
37+
| app/views/foo/bars/_widget.html.erb:8:9:8:29 | ...[...] | app/controllers/foo/bars_controller.rb:19:10:19:15 | call to params : | app/views/foo/bars/_widget.html.erb:8:9:8:29 | ...[...] | Cross-site scripting vulnerability due to $@. | app/controllers/foo/bars_controller.rb:19:10:19:15 | call to params | a user-provided value |
38+
| app/views/foo/bars/show.html.erb:5:9:5:21 | @display_text | app/controllers/foo/bars_controller.rb:19:10:19:15 | call to params : | app/views/foo/bars/show.html.erb:5:9:5:21 | @display_text | Cross-site scripting vulnerability due to $@. | app/controllers/foo/bars_controller.rb:19:10:19:15 | call to params | a user-provided value |
39+
| app/views/foo/bars/show.html.erb:8:9:8:20 | call to display_text | app/controllers/foo/bars_controller.rb:19:10:19:15 | call to params : | app/views/foo/bars/show.html.erb:8:9:8:20 | call to display_text | Cross-site scripting vulnerability due to $@. | app/controllers/foo/bars_controller.rb:19:10:19:15 | call to params | a user-provided value |
40+
| app/views/foo/bars/show.html.erb:11:9:11:29 | ...[...] | app/controllers/foo/bars_controller.rb:19:10:19:15 | call to params : | app/views/foo/bars/show.html.erb:11:9:11:29 | ...[...] | Cross-site scripting vulnerability due to $@. | app/controllers/foo/bars_controller.rb:19:10:19:15 | call to params | a user-provided value |
41+
| app/views/foo/bars/show.html.erb:15:9:15:19 | ...[...] | app/controllers/foo/bars_controller.rb:19:10:19:15 | call to params : | app/views/foo/bars/show.html.erb:15:9:15:19 | ...[...] | Cross-site scripting vulnerability due to $@. | app/controllers/foo/bars_controller.rb:19:10:19:15 | call to params | a user-provided value |
42+
| app/views/foo/bars/show.html.erb:36:3:36:15 | @display_text | app/controllers/foo/bars_controller.rb:19:10:19:15 | call to params : | app/views/foo/bars/show.html.erb:36:3:36:15 | @display_text | Cross-site scripting vulnerability due to $@. | app/controllers/foo/bars_controller.rb:19:10:19:15 | call to params | a user-provided value |
43+
| app/views/foo/bars/show.html.erb:49:5:49:13 | call to user_name | app/controllers/foo/bars_controller.rb:10:12:10:17 | call to params : | app/views/foo/bars/show.html.erb:49:5:49:13 | call to user_name | Cross-site scripting vulnerability due to $@. | app/controllers/foo/bars_controller.rb:10:12:10:17 | call to params | a user-provided value |
44+
| app/views/foo/bars/show.html.erb:56:29:56:44 | ...[...] | app/views/foo/bars/show.html.erb:56:29:56:34 | call to params : | app/views/foo/bars/show.html.erb:56:29:56:44 | ...[...] | Cross-site scripting vulnerability due to $@. | app/views/foo/bars/show.html.erb:56:29:56:34 | call to params | a user-provided value |
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
queries/security/cwe-079/ReflectedXSS.ql
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
class BarsController < ApplicationController
2+
3+
helper_method :user_name, :user_name_memo
4+
5+
def index
6+
render template: "foo/bars/index"
7+
end
8+
9+
def user_name
10+
return params[:user_name]
11+
end
12+
13+
def user_name_memo
14+
@user_name ||= params[:user_name]
15+
end
16+
17+
def show
18+
@user_website = params[:website]
19+
dt = params[:text]
20+
render "foo/bars/show", locals: { display_text: dt, safe_text: "hello" }
21+
end
22+
end

0 commit comments

Comments
 (0)