Skip to content

Commit 5264d24

Browse files
committed
JS: Model vue-router
1 parent d490bea commit 5264d24

File tree

6 files changed

+218
-2
lines changed

6 files changed

+218
-2
lines changed
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
lgtm,codescanning
2+
* Support for Vue has improved. Taint sources from [vue-router](https://npmjs.com/package/vue-router)
3+
route parameters are now recognized.

javascript/ql/src/semmle/javascript/frameworks/Vue.qll

Lines changed: 88 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@ module Vue {
3535
result =
3636
[
3737
"beforeCreate", "created", "beforeMount", "mounted", "beforeUpdate", "updated", "activated",
38-
"deactivated", "beforeDestroy", "destroyed", "errorCaptured"
38+
"deactivated", "beforeDestroy", "destroyed", "errorCaptured", "beforeRouteEnter",
39+
"beforeRouteUpdate", "beforeRouteLeave"
3940
]
4041
}
4142

@@ -210,6 +211,24 @@ module Vue {
210211
*/
211212
DataFlow::Node getComputed() { result = getOption("computed") }
212213

214+
/**
215+
* Gets the node for the `watch` option of this instance.
216+
*/
217+
DataFlow::Node getWatch() { result = getOption("watch") }
218+
219+
/**
220+
* Gets the function responding to changes to the given `propName`.
221+
*/
222+
DataFlow::FunctionNode getWatchHandler(string propName) {
223+
exists(DataFlow::SourceNode watcher |
224+
watcher = getWatch().getALocalSource().getAPropertySource(propName)
225+
|
226+
result = watcher
227+
or
228+
result = watcher.getAPropertySource("handler")
229+
)
230+
}
231+
213232
/**
214233
* Gets a node for a member of the `methods` option of this instance.
215234
*/
@@ -269,7 +288,7 @@ module Vue {
269288
* Gets the node for the life cycle hook of the `hookName` option of this instance.
270289
*/
271290
pragma[noinline]
272-
private DataFlow::Node getALifecycleHook(string hookName) {
291+
DataFlow::Node getALifecycleHook(string hookName) {
273292
hookName = lifecycleHookName() and
274293
(
275294
result = getOption(hookName)
@@ -287,6 +306,10 @@ module Vue {
287306
result = getAnAccessor(_)
288307
or
289308
result = getALifecycleHook(_)
309+
or
310+
result = getOption(_).(DataFlow::FunctionNode)
311+
or
312+
result = getOption(_).getALocalSource().getAPropertySource().(DataFlow::FunctionNode)
290313
}
291314

292315
/**
@@ -552,4 +575,67 @@ module Vue {
552575
HTML::Element getElement() { result = elem }
553576
}
554577
}
578+
579+
/** An API node referring to a `RouteConfig` being passed to `vue-router`. */
580+
private API::Node routeConfig() {
581+
result = API::moduleImport("vue-router").getParameter(0).getMember("routes").getAMember()
582+
or
583+
result = routeConfig().getMember("children").getAMember()
584+
}
585+
586+
/** Gets a data flow node that refers to a `Route` object from `vue-router`. */
587+
private DataFlow::SourceNode routeObject(DataFlow::TypeTracker t) {
588+
t.start() and
589+
(
590+
exists(API::Node router | router = API::moduleImport("vue-router") |
591+
result = router.getInstance().getMember("currentRoute").getAnImmediateUse()
592+
or
593+
result =
594+
router
595+
.getInstance()
596+
.getMember(["beforeEach", "beforeResolve", "afterEach"])
597+
.getParameter(0)
598+
.getParameter([0, 1])
599+
.getAnImmediateUse()
600+
or
601+
result =
602+
router
603+
.getParameter(0)
604+
.getMember("scrollBehavior")
605+
.getParameter([0, 1])
606+
.getAnImmediateUse()
607+
)
608+
or
609+
result = routeConfig().getMember("beforeEnter").getParameter([0, 1]).getAnImmediateUse()
610+
or
611+
exists(Instance i |
612+
result = i.getABoundFunction().getAFunctionValue().getReceiver().getAPropertyRead("$route")
613+
or
614+
result =
615+
i.getALifecycleHook(["beforeRouteEnter", "beforeRouteUpdate", "beforeRouteLeave"])
616+
.getAFunctionValue()
617+
.getParameter([0, 1])
618+
or
619+
result = i.getWatchHandler("$route").getParameter([0, 1])
620+
)
621+
)
622+
or
623+
exists(DataFlow::TypeTracker t2 | result = routeObject(t2).track(t2, t))
624+
}
625+
626+
/** Gets a data flow node that refers to a `Route` object from `vue-router`. */
627+
DataFlow::SourceNode routeObject() { result = routeObject(DataFlow::TypeTracker::end()) }
628+
629+
private class VueRouterFlowSource extends RemoteFlowSource {
630+
VueRouterFlowSource() {
631+
this = routeObject().getAPropertyRead(["params", "query", "hash", "path", "fullPath"])
632+
or
633+
exists(Instance i, string prop |
634+
this = i.getWatchHandler(prop).getParameter([0, 1]) and
635+
prop.regexpMatch("\\$route\\.(params|query|hash|path|fullPath)\\b.*")
636+
)
637+
}
638+
639+
override string getSourceType() { result = "Vue route parameter" }
640+
}
555641
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<template>
2+
<p v-html="dataA"/>
3+
</template>
4+
<script>
5+
import Vue from 'vue'
6+
import Component from 'vue-class-component'
7+
import { router } from './router';
8+
9+
@Component({
10+
watch: {
11+
'$route.params.id': {
12+
deep: true,
13+
handler(newId, oldId) { }
14+
},
15+
$route(to, from) { }
16+
}
17+
})
18+
export default class MyComponent extends Vue {
19+
message = 'Hello!'
20+
21+
sources() {
22+
this.$route.params.x;
23+
this.$route.query.x;
24+
this.$route.hash.x;
25+
this.$route.path;
26+
this.$route.fullPath;
27+
router.currentRoute.query.x;
28+
}
29+
30+
get dataA() {
31+
return this.$route.query.foo; // NOT OK
32+
}
33+
34+
beforeRouteEnter(to, from, next) {
35+
to.query.x;
36+
from.query.x;
37+
}
38+
39+
beforeRouteUpdate(to, from, next) {
40+
to.query.x;
41+
from.query.x;
42+
}
43+
44+
beforeRouteLeave(to, from, next) {
45+
to.query.x;
46+
from.query.x;
47+
}
48+
}
49+
</script>
50+
<style>
51+
</style>
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import Router from 'vue-router';
2+
3+
export const router = new Router({
4+
routes: [
5+
{
6+
path: '/foo',
7+
beforeEnter: (to, from, next) => {
8+
to.query.x;
9+
from.query.x;
10+
},
11+
children: [
12+
{
13+
path: '/bar',
14+
beforeEnter: (to, from, next) => {
15+
to.query.x;
16+
from.query.x;
17+
}
18+
}
19+
]
20+
}
21+
],
22+
scrollBehavior(to, from, savedPosition) {
23+
to.query.x;
24+
from.query.x;
25+
}
26+
});
27+
28+
router.beforeEach((to, from, next) => {
29+
to.query.x;
30+
from.query.x;
31+
});
32+
33+
router.afterEach((to, from) => {
34+
to.query.x;
35+
from.query.x;
36+
});
37+

javascript/ql/test/library-tests/frameworks/Vue/tests.expected

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
instance_getAPropertyValue
2+
| compont-with-route.vue:0:0:0:0 | compont-with-route.vue | dataA | compont-with-route.vue:31:14:31:34 | this.$r ... ery.foo |
3+
| compont-with-route.vue:0:0:0:0 | compont-with-route.vue | message | compont-with-route.vue:19:15:19:22 | 'Hello!' |
24
| single-component-file-1.vue:0:0:0:0 | single-component-file-1.vue | dataA | single-component-file-1.vue:6:40:6:41 | 42 |
35
| single-file-component-3.vue:0:0:0:0 | single-file-component-3.vue | dataA | single-file-component-3-script.js:4:37:4:38 | 42 |
46
| single-file-component-4.vue:0:0:0:0 | single-file-component-4.vue | dataA | single-file-component-4.vue:15:14:15:15 | 42 |
@@ -27,6 +29,7 @@ instance_getAPropertyValue
2729
| tst.js:94:2:96:3 | new Vue ... f,\\n\\t}) | dataA | tst.js:89:22:89:23 | 42 |
2830
| tst.js:99:2:104:3 | new Vue ... \\t\\t}\\n\\t}) | dataA | tst.js:100:18:100:19 | 42 |
2931
instance_getOption
32+
| compont-with-route.vue:0:0:0:0 | compont-with-route.vue | watch | compont-with-route.vue:10:12:16:5 | {\\n ... }\\n } |
3033
| single-component-file-1.vue:0:0:0:0 | single-component-file-1.vue | data | single-component-file-1.vue:6:11:6:45 | functio ... 42 } } |
3134
| single-file-component-3.vue:0:0:0:0 | single-file-component-3.vue | data | single-file-component-3-script.js:4:8:4:42 | functio ... 42 } } |
3235
| single-file-component-4.vue:0:0:0:0 | single-file-component-4.vue | render | single-file-component-4.vue:9:13:9:22 | (h) => { } |
@@ -56,6 +59,7 @@ instance_getOption
5659
| tst.js:99:2:104:3 | new Vue ... \\t\\t}\\n\\t}) | data | tst.js:100:9:100:21 | { dataA: 42 } |
5760
| tst.js:99:2:104:3 | new Vue ... \\t\\t}\\n\\t}) | methods | tst.js:101:12:103:3 | {\\n\\t\\t\\tm: ... ; }\\n\\t\\t} |
5861
instance
62+
| compont-with-route.vue:0:0:0:0 | compont-with-route.vue |
5963
| single-component-file-1.vue:0:0:0:0 | single-component-file-1.vue |
6064
| single-file-component-2.vue:0:0:0:0 | single-file-component-2.vue |
6165
| single-file-component-3.vue:0:0:0:0 | single-file-component-3.vue |
@@ -80,6 +84,10 @@ instance_heapStep
8084
| tst.js:102:20:102:29 | this.dataA | tst.js:100:18:100:19 | 42 | tst.js:102:20:102:29 | this.dataA |
8185
| tst.js:102:20:102:29 | this.dataA | tst.js:102:20:102:23 | this | tst.js:102:20:102:29 | this.dataA |
8286
templateElement
87+
| compont-with-route.vue:1:1:3:11 | <template>...</> |
88+
| compont-with-route.vue:2:5:51:9 | <p>...</> |
89+
| compont-with-route.vue:4:1:49:9 | <script>...</> |
90+
| compont-with-route.vue:50:1:51:8 | <style>...</> |
8391
| single-component-file-1.vue:1:1:3:11 | <template>...</> |
8492
| single-component-file-1.vue:2:5:10:8 | <p>...</> |
8593
| single-component-file-1.vue:4:1:8:9 | <script>...</> |
@@ -101,15 +109,44 @@ templateElement
101109
| single-file-component-5.vue:4:1:16:9 | <script>...</> |
102110
| single-file-component-5.vue:17:1:18:8 | <style>...</> |
103111
vhtmlSourceWrite
112+
| compont-with-route.vue:31:14:31:34 | this.$r ... ery.foo | compont-with-route.vue:31:14:31:30 | this.$route.query | compont-with-route.vue:31:14:31:34 | this.$r ... ery.foo |
113+
| compont-with-route.vue:31:14:31:34 | this.$r ... ery.foo | compont-with-route.vue:31:14:31:34 | this.$r ... ery.foo | compont-with-route.vue:2:8:2:21 | v-html=dataA |
104114
| single-component-file-1.vue:6:40:6:41 | 42 | single-component-file-1.vue:6:40:6:41 | 42 | single-component-file-1.vue:2:8:2:21 | v-html=dataA |
105115
| single-file-component-3-script.js:4:37:4:38 | 42 | single-file-component-3-script.js:4:37:4:38 | 42 | single-file-component-3.vue:2:8:2:21 | v-html=dataA |
106116
| single-file-component-4.vue:15:14:15:15 | 42 | single-file-component-4.vue:15:14:15:15 | 42 | single-file-component-4.vue:2:8:2:21 | v-html=dataA |
107117
| single-file-component-5.vue:13:14:13:15 | 42 | single-file-component-5.vue:13:14:13:15 | 42 | single-file-component-5.vue:2:8:2:21 | v-html=dataA |
108118
xssSink
119+
| compont-with-route.vue:2:8:2:21 | v-html=dataA |
109120
| single-component-file-1.vue:2:8:2:21 | v-html=dataA |
110121
| single-file-component-2.vue:2:8:2:21 | v-html=dataA |
111122
| single-file-component-3.vue:2:8:2:21 | v-html=dataA |
112123
| single-file-component-4.vue:2:8:2:21 | v-html=dataA |
113124
| single-file-component-5.vue:2:8:2:21 | v-html=dataA |
114125
| tst.js:5:13:5:13 | a |
115126
| tst.js:38:12:38:17 | danger |
127+
remoteFlowSource
128+
| compont-with-route.vue:13:17:13:21 | newId |
129+
| compont-with-route.vue:13:24:13:28 | oldId |
130+
| compont-with-route.vue:22:7:22:24 | this.$route.params |
131+
| compont-with-route.vue:23:7:23:23 | this.$route.query |
132+
| compont-with-route.vue:24:7:24:22 | this.$route.hash |
133+
| compont-with-route.vue:25:7:25:22 | this.$route.path |
134+
| compont-with-route.vue:26:7:26:26 | this.$route.fullPath |
135+
| compont-with-route.vue:27:7:27:31 | router. ... e.query |
136+
| compont-with-route.vue:31:14:31:30 | this.$route.query |
137+
| compont-with-route.vue:35:7:35:14 | to.query |
138+
| compont-with-route.vue:36:7:36:16 | from.query |
139+
| compont-with-route.vue:40:7:40:14 | to.query |
140+
| compont-with-route.vue:41:7:41:16 | from.query |
141+
| compont-with-route.vue:45:7:45:14 | to.query |
142+
| compont-with-route.vue:46:7:46:16 | from.query |
143+
| router.js:8:17:8:24 | to.query |
144+
| router.js:9:17:9:26 | from.query |
145+
| router.js:15:25:15:32 | to.query |
146+
| router.js:16:25:16:34 | from.query |
147+
| router.js:23:9:23:16 | to.query |
148+
| router.js:24:9:24:18 | from.query |
149+
| router.js:29:5:29:12 | to.query |
150+
| router.js:30:5:30:14 | from.query |
151+
| router.js:34:5:34:12 | to.query |
152+
| router.js:35:5:35:14 | from.query |

javascript/ql/test/library-tests/frameworks/Vue/tests.ql

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,5 @@ query predicate vhtmlSourceWrite(
2929
import semmle.javascript.security.dataflow.DomBasedXss
3030

3131
query predicate xssSink(DomBasedXss::Sink s) { any() }
32+
33+
query RemoteFlowSource remoteFlowSource() { any() }

0 commit comments

Comments
 (0)