Skip to content

Commit 8ae04e0

Browse files
authored
Merge pull request #8509 from erik-krogh/fpXss
JS: filter away reads of .src that end in a URL sink for js/xss-through-dom
2 parents 5cdf0b5 + 099d91b commit 8ae04e0

File tree

4 files changed

+85
-27
lines changed

4 files changed

+85
-27
lines changed

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

Lines changed: 54 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -32,34 +32,56 @@ module XssThroughDom {
3232
*/
3333
string unsafeDomPropertyName() { result = ["innerText", "textContent", "value", "name", "src"] }
3434

35+
/** A read of a DOM property seen as a source for cross-site scripting vulnerabilities through the DOM. */
36+
abstract class DomPropertySource extends Source {
37+
/**
38+
* Gets the name of the DOM property that the source originated from.
39+
*/
40+
abstract string getPropertyName();
41+
}
42+
43+
/* Gets a jQuery method where the receiver looks like `$("<p>" + ... )`, which is benign for this query. */
44+
private JQuery::MethodCall benignJQueryMethod() {
45+
exists(DataFlow::Node prefix |
46+
DomBasedXss::isPrefixOfJQueryHtmlString(result
47+
.getReceiver()
48+
.(DataFlow::CallNode)
49+
.getAnArgument(), prefix)
50+
|
51+
prefix.getStringValue().regexpMatch("\\s*<.*")
52+
)
53+
}
54+
55+
/** A source for text from the DOM from a JQuery method call. */
56+
class JQueryTextSource extends Source instanceof JQuery::MethodCall {
57+
JQueryTextSource() {
58+
this.getMethodName() = ["text", "val"] and
59+
this.getNumArgument() = 0 and
60+
not this = benignJQueryMethod()
61+
}
62+
}
63+
3564
/**
36-
* A source for text from the DOM from a JQuery method call.
65+
* A source for text from a DOM property read by jQuery.
3766
*/
38-
class JQueryTextSource extends Source, JQuery::MethodCall {
39-
JQueryTextSource() {
40-
(
41-
this.getMethodName() = ["text", "val"] and this.getNumArgument() = 0
67+
class JQueryDOMPropertySource extends DomPropertySource instanceof JQuery::MethodCall {
68+
string prop;
69+
70+
JQueryDOMPropertySource() {
71+
exists(string methodName |
72+
this.getMethodName() = methodName and
73+
this.getNumArgument() = 1 and
74+
forex(InferredType t | t = this.getArgument(0).analyze().getAType() | t = TTString()) and
75+
this.getArgument(0).mayHaveStringValue(prop)
76+
|
77+
methodName = "attr" and prop = unsafeAttributeName()
4278
or
43-
exists(string methodName, string value |
44-
this.getMethodName() = methodName and
45-
this.getNumArgument() = 1 and
46-
forex(InferredType t | t = this.getArgument(0).analyze().getAType() | t = TTString()) and
47-
this.getArgument(0).mayHaveStringValue(value)
48-
|
49-
methodName = "attr" and value = unsafeAttributeName()
50-
or
51-
methodName = "prop" and value = unsafeDomPropertyName()
52-
)
79+
methodName = "prop" and prop = unsafeDomPropertyName()
5380
) and
54-
// looks like a $("<p>" + ... ) source, which is benign for this query.
55-
not exists(DataFlow::Node prefix |
56-
DomBasedXss::isPrefixOfJQueryHtmlString(this.getReceiver()
57-
.(DataFlow::CallNode)
58-
.getAnArgument(), prefix)
59-
|
60-
prefix.getStringValue().regexpMatch("\\s*<.*")
61-
)
81+
not this = benignJQueryMethod()
6282
}
83+
84+
override string getPropertyName() { result = prop }
6385
}
6486

6587
/**
@@ -88,19 +110,25 @@ module XssThroughDom {
88110
/**
89111
* A source for text from the DOM from a DOM property read or call to `getAttribute()`.
90112
*/
91-
class DomTextSource extends Source {
113+
class DomTextSource extends DomPropertySource {
114+
string prop;
115+
92116
DomTextSource() {
93117
exists(DataFlow::PropRead read | read = this |
94118
read.getBase().getALocalSource() = DOM::domValueRef() and
95-
read.mayHavePropertyName(unsafeDomPropertyName())
119+
prop = unsafeDomPropertyName() and
120+
read.mayHavePropertyName(prop)
96121
)
97122
or
98123
exists(DataFlow::MethodCallNode mcn | mcn = this |
99124
mcn.getReceiver().getALocalSource() = DOM::domValueRef() and
100125
mcn.getMethodName() = "getAttribute" and
101-
mcn.getArgument(0).mayHaveStringValue(unsafeAttributeName())
126+
prop = unsafeAttributeName() and
127+
mcn.getArgument(0).mayHaveStringValue(prop)
102128
)
103129
}
130+
131+
override string getPropertyName() { result = prop }
104132
}
105133

106134
/** DEPRECATED: Alias for DomTextSource */

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,4 +35,13 @@ class Configuration extends TaintTracking::Configuration {
3535
override predicate isSanitizerEdge(DataFlow::Node pred, DataFlow::Node succ) {
3636
DomBasedXss::isOptionallySanitizedEdge(pred, succ)
3737
}
38+
39+
override predicate hasFlowPath(DataFlow::SourcePathNode src, DataFlow::SinkPathNode sink) {
40+
super.hasFlowPath(src, sink) and
41+
// filtering away readings of `src` that end in a URL sink.
42+
not (
43+
sink.getNode() instanceof DomBasedXss::WriteURLSink and
44+
src.getNode().(DomPropertySource).getPropertyName() = "src"
45+
)
46+
}
3847
}

javascript/ql/test/query-tests/Security/CWE-079/XssThroughDom/XssThroughDom.expected

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,13 @@ nodes
122122
| xss-through-dom.js:109:31:109:70 | "<a src ... oo</a>" |
123123
| xss-through-dom.js:109:45:109:55 | this.el.src |
124124
| xss-through-dom.js:109:45:109:55 | this.el.src |
125+
| xss-through-dom.js:114:11:114:52 | src |
126+
| xss-through-dom.js:114:17:114:52 | documen ... k").src |
127+
| xss-through-dom.js:114:17:114:52 | documen ... k").src |
128+
| xss-through-dom.js:115:16:115:18 | src |
129+
| xss-through-dom.js:115:16:115:18 | src |
130+
| xss-through-dom.js:117:26:117:28 | src |
131+
| xss-through-dom.js:117:26:117:28 | src |
125132
edges
126133
| forms.js:8:23:8:28 | values | forms.js:9:31:9:36 | values |
127134
| forms.js:8:23:8:28 | values | forms.js:9:31:9:36 | values |
@@ -194,6 +201,12 @@ edges
194201
| xss-through-dom.js:109:45:109:55 | this.el.src | xss-through-dom.js:109:31:109:70 | "<a src ... oo</a>" |
195202
| xss-through-dom.js:109:45:109:55 | this.el.src | xss-through-dom.js:109:31:109:70 | "<a src ... oo</a>" |
196203
| xss-through-dom.js:109:45:109:55 | this.el.src | xss-through-dom.js:109:31:109:70 | "<a src ... oo</a>" |
204+
| xss-through-dom.js:114:11:114:52 | src | xss-through-dom.js:115:16:115:18 | src |
205+
| xss-through-dom.js:114:11:114:52 | src | xss-through-dom.js:115:16:115:18 | src |
206+
| xss-through-dom.js:114:11:114:52 | src | xss-through-dom.js:117:26:117:28 | src |
207+
| xss-through-dom.js:114:11:114:52 | src | xss-through-dom.js:117:26:117:28 | src |
208+
| xss-through-dom.js:114:17:114:52 | documen ... k").src | xss-through-dom.js:114:11:114:52 | src |
209+
| xss-through-dom.js:114:17:114:52 | documen ... k").src | xss-through-dom.js:114:11:114:52 | src |
197210
#select
198211
| forms.js:9:31:9:40 | values.foo | forms.js:8:23:8:28 | values | forms.js:9:31:9:40 | values.foo | $@ is reinterpreted as HTML without escaping meta-characters. | forms.js:8:23:8:28 | values | DOM text |
199212
| forms.js:12:31:12:40 | values.bar | forms.js:11:24:11:29 | values | forms.js:12:31:12:40 | values.bar | $@ is reinterpreted as HTML without escaping meta-characters. | forms.js:11:24:11:29 | values | DOM text |
@@ -228,3 +241,4 @@ edges
228241
| xss-through-dom.js:93:16:93:46 | $("#foo ... ].value | xss-through-dom.js:93:16:93:46 | $("#foo ... ].value | xss-through-dom.js:93:16:93:46 | $("#foo ... ].value | $@ is reinterpreted as HTML without escaping meta-characters. | xss-through-dom.js:93:16:93:46 | $("#foo ... ].value | DOM text |
229242
| xss-through-dom.js:96:17:96:47 | $("#foo ... ].value | xss-through-dom.js:96:17:96:47 | $("#foo ... ].value | xss-through-dom.js:96:17:96:47 | $("#foo ... ].value | $@ is reinterpreted as HTML without escaping meta-characters. | xss-through-dom.js:96:17:96:47 | $("#foo ... ].value | DOM text |
230243
| xss-through-dom.js:109:31:109:70 | "<a src ... oo</a>" | xss-through-dom.js:109:45:109:55 | this.el.src | xss-through-dom.js:109:31:109:70 | "<a src ... oo</a>" | $@ is reinterpreted as HTML without escaping meta-characters. | xss-through-dom.js:109:45:109:55 | this.el.src | DOM text |
244+
| xss-through-dom.js:115:16:115:18 | src | xss-through-dom.js:114:17:114:52 | documen ... k").src | xss-through-dom.js:115:16:115:18 | src | $@ is reinterpreted as HTML without escaping meta-characters. | xss-through-dom.js:114:17:114:52 | documen ... k").src | DOM text |

javascript/ql/test/query-tests/Security/CWE-079/XssThroughDom/xss-through-dom.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,4 +108,11 @@ class Sub extends Super {
108108
super();
109109
$("#id").get(0).innerHTML = "<a src=\"" + this.el.src + "\">foo</a>"; // NOT OK. Attack: `<mytag id="id" src="x:&quot;&gt;&lt;img src=1 onerror=&quot;alert(1)&quot;&gt;" />`
110110
}
111-
}
111+
}
112+
113+
(function () {
114+
const src = document.getElementById("#link").src;
115+
$("#id").html(src); // NOT OK.
116+
117+
$("#id").attr("src", src); // OK
118+
})();

0 commit comments

Comments
 (0)