diff --git a/javascript/ql/lib/semmle/javascript/dataflow/internal/DataFlowPrivate.qll b/javascript/ql/lib/semmle/javascript/dataflow/internal/DataFlowPrivate.qll
index 4b82d895f642..2fcc2acbd167 100644
--- a/javascript/ql/lib/semmle/javascript/dataflow/internal/DataFlowPrivate.qll
+++ b/javascript/ql/lib/semmle/javascript/dataflow/internal/DataFlowPrivate.qll
@@ -372,10 +372,11 @@ class CastNode extends DataFlow::Node {
cached
newtype TDataFlowCallable =
MkSourceCallable(StmtContainer container) or
- MkLibraryCallable(LibraryCallable callable)
+ MkLibraryCallable(LibraryCallable callable) or
+ MkFileCallable(File file)
/**
- * A callable entity. This is a wrapper around either a `StmtContainer` or a `LibraryCallable`.
+ * A callable entity. This is a wrapper around either a `StmtContainer`, `LibraryCallable`, or `File`.
*/
class DataFlowCallable extends TDataFlowCallable {
/** Gets a string representation of this callable. */
@@ -383,14 +384,21 @@ class DataFlowCallable extends TDataFlowCallable {
result = this.asSourceCallable().toString()
or
result = this.asLibraryCallable()
+ or
+ result = this.asFileCallable().toString()
}
/** Gets the location of this callable, if it is present in the source code. */
- Location getLocation() { result = this.asSourceCallable().getLocation() }
+ Location getLocation() {
+ result = this.asSourceCallable().getLocation() or result = this.asFileCallable().getLocation()
+ }
/** Gets the corresponding `StmtContainer` if this is a source callable. */
StmtContainer asSourceCallable() { this = MkSourceCallable(result) }
+ /** Gets the corresponding `File` if this is a file representing a callable. */
+ File asFileCallable() { this = MkFileCallable(result) }
+
/** Gets the corresponding `StmtContainer` if this is a source callable. */
pragma[nomagic]
StmtContainer asSourceCallableNotExterns() {
@@ -537,6 +545,10 @@ DataFlowCallable nodeGetEnclosingCallable(Node node) {
result.asLibraryCallable() = node.(FlowSummaryDefaultExceptionalReturn).getSummarizedCallable()
or
node = TGenericSynthesizedNode(_, _, result)
+ or
+ node instanceof DataFlow::HtmlAttributeNode and result.asFileCallable() = node.getFile()
+ or
+ node instanceof DataFlow::XmlAttributeNode and result.asFileCallable() = node.getFile()
}
newtype TDataFlowType =
diff --git a/javascript/ql/src/change-notes/2025-03-11-vue-fix.md b/javascript/ql/src/change-notes/2025-03-11-vue-fix.md
new file mode 100644
index 000000000000..b32e4f1fe563
--- /dev/null
+++ b/javascript/ql/src/change-notes/2025-03-11-vue-fix.md
@@ -0,0 +1,6 @@
+---
+category: fix
+---
+* Fixed a bug, first introduced in `2.20.3`, that would prevent `v-html` attributes in Vue files
+ from being flagged by the `js/xss` query. The original behaviour has been restored and the `v-html`
+ attribute is once again functioning as a sink for the `js/xss` query.
diff --git a/javascript/ql/test/query-tests/Security/CWE-079/DomBasedXss/Xss.expected b/javascript/ql/test/query-tests/Security/CWE-079/DomBasedXss/Xss.expected
index de03bca13d4b..2e72b292f72d 100644
--- a/javascript/ql/test/query-tests/Security/CWE-079/DomBasedXss/Xss.expected
+++ b/javascript/ql/test/query-tests/Security/CWE-079/DomBasedXss/Xss.expected
@@ -224,6 +224,7 @@
| tst.js:484:33:484:63 | decodeU ... n.hash) | tst.js:484:43:484:62 | window.location.hash | tst.js:484:33:484:63 | decodeU ... n.hash) | Cross-site scripting vulnerability due to $@. | tst.js:484:43:484:62 | window.location.hash | user-provided value |
| tst.js:492:18:492:54 | target. ... "), '') | tst.js:491:16:491:39 | documen ... .search | tst.js:492:18:492:54 | target. ... "), '') | Cross-site scripting vulnerability due to $@. | tst.js:491:16:491:39 | documen ... .search | user-provided value |
| typeahead.js:25:18:25:20 | val | typeahead.js:20:22:20:45 | documen ... .search | typeahead.js:25:18:25:20 | val | Cross-site scripting vulnerability due to $@. | typeahead.js:20:22:20:45 | documen ... .search | user-provided value |
+| v-html.vue:2:8:2:23 | v-html=tainted | v-html.vue:6:42:6:58 | document.location | v-html.vue:2:8:2:23 | v-html=tainted | Cross-site scripting vulnerability due to $@. | v-html.vue:6:42:6:58 | document.location | user-provided value |
| various-concat-obfuscations.js:4:4:4:31 | "
" ...
" | various-concat-obfuscations.js:2:16:2:39 | documen ... .search | various-concat-obfuscations.js:4:4:4:31 | "" ...
" | Cross-site scripting vulnerability due to $@. | various-concat-obfuscations.js:2:16:2:39 | documen ... .search | user-provided value |
| various-concat-obfuscations.js:5:4:5:26 | `$ ...
` | various-concat-obfuscations.js:2:16:2:39 | documen ... .search | various-concat-obfuscations.js:5:4:5:26 | `$ ...
` | Cross-site scripting vulnerability due to $@. | various-concat-obfuscations.js:2:16:2:39 | documen ... .search | user-provided value |
| various-concat-obfuscations.js:6:4:6:43 | "" ... /div>") | various-concat-obfuscations.js:2:16:2:39 | documen ... .search | various-concat-obfuscations.js:6:4:6:43 | "
" ... /div>") | Cross-site scripting vulnerability due to $@. | various-concat-obfuscations.js:2:16:2:39 | documen ... .search | user-provided value |
@@ -748,6 +749,7 @@ edges
| typeahead.js:20:22:20:45 | documen ... .search | typeahead.js:20:13:20:45 | target | provenance | |
| typeahead.js:21:12:21:17 | target | typeahead.js:24:30:24:32 | val | provenance | |
| typeahead.js:24:30:24:32 | val | typeahead.js:25:18:25:20 | val | provenance | |
+| v-html.vue:6:42:6:58 | document.location | v-html.vue:2:8:2:23 | v-html=tainted | provenance | |
| various-concat-obfuscations.js:2:6:2:39 | tainted | various-concat-obfuscations.js:4:14:4:20 | tainted | provenance | |
| various-concat-obfuscations.js:2:6:2:39 | tainted | various-concat-obfuscations.js:5:12:5:18 | tainted | provenance | |
| various-concat-obfuscations.js:2:6:2:39 | tainted | various-concat-obfuscations.js:6:19:6:25 | tainted | provenance | |
@@ -1400,6 +1402,8 @@ nodes
| typeahead.js:21:12:21:17 | target | semmle.label | target |
| typeahead.js:24:30:24:32 | val | semmle.label | val |
| typeahead.js:25:18:25:20 | val | semmle.label | val |
+| v-html.vue:2:8:2:23 | v-html=tainted | semmle.label | v-html=tainted |
+| v-html.vue:6:42:6:58 | document.location | semmle.label | document.location |
| various-concat-obfuscations.js:2:6:2:39 | tainted | semmle.label | tainted |
| various-concat-obfuscations.js:2:16:2:39 | documen ... .search | semmle.label | documen ... .search |
| various-concat-obfuscations.js:4:4:4:31 | "
" ...
" | semmle.label | "
" ...
" |
diff --git a/javascript/ql/test/query-tests/Security/CWE-079/DomBasedXss/XssWithAdditionalSources.expected b/javascript/ql/test/query-tests/Security/CWE-079/DomBasedXss/XssWithAdditionalSources.expected
index 62627ff7ceba..132189a22f36 100644
--- a/javascript/ql/test/query-tests/Security/CWE-079/DomBasedXss/XssWithAdditionalSources.expected
+++ b/javascript/ql/test/query-tests/Security/CWE-079/DomBasedXss/XssWithAdditionalSources.expected
@@ -614,6 +614,8 @@ nodes
| typeahead.js:21:12:21:17 | target | semmle.label | target |
| typeahead.js:24:30:24:32 | val | semmle.label | val |
| typeahead.js:25:18:25:20 | val | semmle.label | val |
+| v-html.vue:2:8:2:23 | v-html=tainted | semmle.label | v-html=tainted |
+| v-html.vue:6:42:6:58 | document.location | semmle.label | document.location |
| various-concat-obfuscations.js:2:6:2:39 | tainted | semmle.label | tainted |
| various-concat-obfuscations.js:2:16:2:39 | documen ... .search | semmle.label | documen ... .search |
| various-concat-obfuscations.js:4:4:4:31 | "
" ...
" | semmle.label | "
" ...
" |
@@ -1189,6 +1191,7 @@ edges
| typeahead.js:20:22:20:45 | documen ... .search | typeahead.js:20:13:20:45 | target | provenance | |
| typeahead.js:21:12:21:17 | target | typeahead.js:24:30:24:32 | val | provenance | |
| typeahead.js:24:30:24:32 | val | typeahead.js:25:18:25:20 | val | provenance | |
+| v-html.vue:6:42:6:58 | document.location | v-html.vue:2:8:2:23 | v-html=tainted | provenance | |
| various-concat-obfuscations.js:2:6:2:39 | tainted | various-concat-obfuscations.js:4:14:4:20 | tainted | provenance | |
| various-concat-obfuscations.js:2:6:2:39 | tainted | various-concat-obfuscations.js:5:12:5:18 | tainted | provenance | |
| various-concat-obfuscations.js:2:6:2:39 | tainted | various-concat-obfuscations.js:6:19:6:25 | tainted | provenance | |
diff --git a/javascript/ql/test/query-tests/Security/CWE-079/DomBasedXss/v-html.vue b/javascript/ql/test/query-tests/Security/CWE-079/DomBasedXss/v-html.vue
index 3964d4adad36..d75413a527b2 100644
--- a/javascript/ql/test/query-tests/Security/CWE-079/DomBasedXss/v-html.vue
+++ b/javascript/ql/test/query-tests/Security/CWE-079/DomBasedXss/v-html.vue
@@ -1,9 +1,9 @@
-
+