Skip to content

Commit 86398a8

Browse files
authored
Merge pull request #8304 from erik-krogh/xssUrl
JS: Refactor the XSS / Client-side-url queries
2 parents 7a9a9d8 + aa8b7c8 commit 86398a8

File tree

13 files changed

+1556
-86
lines changed

13 files changed

+1556
-86
lines changed

javascript/ql/lib/semmle/javascript/security/TaintedUrlSuffix.qll

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,15 @@ module TaintedUrlSuffix {
2626
*/
2727
FlowLabel label() { result instanceof TaintedUrlSuffixLabel }
2828

29+
/** Gets a remote flow source that is a tainted URL query or fragment part from `window.location`. */
30+
ClientSideRemoteFlowSource source() {
31+
result = DOM::locationRef().getAPropertyRead(["search", "hash"])
32+
or
33+
result = DOM::locationSource()
34+
or
35+
result.getKind().isUrl()
36+
}
37+
2938
/** Holds for `pred -> succ` is a step of form `x -> x.p` */
3039
private predicate isSafeLocationProp(DataFlow::PropRead read) {
3140
// Ignore properties that refer to the scheme, domain, port, auth, or path.

javascript/ql/lib/semmle/javascript/security/dataflow/ClientSideUrlRedirectCustomizations.qll

Lines changed: 36 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,6 @@ import javascript
88
import semmle.javascript.security.dataflow.RemoteFlowSources
99

1010
module ClientSideUrlRedirect {
11-
private import Xss::DomBasedXss as DomBasedXss
12-
1311
/**
1412
* A data flow source for unvalidated URL redirect vulnerabilities.
1513
*/
@@ -21,7 +19,12 @@ module ClientSideUrlRedirect {
2119
/**
2220
* A data flow sink for unvalidated URL redirect vulnerabilities.
2321
*/
24-
abstract class Sink extends DataFlow::Node { }
22+
abstract class Sink extends DataFlow::Node {
23+
/** Holds if the sink can execute JavaScript code in the current context. */
24+
predicate isXssSink() {
25+
none() // overwritten in subclasses
26+
}
27+
}
2528

2629
/**
2730
* A sanitizer for unvalidated URL redirect vulnerabilities.
@@ -86,37 +89,46 @@ module ClientSideUrlRedirect {
8689
* A sink which is used to set the window location.
8790
*/
8891
class LocationSink extends Sink, DataFlow::ValueNode {
92+
boolean xss;
93+
8994
LocationSink() {
9095
// A call to a `window.navigate` or `window.open`
9196
exists(string name | name = ["navigate", "open", "openDialog", "showModalDialog"] |
9297
this = DataFlow::globalVarRef(name).getACall().getArgument(0)
93-
)
98+
) and
99+
xss = false
94100
or
95101
// A call to `location.replace` or `location.assign`
96102
exists(DataFlow::MethodCallNode locationCall, string name |
97103
locationCall = DOM::locationRef().getAMethodCall(name) and
98104
this = locationCall.getArgument(0)
99105
|
100106
name = ["replace", "assign"]
101-
)
107+
) and
108+
xss = true
102109
or
103110
// An assignment to `location`
104-
exists(Assignment assgn | isLocation(assgn.getTarget()) and astNode = assgn.getRhs())
111+
exists(Assignment assgn | isLocation(assgn.getTarget()) and astNode = assgn.getRhs()) and
112+
xss = true
105113
or
106114
// An assignment to `location.href`, `location.protocol` or `location.hostname`
107115
exists(DataFlow::PropWrite pw, string propName |
108116
pw = DOM::locationRef().getAPropertyWrite(propName) and
109117
this = pw.getRhs()
110118
|
111-
propName = ["href", "protocol", "hostname"]
119+
propName = ["href", "protocol", "hostname"] and
120+
(if propName = "href" then xss = true else xss = false)
112121
)
113122
or
114123
// A redirection using the AngularJS `$location` service
115124
exists(AngularJS::ServiceReference service |
116125
service.getName() = "$location" and
117126
this.asExpr() = service.getAMethodCall("url").getArgument(0)
118-
)
127+
) and
128+
xss = false
119129
}
130+
131+
override predicate isXssSink() { xss = true }
120132
}
121133

122134
/**
@@ -156,16 +168,23 @@ module ClientSideUrlRedirect {
156168
}
157169

158170
/**
159-
* A script or iframe `src` attribute, viewed as a `ScriptUrlSink`.
171+
* A write to a `href` or similar attribute viewed as a `ScriptUrlSink`.
160172
*/
161-
class SrcAttributeUrlSink extends ScriptUrlSink, DataFlow::ValueNode {
162-
SrcAttributeUrlSink() {
173+
class AttributeUrlSink extends ScriptUrlSink {
174+
AttributeUrlSink() {
175+
// e.g. `$("<a>", {href: sink}).appendTo("body")`
163176
exists(DOM::AttributeDefinition attr |
164-
attr.getElement().getName() = ["script", "iframe"] and
165-
attr.getName() = "src" and
177+
not attr instanceof JsxAttribute and // handled more precisely in `ReactAttributeWriteUrlSink`.
178+
attr.getName() = DOM::getAPropertyNameInterpretedAsJavaScriptUrl()
179+
|
166180
this = attr.getValueNode()
167181
)
182+
or
183+
// e.g. node.setAttribute("href", sink)
184+
any(DomMethodCallExpr call).interpretsArgumentsAsURL(this.asExpr())
168185
}
186+
187+
override predicate isXssSink() { any() }
169188
}
170189

171190
/**
@@ -179,6 +198,8 @@ module ClientSideUrlRedirect {
179198
this = DataFlow::valueNode(pw.getRhs())
180199
)
181200
}
201+
202+
override predicate isXssSink() { any() }
182203
}
183204

184205
/**
@@ -195,6 +216,8 @@ module ClientSideUrlRedirect {
195216
this = attr.getValue().flow()
196217
)
197218
}
219+
220+
override predicate isXssSink() { any() }
198221
}
199222

200223
/**

javascript/ql/lib/semmle/javascript/security/dataflow/DOM.qll

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,17 @@ class DomMethodCallExpr extends MethodCallExpr {
9494
name = "createElement" and argPos = 0
9595
or
9696
name = "appendChild" and argPos = 0
97-
or
97+
)
98+
}
99+
100+
/**
101+
* Holds if `arg` is an argument that is used as an URL.
102+
*/
103+
predicate interpretsArgumentsAsURL(Expr arg) {
104+
exists(int argPos, string name |
105+
arg = this.getArgument(argPos) and
106+
name = this.getMethodName()
107+
|
98108
(
99109
name = "setAttribute" and argPos = 1
100110
or

javascript/ql/lib/semmle/javascript/security/dataflow/DomBasedXssCustomizations.qll

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,14 @@ module DomBasedXss {
1212
class RemoteFlowSourceAsSource extends Source {
1313
RemoteFlowSourceAsSource() { this instanceof RemoteFlowSource }
1414
}
15+
16+
/**
17+
* A flow-label representing tainted values where the prefix is attacker controlled.
18+
*/
19+
class PrefixString extends DataFlow::FlowLabel {
20+
PrefixString() { this = "PrefixString" }
21+
}
22+
23+
/** Gets the flow-label representing tainted values where the prefix is attacker controlled. */
24+
PrefixString prefixLabel() { any() }
1525
}

javascript/ql/lib/semmle/javascript/security/dataflow/DomBasedXssQuery.qll

Lines changed: 81 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -7,65 +7,61 @@ import javascript
77
private import semmle.javascript.security.TaintedUrlSuffix
88
import DomBasedXssCustomizations::DomBasedXss
99

10-
/**
11-
* DEPRECATED. Use `HtmlInjectionConfiguration` or `JQueryHtmlOrSelectorInjectionConfiguration`.
12-
*/
13-
deprecated class Configuration = HtmlInjectionConfiguration;
14-
1510
/**
1611
* DEPRECATED. Use `Vue::VHtmlSourceWrite` instead.
1712
*/
1813
deprecated class VHtmlSourceWrite = Vue::VHtmlSourceWrite;
1914

20-
/**
21-
* A taint-tracking configuration for reasoning about XSS.
22-
*/
23-
class HtmlInjectionConfiguration extends TaintTracking::Configuration {
24-
HtmlInjectionConfiguration() { this = "HtmlInjection" }
25-
26-
override predicate isSource(DataFlow::Node source) { source instanceof Source }
27-
28-
override predicate isSink(DataFlow::Node sink) {
29-
sink instanceof Sink and
30-
not sink instanceof JQueryHtmlOrSelectorSink // Handled by JQueryHtmlOrSelectorInjectionConfiguration below
31-
}
32-
33-
override predicate isSanitizer(DataFlow::Node node) {
34-
super.isSanitizer(node)
35-
or
36-
node instanceof Sanitizer
37-
}
15+
/** DEPRECATED. Use `Configuration`. */
16+
deprecated class HtmlInjectionConfiguration = Configuration;
3817

39-
override predicate isSanitizerGuard(TaintTracking::SanitizerGuardNode guard) {
40-
guard instanceof SanitizerGuard
41-
}
18+
/** DEPRECATED. Use `Configuration`. */
19+
deprecated class JQueryHtmlOrSelectorInjectionConfiguration = Configuration;
4220

43-
override predicate isSanitizerEdge(DataFlow::Node pred, DataFlow::Node succ) {
44-
isOptionallySanitizedEdge(pred, succ)
21+
/**
22+
* A sink that is not a URL write or a JQuery selector,
23+
* assumed to be a value that is interpreted as HTML.
24+
*/
25+
class HTMLSink extends DataFlow::Node instanceof Sink {
26+
HTMLSink() {
27+
not this instanceof WriteURLSink and
28+
not this instanceof JQueryHtmlOrSelectorSink
4529
}
4630
}
4731

4832
/**
49-
* A taint-tracking configuration for reasoning about injection into the jQuery `$` function
50-
* or similar, where the interpretation of the input string depends on its first character.
33+
* A taint-tracking configuration for reasoning about XSS.
34+
* Both ordinary HTML sinks, URL sinks, and JQuery selector based sinks.
35+
* - HTML sinks are sinks for any tainted value
36+
* - URL sinks are only sinks when the scheme is user controlled
37+
* - JQuery selector sinks are sinks when the tainted value can start with `<`.
5138
*
52-
* Values are only considered tainted if they can start with the `<` character.
39+
* The above is achieved using three flow labels:
40+
* - TaintedUrlSuffix: a URL where the attacker only controls a suffix.
41+
* - Taint: a tainted value where the attacker controls part of the value.
42+
* - PrefixLabel: a tainted value where the attacker controls the prefix
5343
*/
54-
class JQueryHtmlOrSelectorInjectionConfiguration extends TaintTracking::Configuration {
55-
JQueryHtmlOrSelectorInjectionConfiguration() { this = "JQueryHtmlOrSelectorInjection" }
44+
class Configuration extends TaintTracking::Configuration {
45+
Configuration() { this = "HtmlInjection" }
5646

5747
override predicate isSource(DataFlow::Node source, DataFlow::FlowLabel label) {
58-
// Reuse any source not derived from location
5948
source instanceof Source and
60-
not source = [DOM::locationRef(), DOM::locationRef().getAPropertyRead()] and
61-
label.isTaint()
49+
(label.isTaint() or label = prefixLabel()) and
50+
not source = TaintedUrlSuffix::source()
6251
or
63-
source = [DOM::locationSource(), DOM::locationRef().getAPropertyRead(["hash", "search"])] and
52+
source = TaintedUrlSuffix::source() and
6453
label = TaintedUrlSuffix::label()
6554
}
6655

6756
override predicate isSink(DataFlow::Node sink, DataFlow::FlowLabel label) {
68-
sink instanceof JQueryHtmlOrSelectorSink and label.isTaint()
57+
sink instanceof HTMLSink and
58+
label = [TaintedUrlSuffix::label(), prefixLabel(), DataFlow::FlowLabel::taint()]
59+
or
60+
sink instanceof JQueryHtmlOrSelectorSink and
61+
label = [DataFlow::FlowLabel::taint(), prefixLabel()]
62+
or
63+
sink instanceof WriteURLSink and
64+
label = prefixLabel()
6965
}
7066

7167
override predicate isSanitizer(DataFlow::Node node) {
@@ -78,6 +74,32 @@ class JQueryHtmlOrSelectorInjectionConfiguration extends TaintTracking::Configur
7874
guard instanceof SanitizerGuard
7975
}
8076

77+
override predicate isLabeledBarrier(DataFlow::Node node, DataFlow::FlowLabel lbl) {
78+
super.isLabeledBarrier(node, lbl)
79+
or
80+
// copy all taint barriers to the TaintedUrlSuffix/PrefixLabel label. This copies both the ordinary sanitizers and the sanitizer-guards.
81+
super.isLabeledBarrier(node, DataFlow::FlowLabel::taint()) and
82+
lbl = [TaintedUrlSuffix::label(), prefixLabel()]
83+
or
84+
// any non-first string-concatenation leaf is a barrier for the prefix label.
85+
exists(StringOps::ConcatenationRoot root |
86+
node = root.getALeaf() and
87+
not node = root.getFirstLeaf() and
88+
lbl = prefixLabel()
89+
)
90+
or
91+
// we assume that `.join()` calls have a prefix, and thus block the prefix label.
92+
node = any(DataFlow::MethodCallNode call | call.getMethodName() = "join") and
93+
lbl = prefixLabel()
94+
}
95+
96+
override predicate isSanitizerEdge(
97+
DataFlow::Node pred, DataFlow::Node succ, DataFlow::FlowLabel label
98+
) {
99+
isOptionallySanitizedEdge(pred, succ) and
100+
label = [DataFlow::FlowLabel::taint(), prefixLabel(), TaintedUrlSuffix::label()]
101+
}
102+
81103
override predicate isAdditionalFlowStep(
82104
DataFlow::Node src, DataFlow::Node trg, DataFlow::FlowLabel inlbl, DataFlow::FlowLabel outlbl
83105
) {
@@ -89,5 +111,26 @@ class JQueryHtmlOrSelectorInjectionConfiguration extends TaintTracking::Configur
89111
inlbl = TaintedUrlSuffix::label() and
90112
outlbl.isTaint()
91113
)
114+
or
115+
// inherit all ordinary taint steps for prefixLabel
116+
inlbl = prefixLabel() and
117+
outlbl = prefixLabel() and
118+
TaintTracking::sharedTaintStep(src, trg)
119+
or
120+
// steps out of taintedSuffixlabel to taint-label are also a steps to prefixLabel.
121+
TaintedUrlSuffix::step(src, trg, TaintedUrlSuffix::label(), DataFlow::FlowLabel::taint()) and
122+
inlbl = TaintedUrlSuffix::label() and
123+
outlbl = prefixLabel()
124+
}
125+
}
126+
127+
/**
128+
* A sanitizer that blocks the `PrefixString` label when the start of the string is being tested as being of a particular prefix.
129+
*/
130+
class PrefixStringSanitizer extends SanitizerGuard, TaintTracking::LabeledSanitizerGuardNode instanceof StringOps::StartsWith {
131+
override predicate sanitizes(boolean outcome, Expr e, DataFlow::FlowLabel label) {
132+
e = super.getBaseString().asExpr() and
133+
label = prefixLabel() and
134+
outcome = super.getPolarity()
92135
}
93136
}

javascript/ql/lib/semmle/javascript/security/dataflow/Xss.qll

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,15 @@ module DomBasedXss {
253253
}
254254
}
255255

256+
import ClientSideUrlRedirectCustomizations::ClientSideUrlRedirect as ClientSideUrlRedirect
257+
258+
/**
259+
* A write to a URL which may execute JavaScript code.
260+
*/
261+
class WriteURLSink extends Sink instanceof ClientSideUrlRedirect::Sink {
262+
WriteURLSink() { super.isXssSink() }
263+
}
264+
256265
/**
257266
* An expression whose value is interpreted as HTML or CSS
258267
* and may be inserted into the DOM.
@@ -347,7 +356,7 @@ module DomBasedXss {
347356
/**
348357
* A write to the `template` option of a Vue instance, viewed as an XSS sink.
349358
*/
350-
class VueTemplateSink extends DomBasedXss::Sink {
359+
class VueTemplateSink extends Sink {
351360
VueTemplateSink() {
352361
// Note: don't use Vue::Component#getTemplate as it includes an unwanted getALocalSource() step
353362
this = any(Vue::Component c).getOption("template")
@@ -358,7 +367,7 @@ module DomBasedXss {
358367
* The tag name argument to the `createElement` parameter of the
359368
* `render` method of a Vue instance, viewed as an XSS sink.
360369
*/
361-
class VueCreateElementSink extends DomBasedXss::Sink {
370+
class VueCreateElementSink extends Sink {
362371
VueCreateElementSink() {
363372
exists(Vue::Component c, DataFlow::FunctionNode f |
364373
f.flowsTo(c.getRender()) and
@@ -370,12 +379,12 @@ module DomBasedXss {
370379
/**
371380
* A Vue `v-html` attribute, viewed as an XSS sink.
372381
*/
373-
class VHtmlSink extends Vue::VHtmlAttribute, DomBasedXss::Sink { }
382+
class VHtmlSink extends Vue::VHtmlAttribute, Sink { }
374383

375384
/**
376385
* A raw interpolation tag in a template file, viewed as an XSS sink.
377386
*/
378-
class TemplateSink extends DomBasedXss::Sink {
387+
class TemplateSink extends Sink {
379388
TemplateSink() {
380389
exists(Templating::TemplatePlaceholderTag tag |
381390
tag.isRawInterpolation() and
@@ -388,7 +397,7 @@ module DomBasedXss {
388397
* A value being piped into the `safe` pipe in a template file,
389398
* disabling subsequent HTML escaping.
390399
*/
391-
class SafePipe extends DomBasedXss::Sink {
400+
class SafePipe extends Sink {
392401
SafePipe() { this = Templating::getAPipeCall("safe").getArgument(0) }
393402
}
394403

0 commit comments

Comments
 (0)