-
Notifications
You must be signed in to change notification settings - Fork 1.8k
JS: Add XSS-through-dom query #3191
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
dd9aec0
55edfed
14b551f
1b80f46
2d3e42e
12f4ce8
73b0aa4
9fc29ee
59b94b3
947e982
76503d3
8811455
7bfea94
a5bbfa3
0a29d13
ac26741
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
<!DOCTYPE qhelp PUBLIC | ||
"-//Semmle//qhelp//EN" | ||
"qhelp.dtd"> | ||
<qhelp> | ||
|
||
<overview> | ||
<p> | ||
Extracting text from a DOM node and interpreting it as HTML can lead to a cross-site scripting vulnerability. | ||
</p> | ||
<p> | ||
A webpage with this vulnerability reads text from the DOM, and afterwards adds the text as HTML to the DOM. | ||
Using text from the DOM as HTML effectively unescapes the text, and thereby invalidates any escaping done on the text. | ||
If an attacker is able to control the safe sanitized text, then this vulnerability can be exploited to perform a cross-site scripting attack. | ||
</p> | ||
</overview> | ||
|
||
<recommendation> | ||
<p> | ||
To guard against cross-site scripting, consider using contextual output encoding/escaping before | ||
writing text to the page, or one of the other solutions that are mentioned in the References section below. | ||
</p> | ||
</recommendation> | ||
|
||
<example> | ||
<p> | ||
The following example shows a webpage using a <code>data-target</code> attribute | ||
to select and manipulate a DOM element using the JQuery library. In the example, the | ||
<code>data-target</code> attribute is read into the <code>target</code> variable, and the | ||
<code>$</code> function is then supposed to use the <code>target</code> variable as a CSS | ||
selector to determine which element should be manipulated. | ||
</p> | ||
<sample src="examples/XssThroughDom.js" /> | ||
<p> | ||
However, if an attacker can control the <code>data-target</code> attribute, | ||
then the value of <code>target</code> can be used to cause the <code>$</code> function | ||
to execute arbitary JavaScript. | ||
</p> | ||
<p> | ||
The above vulnerability can be fixed by using <code>$.find</code> instead of <code>$</code>. | ||
The <code>$.find</code> function will only interpret <code>target</code> as a CSS selector | ||
and never as HTML, thereby preventing an XSS attack. | ||
</p> | ||
<sample src="examples/XssThroughDomFixed.js" /> | ||
</example> | ||
|
||
<references> | ||
<li> | ||
OWASP: | ||
<a href="https://cheatsheetseries.owasp.org/cheatsheets/DOM_based_XSS_Prevention_Cheat_Sheet.html">DOM based | ||
XSS Prevention Cheat Sheet</a>. | ||
</li> | ||
<li> | ||
OWASP: | ||
<a href="https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html">XSS | ||
(Cross Site Scripting) Prevention Cheat Sheet</a>. | ||
</li> | ||
<li> | ||
OWASP | ||
<a href="https://owasp.org/www-community/attacks/DOM_Based_XSS">DOM Based XSS</a>. | ||
</li> | ||
<li> | ||
OWASP | ||
<a href="https://owasp.org/www-community/Types_of_Cross-Site_Scripting">Types of Cross-Site | ||
Scripting</a>. | ||
</li> | ||
<li> | ||
Wikipedia: <a href="http://en.wikipedia.org/wiki/Cross-site_scripting">Cross-site scripting</a>. | ||
</li> | ||
</references> | ||
</qhelp> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
/** | ||
* @name Cross-site scripting through DOM | ||
* @description Writing user-controlled DOM to HTML can allow for | ||
* a cross-site scripting vulnerability. | ||
* @kind path-problem | ||
* @problem.severity error | ||
* @precision medium | ||
* @id js/xss-through-dom | ||
* @tags security | ||
* external/cwe/cwe-079 | ||
* external/cwe/cwe-116 | ||
*/ | ||
|
||
import javascript | ||
import semmle.javascript.security.dataflow.XssThroughDom::XssThroughDom | ||
import DataFlow::PathGraph | ||
|
||
from Configuration cfg, DataFlow::PathNode source, DataFlow::PathNode sink | ||
where cfg.hasFlowPath(source, sink) | ||
select sink.getNode(), source, sink, "Cross-site scripting vulnerability due to $@.", | ||
source.getNode(), "DOM text" |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
$("button").click(function () { | ||
var target = this.attr("data-target"); | ||
$(target).hide(); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
$("button").click(function () { | ||
var target = this.attr("data-target"); | ||
$.find(target).hide(); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,126 @@ | ||
/** | ||
* Provides a taint-tracking configuration for reasoning about | ||
* cross-site scripting vulnerabilities through the DOM. | ||
*/ | ||
|
||
import javascript | ||
|
||
/** | ||
* Classes and predicates for the XSS through DOM query. | ||
*/ | ||
module XssThroughDom { | ||
import Xss::XssThroughDom | ||
private import semmle.javascript.security.dataflow.Xss::DomBasedXss as DomBasedXss | ||
private import semmle.javascript.dataflow.InferredTypes | ||
private import semmle.javascript.security.dataflow.UnsafeJQueryPluginCustomizations::UnsafeJQueryPlugin as UnsafeJQuery | ||
|
||
/** | ||
* A taint-tracking configuration for reasoning about XSS through the DOM. | ||
*/ | ||
class Configuration extends TaintTracking::Configuration { | ||
Configuration() { this = "XssThroughDOM" } | ||
|
||
override predicate isSource(DataFlow::Node source) { source instanceof Source } | ||
|
||
override predicate isSink(DataFlow::Node sink) { sink instanceof DomBasedXss::Sink } | ||
|
||
override predicate isSanitizer(DataFlow::Node node) { | ||
super.isSanitizer(node) or | ||
node instanceof DomBasedXss::Sanitizer | ||
} | ||
|
||
override predicate isSanitizerGuard(TaintTracking::SanitizerGuardNode guard) { | ||
guard instanceof TypeTestGuard or | ||
guard instanceof UnsafeJQuery::PropertyPresenceSanitizer | ||
} | ||
} | ||
|
||
/** | ||
* Gets an attribute name that could store user-controlled data. | ||
* | ||
* Attributes such as "id", "href", and "src" are often used as input to HTML. | ||
* However, they are either rarely controlable by a user, or already a sink for other XSS vulnerabilities. | ||
* Such attributes are therefore ignored. | ||
*/ | ||
bindingset[result] | ||
string unsafeAttributeName() { | ||
result.regexpMatch("data-.*") or | ||
result.regexpMatch("aria-.*") or | ||
result = ["name", "value", "title", "alt"] | ||
} | ||
|
||
/** | ||
* A source for text from the DOM from a JQuery method call. | ||
*/ | ||
class JQueryTextSource extends Source, JQuery::MethodCall { | ||
JQueryTextSource() { | ||
( | ||
this.getMethodName() = ["text", "val"] and this.getNumArgument() = 0 | ||
or | ||
this.getMethodName() = "attr" and | ||
this.getNumArgument() = 1 and | ||
forex(InferredType t | t = this.getArgument(0).analyze().getAType() | t = TTString()) and | ||
this.getArgument(0).mayHaveStringValue(unsafeAttributeName()) | ||
) and | ||
// looks like a $("<p>" + ... ) source, which is benign for this query. | ||
not exists(DataFlow::Node prefix | | ||
DomBasedXss::isPrefixOfJQueryHtmlString(this | ||
.getReceiver() | ||
.(DataFlow::CallNode) | ||
.getAnArgument(), prefix) | ||
| | ||
prefix.getStringValue().regexpMatch("\\s*<.*") | ||
) | ||
} | ||
} | ||
|
||
/** | ||
* A source for text from the DOM from a DOM property read or call to `getAttribute()`. | ||
*/ | ||
class DOMTextSource extends Source { | ||
DOMTextSource() { | ||
exists(DataFlow::PropRead read | read = this | | ||
read.getBase().getALocalSource() = DOM::domValueRef() and | ||
exists(string propName | propName = ["innerText", "textContent", "value", "name"] | | ||
read.getPropertyName() = propName or | ||
read.getPropertyNameExpr().flow().mayHaveStringValue(propName) | ||
Comment on lines
+85
to
+86
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I created https://github.com/github/codeql-javascript-team/issues/98 for this pattern just now. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Lets do that in a later PR. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Absolutely. |
||
) | ||
) | ||
or | ||
exists(DataFlow::MethodCallNode mcn | mcn = this | | ||
mcn.getReceiver().getALocalSource() = DOM::domValueRef() and | ||
mcn.getMethodName() = "getAttribute" and | ||
mcn.getArgument(0).mayHaveStringValue(unsafeAttributeName()) | ||
) | ||
} | ||
} | ||
|
||
/** | ||
* A test of form `typeof x === "something"`, preventing `x` from being a string in some cases. | ||
* | ||
* This sanitizer helps prune infeasible paths in type-overloaded functions. | ||
*/ | ||
class TypeTestGuard extends TaintTracking::SanitizerGuardNode, DataFlow::ValueNode { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Have you considered the sanitizers for js/unsafe-jquery-plugin?https://github.com/Semmle/ql/blob/1b88c9768827c37355eea473cba4e24796be310a/javascript/ql/src/semmle/javascript/security/dataflow/UnsafeJQueryPluginCustomizations.qll#L187-L190 They are also used to eliminate some non-string values. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That sanitizer is a generalization of one of my sanitizers, so that is perfect 👍 |
||
override EqualityTest astNode; | ||
TypeofExpr typeof; | ||
boolean polarity; | ||
|
||
TypeTestGuard() { | ||
astNode.getAnOperand() = typeof and | ||
( | ||
// typeof x === "string" sanitizes `x` when it evaluates to false | ||
astNode.getAnOperand().getStringValue() = "string" and | ||
polarity = astNode.getPolarity().booleanNot() | ||
or | ||
// typeof x === "object" sanitizes `x` when it evaluates to true | ||
astNode.getAnOperand().getStringValue() != "string" and | ||
polarity = astNode.getPolarity() | ||
) | ||
} | ||
|
||
override predicate sanitizes(boolean outcome, Expr e) { | ||
polarity = outcome and | ||
e = typeof.getOperand() | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
nodes | ||
| xss-through-dom.js:2:16:2:34 | $("textarea").val() | | ||
| xss-through-dom.js:2:16:2:34 | $("textarea").val() | | ||
| xss-through-dom.js:2:16:2:34 | $("textarea").val() | | ||
| xss-through-dom.js:4:16:4:40 | $(".som ... .text() | | ||
| xss-through-dom.js:4:16:4:40 | $(".som ... .text() | | ||
| xss-through-dom.js:4:16:4:40 | $(".som ... .text() | | ||
| xss-through-dom.js:8:16:8:53 | $(".som ... arget") | | ||
| xss-through-dom.js:8:16:8:53 | $(".som ... arget") | | ||
| xss-through-dom.js:8:16:8:53 | $(".som ... arget") | | ||
| xss-through-dom.js:11:3:11:42 | documen ... nerText | | ||
| xss-through-dom.js:11:3:11:42 | documen ... nerText | | ||
| xss-through-dom.js:11:3:11:42 | documen ... nerText | | ||
| xss-through-dom.js:19:3:19:44 | documen ... Content | | ||
| xss-through-dom.js:19:3:19:44 | documen ... Content | | ||
| xss-through-dom.js:19:3:19:44 | documen ... Content | | ||
| xss-through-dom.js:23:3:23:48 | documen ... ].value | | ||
| xss-through-dom.js:23:3:23:48 | documen ... ].value | | ||
| xss-through-dom.js:23:3:23:48 | documen ... ].value | | ||
| xss-through-dom.js:27:3:27:61 | documen ... arget') | | ||
| xss-through-dom.js:27:3:27:61 | documen ... arget') | | ||
| xss-through-dom.js:27:3:27:61 | documen ... arget') | | ||
| xss-through-dom.js:51:30:51:48 | $("textarea").val() | | ||
| xss-through-dom.js:51:30:51:48 | $("textarea").val() | | ||
| xss-through-dom.js:51:30:51:48 | $("textarea").val() | | ||
| xss-through-dom.js:54:31:54:49 | $("textarea").val() | | ||
| xss-through-dom.js:54:31:54:49 | $("textarea").val() | | ||
| xss-through-dom.js:54:31:54:49 | $("textarea").val() | | ||
| xss-through-dom.js:56:30:56:51 | $("inpu ... 0).name | | ||
| xss-through-dom.js:56:30:56:51 | $("inpu ... 0).name | | ||
| xss-through-dom.js:56:30:56:51 | $("inpu ... 0).name | | ||
| xss-through-dom.js:57:30:57:67 | $("inpu ... "name") | | ||
| xss-through-dom.js:57:30:57:67 | $("inpu ... "name") | | ||
| xss-through-dom.js:57:30:57:67 | $("inpu ... "name") | | ||
| xss-through-dom.js:61:30:61:69 | $(docum ... value") | | ||
| xss-through-dom.js:61:30:61:69 | $(docum ... value") | | ||
| xss-through-dom.js:61:30:61:69 | $(docum ... value") | | ||
| xss-through-dom.js:64:30:64:40 | valMethod() | | ||
| xss-through-dom.js:64:30:64:40 | valMethod() | | ||
| xss-through-dom.js:64:30:64:40 | valMethod() | | ||
edges | ||
| xss-through-dom.js:2:16:2:34 | $("textarea").val() | xss-through-dom.js:2:16:2:34 | $("textarea").val() | | ||
| xss-through-dom.js:4:16:4:40 | $(".som ... .text() | xss-through-dom.js:4:16:4:40 | $(".som ... .text() | | ||
| xss-through-dom.js:8:16:8:53 | $(".som ... arget") | xss-through-dom.js:8:16:8:53 | $(".som ... arget") | | ||
| xss-through-dom.js:11:3:11:42 | documen ... nerText | xss-through-dom.js:11:3:11:42 | documen ... nerText | | ||
| xss-through-dom.js:19:3:19:44 | documen ... Content | xss-through-dom.js:19:3:19:44 | documen ... Content | | ||
| xss-through-dom.js:23:3:23:48 | documen ... ].value | xss-through-dom.js:23:3:23:48 | documen ... ].value | | ||
| xss-through-dom.js:27:3:27:61 | documen ... arget') | xss-through-dom.js:27:3:27:61 | documen ... arget') | | ||
| xss-through-dom.js:51:30:51:48 | $("textarea").val() | xss-through-dom.js:51:30:51:48 | $("textarea").val() | | ||
| xss-through-dom.js:54:31:54:49 | $("textarea").val() | xss-through-dom.js:54:31:54:49 | $("textarea").val() | | ||
| xss-through-dom.js:56:30:56:51 | $("inpu ... 0).name | xss-through-dom.js:56:30:56:51 | $("inpu ... 0).name | | ||
| xss-through-dom.js:57:30:57:67 | $("inpu ... "name") | xss-through-dom.js:57:30:57:67 | $("inpu ... "name") | | ||
| xss-through-dom.js:61:30:61:69 | $(docum ... value") | xss-through-dom.js:61:30:61:69 | $(docum ... value") | | ||
| xss-through-dom.js:64:30:64:40 | valMethod() | xss-through-dom.js:64:30:64:40 | valMethod() | | ||
#select | ||
| xss-through-dom.js:2:16:2:34 | $("textarea").val() | xss-through-dom.js:2:16:2:34 | $("textarea").val() | xss-through-dom.js:2:16:2:34 | $("textarea").val() | Cross-site scripting vulnerability due to $@. | xss-through-dom.js:2:16:2:34 | $("textarea").val() | DOM text | | ||
| xss-through-dom.js:4:16:4:40 | $(".som ... .text() | xss-through-dom.js:4:16:4:40 | $(".som ... .text() | xss-through-dom.js:4:16:4:40 | $(".som ... .text() | Cross-site scripting vulnerability due to $@. | xss-through-dom.js:4:16:4:40 | $(".som ... .text() | DOM text | | ||
| xss-through-dom.js:8:16:8:53 | $(".som ... arget") | xss-through-dom.js:8:16:8:53 | $(".som ... arget") | xss-through-dom.js:8:16:8:53 | $(".som ... arget") | Cross-site scripting vulnerability due to $@. | xss-through-dom.js:8:16:8:53 | $(".som ... arget") | DOM text | | ||
| xss-through-dom.js:11:3:11:42 | documen ... nerText | xss-through-dom.js:11:3:11:42 | documen ... nerText | xss-through-dom.js:11:3:11:42 | documen ... nerText | Cross-site scripting vulnerability due to $@. | xss-through-dom.js:11:3:11:42 | documen ... nerText | DOM text | | ||
| xss-through-dom.js:19:3:19:44 | documen ... Content | xss-through-dom.js:19:3:19:44 | documen ... Content | xss-through-dom.js:19:3:19:44 | documen ... Content | Cross-site scripting vulnerability due to $@. | xss-through-dom.js:19:3:19:44 | documen ... Content | DOM text | | ||
| xss-through-dom.js:23:3:23:48 | documen ... ].value | xss-through-dom.js:23:3:23:48 | documen ... ].value | xss-through-dom.js:23:3:23:48 | documen ... ].value | Cross-site scripting vulnerability due to $@. | xss-through-dom.js:23:3:23:48 | documen ... ].value | DOM text | | ||
| xss-through-dom.js:27:3:27:61 | documen ... arget') | xss-through-dom.js:27:3:27:61 | documen ... arget') | xss-through-dom.js:27:3:27:61 | documen ... arget') | Cross-site scripting vulnerability due to $@. | xss-through-dom.js:27:3:27:61 | documen ... arget') | DOM text | | ||
| xss-through-dom.js:51:30:51:48 | $("textarea").val() | xss-through-dom.js:51:30:51:48 | $("textarea").val() | xss-through-dom.js:51:30:51:48 | $("textarea").val() | Cross-site scripting vulnerability due to $@. | xss-through-dom.js:51:30:51:48 | $("textarea").val() | DOM text | | ||
| xss-through-dom.js:54:31:54:49 | $("textarea").val() | xss-through-dom.js:54:31:54:49 | $("textarea").val() | xss-through-dom.js:54:31:54:49 | $("textarea").val() | Cross-site scripting vulnerability due to $@. | xss-through-dom.js:54:31:54:49 | $("textarea").val() | DOM text | | ||
| xss-through-dom.js:56:30:56:51 | $("inpu ... 0).name | xss-through-dom.js:56:30:56:51 | $("inpu ... 0).name | xss-through-dom.js:56:30:56:51 | $("inpu ... 0).name | Cross-site scripting vulnerability due to $@. | xss-through-dom.js:56:30:56:51 | $("inpu ... 0).name | DOM text | | ||
| xss-through-dom.js:57:30:57:67 | $("inpu ... "name") | xss-through-dom.js:57:30:57:67 | $("inpu ... "name") | xss-through-dom.js:57:30:57:67 | $("inpu ... "name") | Cross-site scripting vulnerability due to $@. | xss-through-dom.js:57:30:57:67 | $("inpu ... "name") | DOM text | | ||
| xss-through-dom.js:61:30:61:69 | $(docum ... value") | xss-through-dom.js:61:30:61:69 | $(docum ... value") | xss-through-dom.js:61:30:61:69 | $(docum ... value") | Cross-site scripting vulnerability due to $@. | xss-through-dom.js:61:30:61:69 | $(docum ... value") | DOM text | | ||
| xss-through-dom.js:64:30:64:40 | valMethod() | xss-through-dom.js:64:30:64:40 | valMethod() | xss-through-dom.js:64:30:64:40 | valMethod() | Cross-site scripting vulnerability due to $@. | xss-through-dom.js:64:30:64:40 | valMethod() | DOM text | |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
Security/CWE-079/XssThroughDom.ql |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we merge this with the above case? It seems to almost subsume it with the only caveat about this only recognizing method calls.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
With the current version we loose support for non-method calls, which I think is OK.
I can quickly add it back by adding a
.getLocalSource()
.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hm, we'd lose quite a few calls. They don't look very interesting I'll give you that, but I think we should keep them.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I'm ok with loosing non-method calls, but I'm not comfortable with loosing the reflective calls.
I'll revert it back to having 2 cases.