From 7ef911e61ba2106c51d51396f7a9cd3daa374abd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 16 Jul 2026 15:48:53 +0000 Subject: [PATCH 1/9] Model Vue Router useRoute sources --- javascript/ql/lib/semmle/javascript/frameworks/Vue.qll | 2 ++ 1 file changed, 2 insertions(+) diff --git a/javascript/ql/lib/semmle/javascript/frameworks/Vue.qll b/javascript/ql/lib/semmle/javascript/frameworks/Vue.qll index 59490a2d5c65..193571b3b773 100644 --- a/javascript/ql/lib/semmle/javascript/frameworks/Vue.qll +++ b/javascript/ql/lib/semmle/javascript/frameworks/Vue.qll @@ -652,6 +652,8 @@ module Vue { t.start() and ( exists(API::Node router | router = API::moduleImport("vue-router") | + result = router.getMember("useRoute").getACall() + or result = router.getInstance().getMember("currentRoute").asSource() or result = From 74496589f0be3e1f6785a039743f809e52190558 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 16 Jul 2026 15:51:49 +0000 Subject: [PATCH 2/9] Test Vue useRoute query source --- javascript/ql/test/library-tests/frameworks/Vue/router.js | 4 +++- .../ql/test/library-tests/frameworks/Vue/tests.expected | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) 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..9379678a8bd3 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,7 @@ 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 | 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 | From 9aaa1492544426fbd302b641cd87f7e1b22bf6cd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 16 Jul 2026 16:04:19 +0000 Subject: [PATCH 3/9] Add change note for Vue Router useRoute query support --- change-notes/1.26/analysis-javascript.md | 1 + 1 file changed, 1 insertion(+) diff --git a/change-notes/1.26/analysis-javascript.md b/change-notes/1.26/analysis-javascript.md index 15edb607c70a..ef3a8e23ffd0 100644 --- a/change-notes/1.26/analysis-javascript.md +++ b/change-notes/1.26/analysis-javascript.md @@ -42,6 +42,7 @@ - [styled-components](https://www.npmjs.com/package/styled-components) - [throttle-debounce](https://www.npmjs.com/package/throttle-debounce) - [underscore](https://www.npmjs.com/package/underscore) + - [vue-router](https://www.npmjs.com/package/vue-router) * Analyzing files with the ".cjs" extension is now supported. * ES2021 features are now supported. From 03d2ad50e7f15f9ad3072bf007906e6a6931ddeb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 16 Jul 2026 16:08:50 +0000 Subject: [PATCH 4/9] Add YYYY-MM-DD format change note for Vue Router useRoute query --- .../lib/change-notes/2026-07-16-vue-router-useRoute-query.md | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 javascript/ql/lib/change-notes/2026-07-16-vue-router-useRoute-query.md 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..eeec2232f3e9 --- /dev/null +++ b/javascript/ql/lib/change-notes/2026-07-16-vue-router-useRoute-query.md @@ -0,0 +1,4 @@ +--- +category: minorAnalysis +--- +* The query parameter of Vue Router's `useRoute()` Composition API is now recognized as a client-side remote flow source. From 7e08178d63828c4b9a23db2583d28a8be4614c05 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 16 Jul 2026 17:12:55 +0000 Subject: [PATCH 5/9] Add Vue summary models for ref, shallowRef, toRef, reactive, and computed --- javascript/ql/lib/ext/vue.model.yml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 javascript/ql/lib/ext/vue.model.yml diff --git a/javascript/ql/lib/ext/vue.model.yml b/javascript/ql/lib/ext/vue.model.yml new file mode 100644 index 000000000000..20af3b1f1acb --- /dev/null +++ b/javascript/ql/lib/ext/vue.model.yml @@ -0,0 +1,15 @@ +extensions: + - addsTo: + pack: codeql/javascript-all + extensible: summaryModel + data: + # ref(x).value === x + - ["vue", "Member[ref]", "Argument[0]", "ReturnValue.Member[value]", "value"] + # shallowRef(x).value === x + - ["vue", "Member[shallowRef]", "Argument[0]", "ReturnValue.Member[value]", "value"] + # customRef / toRef keep the wrapped value reachable via .value + - ["vue", "Member[toRef]", "Argument[0]", "ReturnValue.Member[value]", "taint"] + # reactive(x) returns a proxy that preserves the members of x + - ["vue", "Member[reactive]", "Argument[0]", "ReturnValue", "taint"] + # computed(() => expr).value === return value of the getter + - ["vue", "Member[computed]", "Argument[0].ReturnValue", "ReturnValue.Member[value]", "value"] From dc15554a1ed5f86315ebbbbdc9c359e97e8f38eb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 16 Jul 2026 17:38:30 +0000 Subject: [PATCH 6/9] Move Vue flow models into QL --- .../2026-07-16-vue-composition-api-flow.md | 4 +++ javascript/ql/lib/ext/vue.model.yml | 15 -------- .../lib/semmle/javascript/frameworks/Vue.qll | 36 +++++++++++++++++++ .../library-tests/frameworks/Vue/tests.ql | 18 ++++++++++ .../test/library-tests/frameworks/Vue/tst.js | 6 ++++ 5 files changed, 64 insertions(+), 15 deletions(-) create mode 100644 javascript/ql/lib/change-notes/2026-07-16-vue-composition-api-flow.md delete mode 100644 javascript/ql/lib/ext/vue.model.yml diff --git a/javascript/ql/lib/change-notes/2026-07-16-vue-composition-api-flow.md b/javascript/ql/lib/change-notes/2026-07-16-vue-composition-api-flow.md new file mode 100644 index 000000000000..d03bebdb129d --- /dev/null +++ b/javascript/ql/lib/change-notes/2026-07-16-vue-composition-api-flow.md @@ -0,0 +1,4 @@ +--- +category: minorAnalysis +--- +* 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 deleted file mode 100644 index 20af3b1f1acb..000000000000 --- a/javascript/ql/lib/ext/vue.model.yml +++ /dev/null @@ -1,15 +0,0 @@ -extensions: - - addsTo: - pack: codeql/javascript-all - extensible: summaryModel - data: - # ref(x).value === x - - ["vue", "Member[ref]", "Argument[0]", "ReturnValue.Member[value]", "value"] - # shallowRef(x).value === x - - ["vue", "Member[shallowRef]", "Argument[0]", "ReturnValue.Member[value]", "value"] - # customRef / toRef keep the wrapped value reachable via .value - - ["vue", "Member[toRef]", "Argument[0]", "ReturnValue.Member[value]", "taint"] - # reactive(x) returns a proxy that preserves the members of x - - ["vue", "Member[reactive]", "Argument[0]", "ReturnValue", "taint"] - # computed(() => expr).value === return value of the getter - - ["vue", "Member[computed]", "Argument[0].ReturnValue", "ReturnValue.Member[value]", "value"] diff --git a/javascript/ql/lib/semmle/javascript/frameworks/Vue.qll b/javascript/ql/lib/semmle/javascript/frameworks/Vue.qll index 193571b3b773..0e8f48de49bc 100644 --- a/javascript/ql/lib/semmle/javascript/frameworks/Vue.qll +++ b/javascript/ql/lib/semmle/javascript/frameworks/Vue.qll @@ -35,6 +35,42 @@ module Vue { result = any(GlobalVueEntryPoint e).getANode() } + /** Models data flow through Vue Composition API helpers. */ + private class VueCompositionApiSummary extends DataFlow::SummarizedCallable::Range { + string name; + + VueCompositionApiSummary() { + name = ["ref", "shallowRef", "toRef", "reactive", "computed"] and + this = "vue." + name + } + + override predicate propagatesFlow(string input, string output, boolean preservesValue) { + name = ["ref", "shallowRef"] and + input = "Argument[0]" and + output = "ReturnValue.Member[value]" and + preservesValue = true + or + name = "toRef" and + input = "Argument[0]" and + output = "ReturnValue.Member[value]" and + preservesValue = false + or + name = "reactive" and + input = "Argument[0]" and + output = "ReturnValue" and + preservesValue = false + or + name = "computed" and + input = "Argument[0].ReturnValue" and + output = "ReturnValue.Member[value]" and + preservesValue = true + } + + override DataFlow::InvokeNode getACall() { + result = API::moduleImport("vue").getMember(name).getACall() + } + } + /** * Gets a reference to the 'Vue' object. */ 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..64c14a291194 100644 --- a/javascript/ql/test/library-tests/frameworks/Vue/tst.js +++ b/javascript/ql/test/library-tests/frameworks/Vue/tst.js @@ -115,3 +115,9 @@ 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); From d99bb542b70cf994535bf4c102350e1200facc5b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 16 Jul 2026 17:42:19 +0000 Subject: [PATCH 7/9] Fix and verify Vue flow summaries --- javascript/ql/lib/semmle/javascript/frameworks/Vue.qll | 1 + .../test/library-tests/frameworks/Vue/tests.expected | 10 ++++++++++ 2 files changed, 11 insertions(+) diff --git a/javascript/ql/lib/semmle/javascript/frameworks/Vue.qll b/javascript/ql/lib/semmle/javascript/frameworks/Vue.qll index 0e8f48de49bc..1edb53e814c5 100644 --- a/javascript/ql/lib/semmle/javascript/frameworks/Vue.qll +++ b/javascript/ql/lib/semmle/javascript/frameworks/Vue.qll @@ -36,6 +36,7 @@ module Vue { } /** Models data flow through Vue Composition API helpers. */ + overlay[local?] private class VueCompositionApiSummary extends DataFlow::SummarizedCallable::Range { string name; diff --git a/javascript/ql/test/library-tests/frameworks/Vue/tests.expected b/javascript/ql/test/library-tests/frameworks/Vue/tests.expected index 9379678a8bd3..ffa220d5dc8e 100644 --- a/javascript/ql/test/library-tests/frameworks/Vue/tests.expected +++ b/javascript/ql/test/library-tests/frameworks/Vue/tests.expected @@ -248,3 +248,13 @@ 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:123:25:123:42 | source("computed") | tst.js:123:6:123:49 | 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 | From c5b3ca0e98841a515218c7e504d88cc637b1e63a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 16 Jul 2026 17:47:12 +0000 Subject: [PATCH 8/9] Merge Vue change notes --- .../lib/change-notes/2026-07-16-vue-composition-api-flow.md | 4 ---- .../lib/change-notes/2026-07-16-vue-router-useRoute-query.md | 1 + 2 files changed, 1 insertion(+), 4 deletions(-) delete mode 100644 javascript/ql/lib/change-notes/2026-07-16-vue-composition-api-flow.md diff --git a/javascript/ql/lib/change-notes/2026-07-16-vue-composition-api-flow.md b/javascript/ql/lib/change-notes/2026-07-16-vue-composition-api-flow.md deleted file mode 100644 index d03bebdb129d..000000000000 --- a/javascript/ql/lib/change-notes/2026-07-16-vue-composition-api-flow.md +++ /dev/null @@ -1,4 +0,0 @@ ---- -category: minorAnalysis ---- -* Added flow models for Vue's `ref`, `shallowRef`, `toRef`, `reactive`, and `computed` Composition API helpers. 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 index eeec2232f3e9..91f8d0a204d4 100644 --- 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 @@ -2,3 +2,4 @@ category: minorAnalysis --- * The query parameter of Vue Router's `useRoute()` Composition API is now recognized as a client-side remote flow source. +* Added flow models for Vue's `ref`, `shallowRef`, `toRef`, `reactive`, and `computed` Composition API helpers. From 0f8f88484778ebf96529826846f14cb968a07926 Mon Sep 17 00:00:00 2001 From: Adrien Pessu <7055334+adrienpessu@users.noreply.github.com> Date: Fri, 17 Jul 2026 15:27:48 +0200 Subject: [PATCH 9/9] Add Vue toRef value-flow and computed object-overload tests Make `toRef` value-preserving and model the `computed` object overload so `.value` flow is exercised for both `computed` API shapes. `computed` is moved into the `vue.model.yml` data extension because the object overload requires callback flow synthesis that a hand-written `SummarizedCallable` cannot provide. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 5c95b651-1e79-499e-9e11-e09065b14aa9 --- javascript/ql/lib/ext/vue.model.yml | 9 ++++++++ .../lib/semmle/javascript/frameworks/Vue.qll | 23 ++++++++----------- .../frameworks/Vue/tests.expected | 3 +++ .../test/library-tests/frameworks/Vue/tst.js | 1 + 4 files changed, 23 insertions(+), 13 deletions(-) create mode 100644 javascript/ql/lib/ext/vue.model.yml diff --git a/javascript/ql/lib/ext/vue.model.yml b/javascript/ql/lib/ext/vue.model.yml new file mode 100644 index 000000000000..9ff24265a8d4 --- /dev/null +++ b/javascript/ql/lib/ext/vue.model.yml @@ -0,0 +1,9 @@ +extensions: + - addsTo: + pack: codeql/javascript-all + extensible: summaryModel + data: + # `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/lib/semmle/javascript/frameworks/Vue.qll b/javascript/ql/lib/semmle/javascript/frameworks/Vue.qll index 1edb53e814c5..35a66debf4f1 100644 --- a/javascript/ql/lib/semmle/javascript/frameworks/Vue.qll +++ b/javascript/ql/lib/semmle/javascript/frameworks/Vue.qll @@ -35,36 +35,33 @@ module Vue { result = any(GlobalVueEntryPoint e).getANode() } - /** Models data flow through Vue Composition API helpers. */ + /** + * Models data flow through Vue Composition API helpers. + * + * Note that `computed` is not modeled here but in the `vue.model.yml` data + * extension, because its object overload (`computed({ get() { ... } })`) + * requires callback flow synthesis that data extensions support but a + * hand-written `SummarizedCallable` does not. + */ overlay[local?] private class VueCompositionApiSummary extends DataFlow::SummarizedCallable::Range { string name; VueCompositionApiSummary() { - name = ["ref", "shallowRef", "toRef", "reactive", "computed"] and + name = ["ref", "shallowRef", "toRef", "reactive"] and this = "vue." + name } override predicate propagatesFlow(string input, string output, boolean preservesValue) { - name = ["ref", "shallowRef"] and + name = ["ref", "shallowRef", "toRef"] and input = "Argument[0]" and output = "ReturnValue.Member[value]" and preservesValue = true or - name = "toRef" and - input = "Argument[0]" and - output = "ReturnValue.Member[value]" and - preservesValue = false - or name = "reactive" and input = "Argument[0]" and output = "ReturnValue" and preservesValue = false - or - name = "computed" and - input = "Argument[0].ReturnValue" and - output = "ReturnValue.Member[value]" and - preservesValue = true } override DataFlow::InvokeNode getACall() { diff --git a/javascript/ql/test/library-tests/frameworks/Vue/tests.expected b/javascript/ql/test/library-tests/frameworks/Vue/tests.expected index ffa220d5dc8e..01ff68751781 100644 --- a/javascript/ql/test/library-tests/frameworks/Vue/tests.expected +++ b/javascript/ql/test/library-tests/frameworks/Vue/tests.expected @@ -251,10 +251,13 @@ threatModelSource 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/tst.js b/javascript/ql/test/library-tests/frameworks/Vue/tst.js index 64c14a291194..d402668c2e1d 100644 --- a/javascript/ql/test/library-tests/frameworks/Vue/tst.js +++ b/javascript/ql/test/library-tests/frameworks/Vue/tst.js @@ -121,3 +121,4 @@ 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);