Skip to content

Commit db5fbe2

Browse files
author
Max Schaefer
authored
Merge pull request #941 from esben-semmle/js/vue-support-2
JS: Vue security improvements
2 parents 86e646b + b0358d7 commit db5fbe2

File tree

16 files changed

+232
-3
lines changed

16 files changed

+232
-3
lines changed

javascript/ql/src/semmle/javascript/dataflow/DataFlow.qll

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,8 @@ module DataFlow {
4040
} or
4141
TDestructuredModuleImportNode(ImportDeclaration decl) {
4242
exists(decl.getASpecifier().getImportedName())
43-
}
43+
} or
44+
THtmlAttributeNode(HTML::Attribute attr)
4445

4546
/**
4647
* A node in the data flow graph.
@@ -115,7 +116,9 @@ module DataFlow {
115116
int getIntValue() { result = asExpr().getIntValue() }
116117

117118
/** Gets a function value that may reach this node. */
118-
FunctionNode getAFunctionValue() { result.getAstNode() = analyze().getAValue().(AbstractCallable).getFunction() }
119+
FunctionNode getAFunctionValue() {
120+
result.getAstNode() = analyze().getAValue().(AbstractCallable).getFunction()
121+
}
119122

120123
/**
121124
* Holds if this expression may refer to the initial value of parameter `p`.
@@ -738,6 +741,26 @@ module DataFlow {
738741
}
739742
}
740743

744+
/**
745+
* A data flow node representing an HTML attribute.
746+
*/
747+
class HtmlAttributeNode extends DataFlow::Node, THtmlAttributeNode {
748+
HTML::Attribute attr;
749+
750+
HtmlAttributeNode() { this = THtmlAttributeNode(attr) }
751+
752+
override string toString() { result = attr.toString() }
753+
754+
override predicate hasLocationInfo(
755+
string filepath, int startline, int startcolumn, int endline, int endcolumn
756+
) {
757+
attr.getLocation().hasLocationInfo(filepath, startline, startcolumn, endline, endcolumn)
758+
}
759+
760+
/** Gets the attribute corresponding to this data flow node. */
761+
HTML::Attribute getAttribute() { result = attr }
762+
}
763+
741764
/**
742765
* Provides classes representing various kinds of calls.
743766
*
@@ -1134,7 +1157,7 @@ module DataFlow {
11341157
nd.asExpr() instanceof ExternalModuleReference and
11351158
cause = "import"
11361159
or
1137-
exists (Expr e | e = nd.asExpr() and cause = "heap" |
1160+
exists(Expr e | e = nd.asExpr() and cause = "heap" |
11381161
e instanceof PropAccess or
11391162
e instanceof E4X::XMLAnyName or
11401163
e instanceof E4X::XMLAttributeSelector or

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

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,11 @@ module Vue {
9494
)
9595
}
9696

97+
/**
98+
* Gets the template element used by this instance, if any.
99+
*/
100+
abstract Template::Element getTemplateElement();
101+
97102
/**
98103
* Gets the node for the `data` option object of this instance.
99104
*/
@@ -245,6 +250,8 @@ module Vue {
245250
}
246251

247252
override DataFlow::Node getOwnOption(string name) { result = def.getOptionArgument(0, name) }
253+
254+
override Template::Element getTemplateElement() { none() }
248255
}
249256

250257
/**
@@ -264,6 +271,8 @@ module Vue {
264271
}
265272

266273
override DataFlow::Node getOwnOption(string name) { result = extend.getOptionArgument(0, name) }
274+
275+
override Template::Element getTemplateElement() { none() }
267276
}
268277

269278
/**
@@ -291,6 +300,8 @@ module Vue {
291300
or
292301
result = MkExtendedVue(extend).(ExtendedVue).getOption(name)
293302
}
303+
304+
override Template::Element getTemplateElement() { none() }
294305
}
295306

296307
/**
@@ -310,6 +321,8 @@ module Vue {
310321
}
311322

312323
override DataFlow::Node getOwnOption(string name) { result = def.getOptionArgument(1, name) }
324+
325+
override Template::Element getTemplateElement() { none() }
313326
}
314327

315328
/**
@@ -320,6 +333,14 @@ module Vue {
320333

321334
SingleFileComponent() { this = MkSingleFileComponent(file) }
322335

336+
override Template::Element getTemplateElement() {
337+
exists(HTML::Element e | result.(Template::HtmlElement).getElement() = e |
338+
e.getFile() = file and
339+
e.getName() = "template" and
340+
e.isTopLevel()
341+
)
342+
}
343+
323344
override predicate hasLocationInfo(
324345
string filepath, int startline, int startcolumn, int endline, int endcolumn
325346
) {
@@ -366,4 +387,85 @@ module Vue {
366387
class VueFile extends File {
367388
VueFile() { getExtension() = "vue" }
368389
}
390+
391+
/**
392+
* A taint propagating data flow edge through a Vue instance property.
393+
*/
394+
class InstanceHeapStep extends TaintTracking::AdditionalTaintStep {
395+
DataFlow::Node src;
396+
397+
InstanceHeapStep() {
398+
exists(Instance i, string name, DataFlow::FunctionNode bound |
399+
bound.flowsTo(i.getABoundFunction()) and
400+
not bound.getFunction() instanceof ArrowFunctionExpr and
401+
bound.getReceiver().getAPropertyRead(name) = this and
402+
src = i.getAPropertyValue(name)
403+
)
404+
}
405+
406+
override predicate step(DataFlow::Node pred, DataFlow::Node succ) { pred = src and succ = this }
407+
}
408+
409+
/*
410+
* Provides classes for working with Vue templates.
411+
*/
412+
413+
module Template {
414+
// Currently only supports HTML elements, but it may be possible to parse simple string templates later
415+
private newtype TElement = MkHtmlElement(HTML::Element e) { e.getFile() instanceof VueFile }
416+
417+
/**
418+
* An element of a template.
419+
*/
420+
abstract class Element extends TElement {
421+
/** Gets a textual representation of this element. */
422+
string toString() { result = "<" + getName() + ">...</>" }
423+
424+
/**
425+
* Holds if this element is at the specified location.
426+
* The location spans column `startcolumn` of line `startline` to
427+
* column `endcolumn` of line `endline` in file `filepath`.
428+
* For more information, see
429+
* [locations](https://help.semmle.com/QL/learn-ql/ql/locations.html).
430+
*/
431+
predicate hasLocationInfo(
432+
string filepath, int startline, int startcolumn, int endline, int endcolumn
433+
) {
434+
filepath = "" and
435+
startline = 0 and
436+
startcolumn = 0 and
437+
endline = 0 and
438+
endcolumn = 0
439+
}
440+
441+
/**
442+
* Gets the name of this element.
443+
*
444+
* For example, the name of `<br>` is `br`.
445+
*/
446+
abstract string getName();
447+
}
448+
449+
/**
450+
* An HTML element as a template element.
451+
*/
452+
class HtmlElement extends Element, MkHtmlElement {
453+
HTML::Element elem;
454+
455+
HtmlElement() { this = MkHtmlElement(elem) }
456+
457+
override predicate hasLocationInfo(
458+
string filepath, int startline, int startcolumn, int endline, int endcolumn
459+
) {
460+
elem.getLocation().hasLocationInfo(filepath, startline, startcolumn, endline, endcolumn)
461+
}
462+
463+
override string getName() { result = elem.getName() }
464+
465+
/**
466+
* Gets the HTML element of this element.
467+
*/
468+
HTML::Element getElement() { result = elem }
469+
}
470+
}
369471
}

javascript/ql/src/semmle/javascript/security/dataflow/Xss.qll

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,51 @@ module DomBasedXss {
198198
}
199199
}
200200

201+
/**
202+
* A Vue `v-html` attribute, viewed as an XSS sink.
203+
*/
204+
class VHtmlSink extends DomBasedXss::Sink {
205+
HTML::Attribute attr;
206+
207+
VHtmlSink() {
208+
this.(DataFlow::HtmlAttributeNode).getAttribute() = attr and attr.getName() = "v-html"
209+
}
210+
211+
/**
212+
* Gets the HTML attribute of this sink.
213+
*/
214+
HTML::Attribute getAttr() { result = attr }
215+
}
216+
217+
/**
218+
* A taint propagating data flow edge through a string interpolation of a
219+
* Vue instance property to a `v-html` attribute.
220+
*
221+
* As an example, `<div v-html="prop"/>` reads the `prop` property
222+
* of `inst = new Vue({ ..., data: { prop: source } })`, if the
223+
* `div` element is part of the template for `inst`.
224+
*/
225+
class VHtmlSourceWrite extends TaintTracking::AdditionalTaintStep {
226+
VHtmlSink attr;
227+
228+
VHtmlSourceWrite() {
229+
exists(Vue::Instance instance, string expr |
230+
attr.getAttr().getRoot() = instance
231+
.getTemplateElement()
232+
.(Vue::Template::HtmlElement)
233+
.getElement() and
234+
expr = attr.getAttr().getValue() and
235+
// only support for simple identifier expressions
236+
expr.regexpMatch("(?i)[a-z0-9_]+") and
237+
this = instance.getAPropertyValue(expr)
238+
)
239+
}
240+
241+
override predicate step(DataFlow::Node pred, DataFlow::Node succ) {
242+
pred = this and succ = attr
243+
}
244+
}
245+
201246
/**
202247
* A regexp replacement involving an HTML meta-character, viewed as a sanitizer for
203248
* XSS vulnerabilities.

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,4 @@
1515
| tst.js:77:20:83:2 | Vue.ext ... \\n }\\n}) |
1616
| tst.js:85:1:87:2 | new Vue ... e; }\\n}) |
1717
| tst.js:94:2:96:3 | new Vue ... f,\\n\\t}) |
18+
| tst.js:99:2:104:3 | new Vue ... \\t\\t}\\n\\t}) |
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
| tst.js:102:20:102:29 | this.dataA | tst.js:100:18:100:19 | 42 | tst.js:102:20:102:29 | this.dataA |
2+
| tst.js:102:20:102:29 | this.dataA | tst.js:102:20:102:23 | this | tst.js:102:20:102:29 | this.dataA |
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import javascript
2+
3+
from Vue::InstanceHeapStep step, DataFlow::Node pred, DataFlow::Node succ
4+
where step.step(pred, succ)
5+
select step, pred, succ

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,4 @@
2020
| tst.js:77:20:83:2 | Vue.ext ... \\n }\\n}) | deadExtended | tst.js:80:21:80:22 | 42 |
2121
| tst.js:85:1:87:2 | new Vue ... e; }\\n}) | created | tst.js:86:38:86:41 | true |
2222
| tst.js:94:2:96:3 | new Vue ... f,\\n\\t}) | dataA | tst.js:89:22:89:23 | 42 |
23+
| tst.js:99:2:104:3 | new Vue ... \\t\\t}\\n\\t}) | dataA | tst.js:100:18:100:19 | 42 |

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,5 @@
2323
| tst.js:77:20:83:2 | Vue.ext ... \\n }\\n}) | data | tst.js:78:9:82:3 | functio ... };\\n } |
2424
| tst.js:85:1:87:2 | new Vue ... e; }\\n}) | created | tst.js:86:11:86:44 | functio ... true; } |
2525
| tst.js:94:2:96:3 | new Vue ... f,\\n\\t}) | data | tst.js:95:9:95:9 | f |
26+
| tst.js:99:2:104:3 | new Vue ... \\t\\t}\\n\\t}) | data | tst.js:100:9:100:21 | { dataA: 42 } |
27+
| 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} |
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
| single-component-file-1.vue:1:1:3:11 | <template>...</> |
2+
| single-component-file-1.vue:2:5:10:8 | <p>...</> |
3+
| single-component-file-1.vue:4:1:8:9 | <script>...</> |
4+
| single-component-file-1.vue:9:1:10:8 | <style>...</> |
5+
| single-file-component-2.vue:1:1:3:11 | <template>...</> |
6+
| single-file-component-2.vue:2:5:11:8 | <p>...</> |
7+
| single-file-component-2.vue:4:1:9:9 | <script>...</> |
8+
| single-file-component-2.vue:10:1:11:8 | <style>...</> |
9+
| single-file-component-3.vue:1:1:3:11 | <template>...</> |
10+
| single-file-component-3.vue:2:5:7:8 | <p>...</> |
11+
| single-file-component-3.vue:4:1:5:9 | <script>...</> |
12+
| single-file-component-3.vue:6:1:7:8 | <style>...</> |
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import javascript
2+
3+
select any(Vue::Template::Element e)

0 commit comments

Comments
 (0)