Skip to content

Commit da58306

Browse files
authored
Merge pull request #4506 from asgerf/js/separate-jquery-config
Approved by esbena
2 parents 5874a7b + 5436bb1 commit da58306

File tree

9 files changed

+269
-118
lines changed

9 files changed

+269
-118
lines changed

change-notes/1.26/analysis-javascript.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,9 @@
5353
| Client-side URL redirect (`js/client-side-unvalidated-url-redirection`) | More results | This query now recognizes some unsafe uses of `importScripts()` inside WebWorkers. |
5454
| Missing CSRF middleware (`js/missing-token-validation`) | More results | This query now recognizes writes to cookie and session variables as potentially vulnerable to CSRF attacks. |
5555
| Missing CSRF middleware (`js/missing-token-validation`) | Fewer results | This query now recognizes more ways of protecting against CSRF attacks. |
56+
| Client-side cross-site scripting (`js/xss`) | More results | This query now tracks data flow from `location.hash` more precisely. |
5657

5758

5859
## Changes to libraries
5960
* The predicate `TypeAnnotation.hasQualifiedName` now works in more cases when the imported library was not present during extraction.
61+
* The class `DomBasedXss::Configuration` has been deprecated, as it has been split into `DomBasedXss::HtmlInjectionConfiguration` and `DomBasedXss::JQueryHtmlOrSelectorInjectionConfiguration`. Unless specifically working with jQuery sinks, subclasses should instead be based on `HtmlInjectionConfiguration`. To use both configurations in a query, see [Xss.ql](https://github.com/github/codeql/blob/main/javascript/ql/src/Security/CWE-079/Xss.ql) for an example.

javascript/ql/src/Security/CWE-079/Xss.ql

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,13 @@ import javascript
1515
import semmle.javascript.security.dataflow.DomBasedXss::DomBasedXss
1616
import DataFlow::PathGraph
1717

18-
from Configuration cfg, DataFlow::PathNode source, DataFlow::PathNode sink
19-
where cfg.hasFlowPath(source, sink)
18+
from DataFlow::Configuration cfg, DataFlow::PathNode source, DataFlow::PathNode sink
19+
where
20+
(
21+
cfg instanceof HtmlInjectionConfiguration or
22+
cfg instanceof JQueryHtmlOrSelectorInjectionConfiguration
23+
) and
24+
cfg.hasFlowPath(source, sink)
2025
select sink.getNode(), source, sink,
2126
sink.getNode().(Sink).getVulnerabilityKind() + " vulnerability due to $@.", source.getNode(),
2227
"user-provided value"

javascript/ql/src/semmle/javascript/security/dataflow/DomBasedXss.qll

Lines changed: 62 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,23 @@ import javascript
88
module DomBasedXss {
99
import DomBasedXssCustomizations::DomBasedXss
1010

11+
/**
12+
* DEPRECATED. Use `HtmlInjectionConfiguration` or `JQueryHtmlOrSelectorInjectionConfiguration`.
13+
*/
14+
deprecated class Configuration = HtmlInjectionConfiguration;
15+
1116
/**
1217
* A taint-tracking configuration for reasoning about XSS.
1318
*/
14-
class Configuration extends TaintTracking::Configuration {
15-
Configuration() { this = "DomBasedXss" }
19+
class HtmlInjectionConfiguration extends TaintTracking::Configuration {
20+
HtmlInjectionConfiguration() { this = "HtmlInjection" }
1621

1722
override predicate isSource(DataFlow::Node source) { source instanceof Source }
1823

19-
override predicate isSink(DataFlow::Node sink) { sink instanceof Sink }
24+
override predicate isSink(DataFlow::Node sink) {
25+
sink instanceof Sink and
26+
not sink instanceof JQueryHtmlOrSelectorSink // Handled by JQueryHtmlOrSelectorInjectionConfiguration below
27+
}
2028

2129
override predicate isSanitizer(DataFlow::Node node) {
2230
super.isSanitizer(node)
@@ -28,59 +36,68 @@ module DomBasedXss {
2836
guard instanceof SanitizerGuard
2937
}
3038

31-
override predicate isAdditionalStoreStep(
32-
DataFlow::Node pred, DataFlow::SourceNode succ, string prop
33-
) {
34-
exists(DataFlow::PropRead read |
35-
pred = read.getBase() and
36-
succ = read and
37-
read.getPropertyName() = "hash" and
38-
prop = urlSuffixPseudoProperty()
39-
)
39+
override predicate isSanitizerEdge(DataFlow::Node pred, DataFlow::Node succ) {
40+
DomBasedXss::isOptionallySanitizedEdge(pred, succ)
41+
}
42+
}
43+
44+
/**
45+
* A taint-tracking configuration for reasoning about injection into the jQuery `$` function
46+
* or similar, where the interpretation of the input string depends on its first character.
47+
*
48+
* Values are only considered tainted if they can start with the `<` character.
49+
*/
50+
class JQueryHtmlOrSelectorInjectionConfiguration extends TaintTracking::Configuration {
51+
JQueryHtmlOrSelectorInjectionConfiguration() { this = "JQueryHtmlOrSelectorInjection" }
52+
53+
override predicate isSource(DataFlow::Node source, DataFlow::FlowLabel label) {
54+
// Reuse any source not derived from location
55+
source instanceof Source and
56+
not source = DOM::locationRef() and
57+
label.isTaint()
58+
or
59+
source = DOM::locationSource() and
60+
label.isData() // Require transformation before reaching sink
61+
or
62+
source = DOM::locationRef().getAPropertyRead(["hash", "search"]) and
63+
label.isData() // Require transformation before reaching sink
64+
}
65+
66+
override predicate isSink(DataFlow::Node sink, DataFlow::FlowLabel label) {
67+
sink instanceof JQueryHtmlOrSelectorSink and label.isTaint()
4068
}
4169

42-
override predicate isAdditionalLoadStoreStep(
43-
DataFlow::Node pred, DataFlow::Node succ, string predProp, string succProp
70+
override predicate isAdditionalFlowStep(
71+
DataFlow::Node pred, DataFlow::Node succ, DataFlow::FlowLabel predlbl,
72+
DataFlow::FlowLabel succlbl
4473
) {
45-
exists(DataFlow::PropRead read |
46-
pred = read.getBase() and
47-
succ = read and
48-
read.getPropertyName() = "hash" and
49-
predProp = "hash" and
50-
succProp = urlSuffixPseudoProperty()
74+
exists(TaintTracking::AdditionalTaintStep step |
75+
step.step(pred, succ) and
76+
predlbl.isData() and
77+
succlbl.isTaint()
5178
)
5279
}
5380

54-
override predicate isAdditionalLoadStep(DataFlow::Node pred, DataFlow::Node succ, string prop) {
55-
exists(DataFlow::MethodCallNode call |
56-
call.getMethodName() = ["substr", "substring", "slice"] and
57-
not call.getArgument(0).getIntValue() = 0 and
58-
pred = call.getReceiver() and
59-
succ = call and
60-
prop = urlSuffixPseudoProperty()
61-
)
62-
or
63-
exists(DataFlow::MethodCallNode call |
64-
call.getMethodName() = "exec" and pred = call.getArgument(0)
65-
or
66-
call.getMethodName() = "match" and pred = call.getReceiver()
67-
|
68-
succ = call and
69-
prop = urlSuffixPseudoProperty()
70-
)
81+
override predicate isSanitizer(DataFlow::Node node) {
82+
super.isSanitizer(node)
7183
or
72-
exists(StringSplitCall split |
73-
split.getSeparator() = ["#", "?"] and
74-
pred = split.getBaseString() and
75-
succ = split.getASubstringRead(1) and
76-
prop = urlSuffixPseudoProperty()
77-
)
84+
node instanceof Sanitizer
85+
}
86+
87+
override predicate isSanitizerGuard(TaintTracking::SanitizerGuardNode guard) {
88+
guard instanceof SanitizerGuard
7889
}
7990

8091
override predicate isSanitizerEdge(DataFlow::Node pred, DataFlow::Node succ) {
8192
DomBasedXss::isOptionallySanitizedEdge(pred, succ)
93+
or
94+
// Avoid stepping from location -> location.hash, as the .hash is already treated as a source
95+
// (with a different flow label)
96+
exists(DataFlow::PropRead read |
97+
read = DOM::locationRef().getAPropertyRead(["hash", "search"]) and
98+
pred = read.getBase() and
99+
succ = read
100+
)
82101
}
83102
}
84-
85-
private string urlSuffixPseudoProperty() { result = "$UrlSuffix$" }
86103
}

javascript/ql/src/semmle/javascript/security/dataflow/UnsafeJQueryPluginCustomizations.qll

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -34,18 +34,11 @@ module UnsafeJQueryPlugin {
3434
/**
3535
* An argument that may act as a HTML fragment rather than a CSS selector, as a sink for remote unsafe jQuery plugins.
3636
*/
37-
class AmbiguousHtmlOrSelectorArgument extends DataFlow::Node {
37+
class AmbiguousHtmlOrSelectorArgument extends DataFlow::Node,
38+
DomBasedXss::JQueryHtmlOrSelectorArgument {
3839
AmbiguousHtmlOrSelectorArgument() {
39-
exists(JQuery::MethodCall call |
40-
call.interpretsArgumentAsSelector(this) and call.interpretsArgumentAsHtml(this)
41-
) and
42-
// the $-function in particular will not construct HTML for non-string values
43-
analyze().getAType() = TTString() and
4440
// any fixed prefix makes the call unambiguous
45-
not exists(DataFlow::Node prefix |
46-
DomBasedXss::isPrefixOfJQueryHtmlString(this, prefix) and
47-
prefix.mayHaveStringValue(_)
48-
)
41+
not exists(getAPrefix())
4942
}
5043
}
5144

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

Lines changed: 47 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
*/
44

55
import javascript
6+
private import semmle.javascript.dataflow.InferredTypes
67

78
/** Provides classes and predicates shared between the XSS queries. */
89
module Shared {
@@ -153,19 +154,9 @@ module DomBasedXss {
153154
class LibrarySink extends Sink, DataFlow::ValueNode {
154155
LibrarySink() {
155156
// call to a jQuery method that interprets its argument as HTML
156-
exists(JQuery::MethodCall call | call.interpretsArgumentAsHtml(this) |
157-
// either the argument is always interpreted as HTML
158-
not call.interpretsArgumentAsSelector(this)
159-
or
160-
// or it doesn't start with something other than `<`, and so at least
161-
// _may_ be interpreted as HTML
162-
not exists(DataFlow::Node prefix, string strval |
163-
isPrefixOfJQueryHtmlString(this, prefix) and
164-
strval = prefix.getStringValue() and
165-
not strval = "" and
166-
not strval.regexpMatch("\\s*<.*")
167-
) and
168-
not DOM::locationRef().flowsTo(this)
157+
exists(JQuery::MethodCall call |
158+
call.interpretsArgumentAsHtml(this) and
159+
not call.interpretsArgumentAsSelector(this) // Handled by `JQuerySelectorSink`
169160
)
170161
or
171162
// call to an Angular method that interprets its argument as HTML
@@ -192,16 +183,54 @@ module DomBasedXss {
192183
* HTML by a jQuery method.
193184
*/
194185
predicate isPrefixOfJQueryHtmlString(DataFlow::Node htmlString, DataFlow::Node prefix) {
195-
any(JQuery::MethodCall call).interpretsArgumentAsHtml(htmlString) and
196-
prefix = htmlString
186+
prefix = getAPrefixOfJQuerySelectorString(htmlString)
187+
}
188+
189+
/**
190+
* Holds if `prefix` is a prefix of `htmlString`, which may be intepreted as
191+
* HTML by a jQuery method.
192+
*/
193+
private DataFlow::Node getAPrefixOfJQuerySelectorString(DataFlow::Node htmlString) {
194+
any(JQuery::MethodCall call).interpretsArgumentAsSelector(htmlString) and
195+
result = htmlString
197196
or
198-
exists(DataFlow::Node pred | isPrefixOfJQueryHtmlString(htmlString, pred) |
199-
prefix = StringConcatenation::getFirstOperand(pred)
197+
exists(DataFlow::Node pred | pred = getAPrefixOfJQuerySelectorString(htmlString) |
198+
result = StringConcatenation::getFirstOperand(pred)
200199
or
201-
prefix = pred.getAPredecessor()
200+
result = pred.getAPredecessor()
202201
)
203202
}
204203

204+
/**
205+
* An argument to the jQuery `$` function or similar, which is interpreted as either a selector
206+
* or as an HTML string depending on its first character.
207+
*/
208+
class JQueryHtmlOrSelectorArgument extends DataFlow::Node {
209+
JQueryHtmlOrSelectorArgument() {
210+
exists(JQuery::MethodCall call |
211+
call.interpretsArgumentAsHtml(this) and
212+
call.interpretsArgumentAsSelector(this) and
213+
analyze().getAType() = TTString()
214+
)
215+
}
216+
217+
/** Gets a string that flows to the prefix of this argument. */
218+
string getAPrefix() { result = getAPrefixOfJQuerySelectorString(this).getStringValue() }
219+
}
220+
221+
/**
222+
* An argument to the jQuery `$` function or similar, which may be interpreted as HTML.
223+
*
224+
* This is the same as `JQueryHtmlOrSelectorArgument`, excluding cases where the value
225+
* is prefixed by something other than `<`.
226+
*/
227+
class JQueryHtmlOrSelectorSink extends Sink, JQueryHtmlOrSelectorArgument {
228+
JQueryHtmlOrSelectorSink() {
229+
// If a prefix of the string is known, it must start with '<' or be an empty string
230+
forall(string strval | strval = getAPrefix() | strval.regexpMatch("(?s)\\s*<.*|"))
231+
}
232+
}
233+
205234
/**
206235
* An expression whose value is interpreted as HTML or CSS
207236
* and may be inserted into the DOM.
@@ -350,11 +379,6 @@ module DomBasedXss {
350379
exists(PropAccess pacc | pacc = this.asExpr() |
351380
isSafeLocationProperty(pacc)
352381
or
353-
// `$(location.hash)` is a fairly common and safe idiom
354-
// (because `location.hash` always starts with `#`),
355-
// so we mark `hash` as safe for the purposes of this query
356-
pacc.getPropertyName() = "hash"
357-
or
358382
pacc.getPropertyName() = "length"
359383
)
360384
}

0 commit comments

Comments
 (0)