Skip to content

Commit 4efea43

Browse files
committed
JS: Use TaintedUrlSuffix flow label in jQuery xss
1 parent 15db6df commit 4efea43

File tree

3 files changed

+122
-23
lines changed

3 files changed

+122
-23
lines changed

javascript/ql/src/semmle/javascript/StringConcatenation.qll

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ module StringConcatenation {
3434
or
3535
exists(DataFlow::ArrayCreationNode array, DataFlow::MethodCallNode call |
3636
call = array.getAMethodCall("join") and
37-
call.getArgument(0).mayHaveStringValue("") and
37+
(call.getArgument(0).mayHaveStringValue("") or call.getNumArgument() = 0) and
3838
(
3939
// step from array element to array
4040
result = array.getElement(n) and
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
/**
2+
* Provides a flow label for reasoning about URLs with a tainted query and fragment part,
3+
* which we collectively refer to as the "suffix" of the URL.
4+
*/
5+
import javascript
6+
7+
/**
8+
* Provides a flow label for reasoning about URLs with a tainted query and fragment part,
9+
* which we collectively refer to as the "suffix" of the URL.
10+
*/
11+
module TaintedUrlSuffix {
12+
private import DataFlow
13+
14+
/**
15+
* The flow label representing a URL with a tainted query and fragment part.
16+
*
17+
* Can also be accessed using `TaintedUrlSuffix::label()`.
18+
*/
19+
class TaintedUrlSuffixLabel extends FlowLabel {
20+
TaintedUrlSuffixLabel() {
21+
this = "tainted-url-suffix"
22+
}
23+
}
24+
25+
/**
26+
* Gets the flow label representing a URL with a tainted query and fragment part.
27+
*/
28+
FlowLabel label() { result instanceof TaintedUrlSuffixLabel }
29+
30+
/** Holds for `pred -> succ` is a step of form `x -> x.p` */
31+
private predicate isSafeLocationProp(DataFlow::PropRead read) {
32+
// Ignore properties that refer to the scheme, domain, port, auth, or path.
33+
exists (string name | name = read.getPropertyName() |
34+
name = "protocol" or
35+
name = "scheme" or
36+
name = "host" or
37+
name = "hostname" or
38+
name = "domain" or
39+
name = "origin" or
40+
name = "port" or
41+
name = "path" or
42+
name = "pathname" or
43+
name = "username" or
44+
name = "password" or
45+
name = "auth"
46+
)
47+
}
48+
49+
/**
50+
* Holds if there is a flow step `src -> dst` involving the URL suffix taint label.
51+
*
52+
* This handles steps through string operations, promises, URL parsers, and URL accessors.
53+
*/
54+
predicate step(Node src, Node dst, FlowLabel srclbl, FlowLabel dstlbl) {
55+
// Inherit all ordinary taint steps except `x -> x.p` steps
56+
srclbl = label() and
57+
dstlbl = label() and
58+
TaintTracking::sharedTaintStep(src, dst) and
59+
not isSafeLocationProp(dst)
60+
or
61+
// Transition from URL suffix to full taint when extracting the query/fragment part.
62+
srclbl = label() and
63+
dstlbl.isTaint() and
64+
(
65+
exists(MethodCallNode call, string name |
66+
src = call.getReceiver() and
67+
dst = call and
68+
name = call.getMethodName()
69+
|
70+
// Substring that is not a prefix
71+
name = ["substring", "substr", "slice"] and
72+
not call.getArgument(0).getIntValue() = 0
73+
or
74+
// Split around '#' or '?' and extract the suffix
75+
name = "split" and
76+
call.getArgument(0).getStringValue() = ["#", "?"] and
77+
not exists(call.getAPropertyRead("0")) // Avoid false flow to the prefix
78+
or
79+
// Replace '#' and '?' with nothing
80+
name = "replace" and
81+
call.getArgument(0).getStringValue() = ["#", "?"] and
82+
call.getArgument(1).getStringValue() = ""
83+
or
84+
// The `get` call in `url.searchParams.get(x)` and `url.hashParams.get(x)`
85+
// The step should be safe since nothing else reachable by this flow label supports a method named 'get'.
86+
name = "get"
87+
or
88+
// Methods on URL objects from the Closure library
89+
name = "getDecodedQuery" or
90+
name = "getFragment" or
91+
name = "getParameterValue" or
92+
name = "getParameterValues" or
93+
name = "getQueryData"
94+
)
95+
or
96+
exists(PropRead read |
97+
src = read.getBase() and
98+
dst = read and
99+
// Unlike the `search` property, the `query` property from `url.parse` does not include the `?`.
100+
read.getPropertyName() = "query"
101+
)
102+
or
103+
// Assume calls to regexp.exec always extract query/fragment parameters.
104+
exists(MethodCallNode call |
105+
call = any(RegExpLiteral re).flow().(DataFlow::SourceNode).getAMethodCall("exec") and
106+
src = call.getArgument(0) and
107+
dst = call
108+
)
109+
)
110+
}
111+
}

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

Lines changed: 10 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
*/
55

66
import javascript
7+
private import semmle.javascript.security.TaintedUrlSuffix
78

89
module DomBasedXss {
910
import DomBasedXssCustomizations::DomBasedXss
@@ -61,26 +62,14 @@ module DomBasedXss {
6162
not source = [DOM::locationRef(), DOM::locationRef().getAPropertyRead()] and
6263
label.isTaint()
6364
or
64-
source = DOM::locationSource() and
65-
label.isData() // Require transformation before reaching sink
66-
or
67-
source = DOM::locationRef().getAPropertyRead(["hash", "search"]) and
68-
label.isData() // Require transformation before reaching sink
65+
source = [DOM::locationSource(), DOM::locationRef().getAPropertyRead(["hash", "search"])] and
66+
label = TaintedUrlSuffix::label()
6967
}
7068

7169
override predicate isSink(DataFlow::Node sink, DataFlow::FlowLabel label) {
7270
sink instanceof JQueryHtmlOrSelectorSink and label.isTaint()
7371
}
7472

75-
override predicate isAdditionalFlowStep(
76-
DataFlow::Node pred, DataFlow::Node succ, DataFlow::FlowLabel predlbl,
77-
DataFlow::FlowLabel succlbl
78-
) {
79-
TaintTracking::sharedTaintStep(pred, succ) and
80-
predlbl.isData() and
81-
succlbl.isTaint()
82-
}
83-
8473
override predicate isSanitizer(DataFlow::Node node) {
8574
super.isSanitizer(node)
8675
or
@@ -91,15 +80,14 @@ module DomBasedXss {
9180
guard instanceof SanitizerGuard
9281
}
9382

94-
override predicate isSanitizerEdge(DataFlow::Node pred, DataFlow::Node succ) {
95-
DomBasedXss::isOptionallySanitizedEdge(pred, succ)
83+
override predicate isAdditionalFlowStep(DataFlow::Node src, DataFlow::Node trg, DataFlow::FlowLabel inlbl, DataFlow::FlowLabel outlbl) {
84+
TaintedUrlSuffix::step(src, trg, inlbl, outlbl)
9685
or
97-
// Avoid stepping from location -> location.hash, as the .hash is already treated as a source
98-
// (with a different flow label)
99-
exists(DataFlow::PropRead read |
100-
read = DOM::locationRef().getAPropertyRead(["hash", "search"]) and
101-
pred = read.getBase() and
102-
succ = read
86+
exists(DataFlow::Node operator |
87+
StringConcatenation::taintStep(src, trg, operator, _) and
88+
StringConcatenation::getOperand(operator, 0).getStringValue() = "<" + any(string s) and
89+
inlbl = TaintedUrlSuffix::label() and
90+
outlbl.isTaint()
10391
)
10492
}
10593
}

0 commit comments

Comments
 (0)