Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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.
22 changes: 22 additions & 0 deletions javascript/ql/lib/ext/vue.model.yml
Original file line number Diff line number Diff line change
@@ -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"]
4 changes: 3 additions & 1 deletion javascript/ql/test/library-tests/frameworks/Vue/router.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import Router from 'vue-router';
import Router, { useRoute } from 'vue-router';

export const router = new Router({
routes: [
Expand Down Expand Up @@ -43,3 +43,5 @@ router.afterEach((to, from) => {
to.query.x;
from.query.x;
});

useRoute().query;
16 changes: 16 additions & 0 deletions javascript/ql/test/library-tests/frameworks/Vue/tests.expected
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down Expand Up @@ -239,10 +240,25 @@ 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 |
| single-file-component-5.vue:19:14:19:23 | this.input | view-component-input |
| 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 |
18 changes: 18 additions & 0 deletions javascript/ql/test/library-tests/frameworks/Vue/tests.ql
Original file line number Diff line number Diff line change
@@ -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<TestConfig>;

module TestTaintFlow = TaintTracking::Global<TestConfig>;

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
}
Expand Down
7 changes: 7 additions & 0 deletions javascript/ql/test/library-tests/frameworks/Vue/tst.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Comment thread
adrienpessu marked this conversation as resolved.
Comment thread
adrienpessu marked this conversation as resolved.
Comment thread
adrienpessu marked this conversation as resolved.
sink(Vue.reactive(source("reactive")));
sink(Vue.computed(() => source("computed")).value);
Comment thread
adrienpessu marked this conversation as resolved.
Comment thread
adrienpessu marked this conversation as resolved.
sink(Vue.computed({ get() { return source("computedObject"); }, set(v) {} }).value);
Loading