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. 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..91f8d0a204d4 --- /dev/null +++ b/javascript/ql/lib/change-notes/2026-07-16-vue-router-useRoute-query.md @@ -0,0 +1,5 @@ +--- +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. 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 59490a2d5c65..35a66debf4f1 100644 --- a/javascript/ql/lib/semmle/javascript/frameworks/Vue.qll +++ b/javascript/ql/lib/semmle/javascript/frameworks/Vue.qll @@ -35,6 +35,40 @@ module Vue { result = any(GlobalVueEntryPoint e).getANode() } + /** + * 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"] and + this = "vue." + name + } + + override predicate propagatesFlow(string input, string output, boolean preservesValue) { + name = ["ref", "shallowRef", "toRef"] and + input = "Argument[0]" and + output = "ReturnValue.Member[value]" and + preservesValue = true + or + name = "reactive" and + input = "Argument[0]" and + output = "ReturnValue" and + preservesValue = false + } + + override DataFlow::InvokeNode getACall() { + result = API::moduleImport("vue").getMember(name).getACall() + } + } + /** * Gets a reference to the 'Vue' object. */ @@ -652,6 +686,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 = 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..01ff68751781 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 | @@ -246,3 +248,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);