diff --git a/change-notes/1.25/analysis-javascript.md b/change-notes/1.25/analysis-javascript.md
index 14a95faab9d..14ca3b82d3c 100644
--- a/change-notes/1.25/analysis-javascript.md
+++ b/change-notes/1.25/analysis-javascript.md
@@ -7,7 +7,7 @@
| **Query** | **Tags** | **Purpose** |
|---------------------------------------------------------------------------------|-------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
-
+| Cross-site scripting through DOM (`js/xss-through-dom`) | security, external/cwe/cwe-079, external/cwe/cwe-116 | Highlights potential XSS vulnerabilities where existing text from the DOM is used as HTML. Results are not shown on LGTM by default. |
## Changes to existing queries
diff --git a/javascript/ql/src/Security/CWE-079/XssThroughDom.qhelp b/javascript/ql/src/Security/CWE-079/XssThroughDom.qhelp
new file mode 100644
index 00000000000..5aa2fe63253
--- /dev/null
+++ b/javascript/ql/src/Security/CWE-079/XssThroughDom.qhelp
@@ -0,0 +1,70 @@
+
+
+Extracting text from a DOM node and interpreting it as HTML can lead to a cross-site scripting vulnerability.
+
+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.
+
+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.
+
+The following example shows a webpage using a
+However, if an attacker can control the
+The above vulnerability can be fixed by using data-target attribute
+to select and manipulate a DOM element using the JQuery library. In the example, the
+data-target attribute is read into the target variable, and the
+$ function is then supposed to use the target variable as a CSS
+selector to determine which element should be manipulated.
+data-target attribute,
+then the value of target can be used to cause the $ function
+to execute arbitary JavaScript.
+$.find instead of $.
+The $.find function will only interpret target as a CSS selector
+and never as HTML, thereby preventing an XSS attack.
+
" + ... ) 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) + ) + ) + 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 { + 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() + } + } +} diff --git a/javascript/ql/test/query-tests/Security/CWE-079/XssThroughDom.expected b/javascript/ql/test/query-tests/Security/CWE-079/XssThroughDom.expected new file mode 100644 index 00000000000..7963d42b65e --- /dev/null +++ b/javascript/ql/test/query-tests/Security/CWE-079/XssThroughDom.expected @@ -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 | diff --git a/javascript/ql/test/query-tests/Security/CWE-079/XssThroughDom.qlref b/javascript/ql/test/query-tests/Security/CWE-079/XssThroughDom.qlref new file mode 100644 index 00000000000..3226decda37 --- /dev/null +++ b/javascript/ql/test/query-tests/Security/CWE-079/XssThroughDom.qlref @@ -0,0 +1 @@ +Security/CWE-079/XssThroughDom.ql diff --git a/javascript/ql/test/query-tests/Security/CWE-079/xss-through-dom.js b/javascript/ql/test/query-tests/Security/CWE-079/xss-through-dom.js new file mode 100644 index 00000000000..0f8a1ff7a09 --- /dev/null +++ b/javascript/ql/test/query-tests/Security/CWE-079/xss-through-dom.js @@ -0,0 +1,70 @@ +(function () { + $("#id").html($("textarea").val()); // NOT OK. + + $("#id").html($(".some-element").text()); // NOT OK. + + $("#id").html($(".some-element").attr("foo", "bar")); // OK. + $("#id").html($(".some-element").attr({"foo": "bar"})); // OK. + $("#id").html($(".some-element").attr("data-target")); // NOT OK. + + $("#id").html( + document.getElementById("foo").innerText // NOT OK. + ); + + $("#id").html( + document.getElementById("foo").innerHTML // OK - only repeats existing XSS. + ); + + $("#id").html( + document.getElementById("foo").textContent // NOT OK. + ); + + $("#id").html( + document.querySelectorAll("textarea")[0].value // NOT OK. + ); + + $("#id").html( + document.getElementById('div1').getAttribute('data-target') // NOT OK + ); + + function safe1(x) { // overloaded function. + if (x.jquery) { + var foo = $(x); // OK + } + + } + safe1($("textarea").val()); + + function safe2(x) { // overloaded function. + if (typeof x === "object") { + var foo = $(x); // OK + } + } + safe2($("textarea").val()); + + + $("#id").html( + $("
" + something() + "
").text() // OK - this is for a flow-step to catch, not this query. + ); + + + $("#id").get(0).innerHTML = $("textarea").val(); // NOT OK. + + var base = $("#id"); + base[html ? 'html' : 'text']($("textarea").val()); // NOT OK. + + $("#id").get(0).innerHTML = $("input").get(0).name; // NOT OK. + $("#id").get(0).innerHTML = $("input").get(0).getAttribute("name"); // NOT OK. + + $("#id").get(0).innerHTML = $("input").getAttribute("id"); // OK. + + $("#id").get(0).innerHTML = $(document).find("option").attr("value"); // NOT OK. + + var valMethod = $("textarea").val; + $("#id").get(0).innerHTML = valMethod(); // NOT OK + + var myValue = $(document).find("option").attr("value"); + if(myValue.property) { + $("#id").get(0).innerHTML = myValue; // OK. + } +})(); \ No newline at end of file