diff --git a/javascript/ql/lib/change-notes/2026-07-16-vue-router-useRoute-query.md b/javascript/ql/lib/change-notes/2026-07-16-vue-router-useRoute-query.md new file mode 100644 index 000000000000..973af15aae03 --- /dev/null +++ b/javascript/ql/lib/change-notes/2026-07-16-vue-router-useRoute-query.md @@ -0,0 +1,5 @@ +--- +category: minorAnalysis +--- +* The route object returned by Vue Router's `useRoute()` Composition API is now recognized as a client-side remote flow source, covering its `query`, `params`, `path`, `fullPath`, and `hash` members. These members are additionally reported under the corresponding `browser-url-query`, `browser-url-path`, and `browser-url-fragment` threat models. +* Added flow models for Vue's `ref`, `shallowRef`, `toRef`, `reactive`, and `computed` Composition API helpers. diff --git a/javascript/ql/lib/ext/vue.model.yml b/javascript/ql/lib/ext/vue.model.yml new file mode 100644 index 000000000000..17d473210335 --- /dev/null +++ b/javascript/ql/lib/ext/vue.model.yml @@ -0,0 +1,22 @@ +extensions: + - addsTo: + pack: codeql/javascript-all + extensible: sourceModel + data: + # `useRoute()` — Vue Router's Composition API returns a route object whose URL-derived + # members are client-side remote flow sources. + - ["vue-router", "Member[useRoute].ReturnValue.Member[params,path,fullPath]", "browser-url-path"] + - ["vue-router", "Member[useRoute].ReturnValue.Member[query]", "browser-url-query"] + - ["vue-router", "Member[useRoute].ReturnValue.Member[hash]", "browser-url-fragment"] + - addsTo: + pack: codeql/javascript-all + extensible: summaryModel + data: + # `ref`, `shallowRef`, `toRef` — the wrapped argument flows to `.value`. + - ["vue", "Member[ref,shallowRef,toRef]", "Argument[0]", "ReturnValue.Member[value]", "value"] + # `reactive` — the wrapped argument taints the returned reactive object. + - ["vue", "Member[reactive]", "Argument[0]", "ReturnValue", "taint"] + # `computed(() => ...)` — function overload: the getter's return value flows to `.value`. + - ["vue", "Member[computed]", "Argument[0].ReturnValue", "ReturnValue.Member[value]", "value"] + # `computed({ get() { ... } })` — object overload: the `get` getter's return value flows to `.value`. + - ["vue", "Member[computed]", "Argument[0].Member[get].ReturnValue", "ReturnValue.Member[value]", "value"] diff --git a/javascript/ql/test/library-tests/frameworks/Vue/router.js b/javascript/ql/test/library-tests/frameworks/Vue/router.js index 65dc4d13e99e..efae87483443 100644 --- a/javascript/ql/test/library-tests/frameworks/Vue/router.js +++ b/javascript/ql/test/library-tests/frameworks/Vue/router.js @@ -1,4 +1,4 @@ -import Router from 'vue-router'; +import Router, { useRoute } from 'vue-router'; export const router = new Router({ routes: [ @@ -43,3 +43,5 @@ router.afterEach((to, from) => { to.query.x; from.query.x; }); + +useRoute().query; diff --git a/javascript/ql/test/library-tests/frameworks/Vue/tests.expected b/javascript/ql/test/library-tests/frameworks/Vue/tests.expected index 633a8f9924db..4ba7ba338637 100644 --- a/javascript/ql/test/library-tests/frameworks/Vue/tests.expected +++ b/javascript/ql/test/library-tests/frameworks/Vue/tests.expected @@ -191,6 +191,7 @@ remoteFlowSource | router.js:39:5:39:14 | from.query | | router.js:43:5:43:12 | to.query | | router.js:44:5:44:14 | from.query | +| router.js:47:1:47:16 | useRoute().query | parseErrors attribute | compont-with-route.vue:2:8:2:21 | v-html=dataA | v-html | @@ -239,6 +240,8 @@ threatModelSource | router.js:39:5:39:14 | from.query | remote | | router.js:43:5:43:12 | to.query | remote | | router.js:44:5:44:14 | from.query | remote | +| router.js:47:1:47:16 | useRoute().query | browser-url-query | +| router.js:47:1:47:16 | useRoute().query | remote | | single-component-file-1.vue:7:45:7:54 | this.input | view-component-input | | single-file-component-3-script.js:5:42:5:51 | this.input | view-component-input | | single-file-component-4.vue:21:14:21:23 | this.input | view-component-input | @@ -246,3 +249,16 @@ threatModelSource | single-file-component-6.vue:5:11:5:15 | input | view-component-input | | single-file-component-7.vue:5:11:5:15 | input | view-component-input | | single-file-component-8.vue:5:11:5:15 | input | view-component-input | +compositionApiDataFlow +| tst.js:119:14:119:26 | source("ref") | tst.js:119:6:119:33 | Vue.ref ... ).value | +| tst.js:120:21:120:40 | source("shallowRef") | tst.js:120:6:120:47 | Vue.sha ... ).value | +| tst.js:121:16:121:30 | source("toRef") | tst.js:121:6:121:37 | Vue.toR ... ).value | +| tst.js:123:25:123:42 | source("computed") | tst.js:123:6:123:49 | Vue.com ... ).value | +| tst.js:124:36:124:59 | source( ... bject") | tst.js:124:6:124:82 | Vue.com ... ).value | +compositionApiTaintFlow +| tst.js:119:14:119:26 | source("ref") | tst.js:119:6:119:33 | Vue.ref ... ).value | +| tst.js:120:21:120:40 | source("shallowRef") | tst.js:120:6:120:47 | Vue.sha ... ).value | +| tst.js:121:16:121:30 | source("toRef") | tst.js:121:6:121:37 | Vue.toR ... ).value | +| tst.js:122:19:122:36 | source("reactive") | tst.js:122:6:122:37 | Vue.rea ... tive")) | +| tst.js:123:25:123:42 | source("computed") | tst.js:123:6:123:49 | Vue.com ... ).value | +| tst.js:124:36:124:59 | source( ... bject") | tst.js:124:6:124:82 | Vue.com ... ).value | diff --git a/javascript/ql/test/library-tests/frameworks/Vue/tests.ql b/javascript/ql/test/library-tests/frameworks/Vue/tests.ql index c631f46d3293..6121a7f9a46b 100644 --- a/javascript/ql/test/library-tests/frameworks/Vue/tests.ql +++ b/javascript/ql/test/library-tests/frameworks/Vue/tests.ql @@ -1,6 +1,24 @@ import javascript import semmle.javascript.security.dataflow.DomBasedXssCustomizations +module TestConfig implements DataFlow::ConfigSig { + predicate isSource(DataFlow::Node source) { + source.(DataFlow::CallNode).getCalleeName() = "source" + } + + predicate isSink(DataFlow::Node sink) { + sink = any(DataFlow::CallNode call | call.getCalleeName() = "sink").getAnArgument() + } +} + +module TestDataFlow = DataFlow::Global; + +module TestTaintFlow = TaintTracking::Global; + +query predicate compositionApiDataFlow = TestDataFlow::flow/2; + +query predicate compositionApiTaintFlow = TestTaintFlow::flow/2; + query predicate component_getAPropertyValue(Vue::Component c, string name, DataFlow::Node prop) { c.getAPropertyValue(name) = prop } diff --git a/javascript/ql/test/library-tests/frameworks/Vue/tst.js b/javascript/ql/test/library-tests/frameworks/Vue/tst.js index 6ee0954063a9..d402668c2e1d 100644 --- a/javascript/ql/test/library-tests/frameworks/Vue/tst.js +++ b/javascript/ql/test/library-tests/frameworks/Vue/tst.js @@ -115,3 +115,10 @@ let subclass2 = base.extend({ fromSubclass2: 100 } }); + +sink(Vue.ref(source("ref")).value); +sink(Vue.shallowRef(source("shallowRef")).value); +sink(Vue.toRef(source("toRef")).value); +sink(Vue.reactive(source("reactive"))); +sink(Vue.computed(() => source("computed")).value); +sink(Vue.computed({ get() { return source("computedObject"); }, set(v) {} }).value);