mirror of
https://github.com/github/codeql.git
synced 2026-04-30 19:26:02 +02:00
Merge pull request #941 from esben-semmle/js/vue-support-2
JS: Vue security improvements
This commit is contained in:
@@ -40,7 +40,8 @@ module DataFlow {
|
||||
} or
|
||||
TDestructuredModuleImportNode(ImportDeclaration decl) {
|
||||
exists(decl.getASpecifier().getImportedName())
|
||||
}
|
||||
} or
|
||||
THtmlAttributeNode(HTML::Attribute attr)
|
||||
|
||||
/**
|
||||
* A node in the data flow graph.
|
||||
@@ -115,7 +116,9 @@ module DataFlow {
|
||||
int getIntValue() { result = asExpr().getIntValue() }
|
||||
|
||||
/** Gets a function value that may reach this node. */
|
||||
FunctionNode getAFunctionValue() { result.getAstNode() = analyze().getAValue().(AbstractCallable).getFunction() }
|
||||
FunctionNode getAFunctionValue() {
|
||||
result.getAstNode() = analyze().getAValue().(AbstractCallable).getFunction()
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if this expression may refer to the initial value of parameter `p`.
|
||||
@@ -738,6 +741,26 @@ module DataFlow {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A data flow node representing an HTML attribute.
|
||||
*/
|
||||
class HtmlAttributeNode extends DataFlow::Node, THtmlAttributeNode {
|
||||
HTML::Attribute attr;
|
||||
|
||||
HtmlAttributeNode() { this = THtmlAttributeNode(attr) }
|
||||
|
||||
override string toString() { result = attr.toString() }
|
||||
|
||||
override predicate hasLocationInfo(
|
||||
string filepath, int startline, int startcolumn, int endline, int endcolumn
|
||||
) {
|
||||
attr.getLocation().hasLocationInfo(filepath, startline, startcolumn, endline, endcolumn)
|
||||
}
|
||||
|
||||
/** Gets the attribute corresponding to this data flow node. */
|
||||
HTML::Attribute getAttribute() { result = attr }
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides classes representing various kinds of calls.
|
||||
*
|
||||
@@ -1134,7 +1157,7 @@ module DataFlow {
|
||||
nd.asExpr() instanceof ExternalModuleReference and
|
||||
cause = "import"
|
||||
or
|
||||
exists (Expr e | e = nd.asExpr() and cause = "heap" |
|
||||
exists(Expr e | e = nd.asExpr() and cause = "heap" |
|
||||
e instanceof PropAccess or
|
||||
e instanceof E4X::XMLAnyName or
|
||||
e instanceof E4X::XMLAttributeSelector or
|
||||
|
||||
@@ -94,6 +94,11 @@ module Vue {
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the template element used by this instance, if any.
|
||||
*/
|
||||
abstract Template::Element getTemplateElement();
|
||||
|
||||
/**
|
||||
* Gets the node for the `data` option object of this instance.
|
||||
*/
|
||||
@@ -245,6 +250,8 @@ module Vue {
|
||||
}
|
||||
|
||||
override DataFlow::Node getOwnOption(string name) { result = def.getOptionArgument(0, name) }
|
||||
|
||||
override Template::Element getTemplateElement() { none() }
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -264,6 +271,8 @@ module Vue {
|
||||
}
|
||||
|
||||
override DataFlow::Node getOwnOption(string name) { result = extend.getOptionArgument(0, name) }
|
||||
|
||||
override Template::Element getTemplateElement() { none() }
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -291,6 +300,8 @@ module Vue {
|
||||
or
|
||||
result = MkExtendedVue(extend).(ExtendedVue).getOption(name)
|
||||
}
|
||||
|
||||
override Template::Element getTemplateElement() { none() }
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -310,6 +321,8 @@ module Vue {
|
||||
}
|
||||
|
||||
override DataFlow::Node getOwnOption(string name) { result = def.getOptionArgument(1, name) }
|
||||
|
||||
override Template::Element getTemplateElement() { none() }
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -320,6 +333,14 @@ module Vue {
|
||||
|
||||
SingleFileComponent() { this = MkSingleFileComponent(file) }
|
||||
|
||||
override Template::Element getTemplateElement() {
|
||||
exists(HTML::Element e | result.(Template::HtmlElement).getElement() = e |
|
||||
e.getFile() = file and
|
||||
e.getName() = "template" and
|
||||
e.isTopLevel()
|
||||
)
|
||||
}
|
||||
|
||||
override predicate hasLocationInfo(
|
||||
string filepath, int startline, int startcolumn, int endline, int endcolumn
|
||||
) {
|
||||
@@ -366,4 +387,85 @@ module Vue {
|
||||
class VueFile extends File {
|
||||
VueFile() { getExtension() = "vue" }
|
||||
}
|
||||
|
||||
/**
|
||||
* A taint propagating data flow edge through a Vue instance property.
|
||||
*/
|
||||
class InstanceHeapStep extends TaintTracking::AdditionalTaintStep {
|
||||
DataFlow::Node src;
|
||||
|
||||
InstanceHeapStep() {
|
||||
exists(Instance i, string name, DataFlow::FunctionNode bound |
|
||||
bound.flowsTo(i.getABoundFunction()) and
|
||||
not bound.getFunction() instanceof ArrowFunctionExpr and
|
||||
bound.getReceiver().getAPropertyRead(name) = this and
|
||||
src = i.getAPropertyValue(name)
|
||||
)
|
||||
}
|
||||
|
||||
override predicate step(DataFlow::Node pred, DataFlow::Node succ) { pred = src and succ = this }
|
||||
}
|
||||
|
||||
/*
|
||||
* Provides classes for working with Vue templates.
|
||||
*/
|
||||
|
||||
module Template {
|
||||
// Currently only supports HTML elements, but it may be possible to parse simple string templates later
|
||||
private newtype TElement = MkHtmlElement(HTML::Element e) { e.getFile() instanceof VueFile }
|
||||
|
||||
/**
|
||||
* An element of a template.
|
||||
*/
|
||||
abstract class Element extends TElement {
|
||||
/** Gets a textual representation of this element. */
|
||||
string toString() { result = "<" + getName() + ">...</>" }
|
||||
|
||||
/**
|
||||
* Holds if this element is at the specified location.
|
||||
* The location spans column `startcolumn` of line `startline` to
|
||||
* column `endcolumn` of line `endline` in file `filepath`.
|
||||
* For more information, see
|
||||
* [locations](https://help.semmle.com/QL/learn-ql/ql/locations.html).
|
||||
*/
|
||||
predicate hasLocationInfo(
|
||||
string filepath, int startline, int startcolumn, int endline, int endcolumn
|
||||
) {
|
||||
filepath = "" and
|
||||
startline = 0 and
|
||||
startcolumn = 0 and
|
||||
endline = 0 and
|
||||
endcolumn = 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the name of this element.
|
||||
*
|
||||
* For example, the name of `<br>` is `br`.
|
||||
*/
|
||||
abstract string getName();
|
||||
}
|
||||
|
||||
/**
|
||||
* An HTML element as a template element.
|
||||
*/
|
||||
class HtmlElement extends Element, MkHtmlElement {
|
||||
HTML::Element elem;
|
||||
|
||||
HtmlElement() { this = MkHtmlElement(elem) }
|
||||
|
||||
override predicate hasLocationInfo(
|
||||
string filepath, int startline, int startcolumn, int endline, int endcolumn
|
||||
) {
|
||||
elem.getLocation().hasLocationInfo(filepath, startline, startcolumn, endline, endcolumn)
|
||||
}
|
||||
|
||||
override string getName() { result = elem.getName() }
|
||||
|
||||
/**
|
||||
* Gets the HTML element of this element.
|
||||
*/
|
||||
HTML::Element getElement() { result = elem }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -198,6 +198,51 @@ module DomBasedXss {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A Vue `v-html` attribute, viewed as an XSS sink.
|
||||
*/
|
||||
class VHtmlSink extends DomBasedXss::Sink {
|
||||
HTML::Attribute attr;
|
||||
|
||||
VHtmlSink() {
|
||||
this.(DataFlow::HtmlAttributeNode).getAttribute() = attr and attr.getName() = "v-html"
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the HTML attribute of this sink.
|
||||
*/
|
||||
HTML::Attribute getAttr() { result = attr }
|
||||
}
|
||||
|
||||
/**
|
||||
* A taint propagating data flow edge through a string interpolation of a
|
||||
* Vue instance property to a `v-html` attribute.
|
||||
*
|
||||
* As an example, `<div v-html="prop"/>` reads the `prop` property
|
||||
* of `inst = new Vue({ ..., data: { prop: source } })`, if the
|
||||
* `div` element is part of the template for `inst`.
|
||||
*/
|
||||
class VHtmlSourceWrite extends TaintTracking::AdditionalTaintStep {
|
||||
VHtmlSink attr;
|
||||
|
||||
VHtmlSourceWrite() {
|
||||
exists(Vue::Instance instance, string expr |
|
||||
attr.getAttr().getRoot() = instance
|
||||
.getTemplateElement()
|
||||
.(Vue::Template::HtmlElement)
|
||||
.getElement() and
|
||||
expr = attr.getAttr().getValue() and
|
||||
// only support for simple identifier expressions
|
||||
expr.regexpMatch("(?i)[a-z0-9_]+") and
|
||||
this = instance.getAPropertyValue(expr)
|
||||
)
|
||||
}
|
||||
|
||||
override predicate step(DataFlow::Node pred, DataFlow::Node succ) {
|
||||
pred = this and succ = attr
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A regexp replacement involving an HTML meta-character, viewed as a sanitizer for
|
||||
* XSS vulnerabilities.
|
||||
|
||||
@@ -15,3 +15,4 @@
|
||||
| tst.js:77:20:83:2 | Vue.ext ... \\n }\\n}) |
|
||||
| tst.js:85:1:87:2 | new Vue ... e; }\\n}) |
|
||||
| tst.js:94:2:96:3 | new Vue ... f,\\n\\t}) |
|
||||
| tst.js:99:2:104:3 | new Vue ... \\t\\t}\\n\\t}) |
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
| tst.js:102:20:102:29 | this.dataA | tst.js:100:18:100:19 | 42 | tst.js:102:20:102:29 | this.dataA |
|
||||
| tst.js:102:20:102:29 | this.dataA | tst.js:102:20:102:23 | this | tst.js:102:20:102:29 | this.dataA |
|
||||
@@ -0,0 +1,5 @@
|
||||
import javascript
|
||||
|
||||
from Vue::InstanceHeapStep step, DataFlow::Node pred, DataFlow::Node succ
|
||||
where step.step(pred, succ)
|
||||
select step, pred, succ
|
||||
@@ -20,3 +20,4 @@
|
||||
| tst.js:77:20:83:2 | Vue.ext ... \\n }\\n}) | deadExtended | tst.js:80:21:80:22 | 42 |
|
||||
| tst.js:85:1:87:2 | new Vue ... e; }\\n}) | created | tst.js:86:38:86:41 | true |
|
||||
| tst.js:94:2:96:3 | new Vue ... f,\\n\\t}) | dataA | tst.js:89:22:89:23 | 42 |
|
||||
| tst.js:99:2:104:3 | new Vue ... \\t\\t}\\n\\t}) | dataA | tst.js:100:18:100:19 | 42 |
|
||||
|
||||
@@ -23,3 +23,5 @@
|
||||
| tst.js:77:20:83:2 | Vue.ext ... \\n }\\n}) | data | tst.js:78:9:82:3 | functio ... };\\n } |
|
||||
| tst.js:85:1:87:2 | new Vue ... e; }\\n}) | created | tst.js:86:11:86:44 | functio ... true; } |
|
||||
| tst.js:94:2:96:3 | new Vue ... f,\\n\\t}) | data | tst.js:95:9:95:9 | f |
|
||||
| tst.js:99:2:104:3 | new Vue ... \\t\\t}\\n\\t}) | data | tst.js:100:9:100:21 | { dataA: 42 } |
|
||||
| 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} |
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
| single-component-file-1.vue:1:1:3:11 | <template>...</> |
|
||||
| single-component-file-1.vue:2:5:10:8 | <p>...</> |
|
||||
| single-component-file-1.vue:4:1:8:9 | <script>...</> |
|
||||
| single-component-file-1.vue:9:1:10:8 | <style>...</> |
|
||||
| single-file-component-2.vue:1:1:3:11 | <template>...</> |
|
||||
| single-file-component-2.vue:2:5:11:8 | <p>...</> |
|
||||
| single-file-component-2.vue:4:1:9:9 | <script>...</> |
|
||||
| single-file-component-2.vue:10:1:11:8 | <style>...</> |
|
||||
| single-file-component-3.vue:1:1:3:11 | <template>...</> |
|
||||
| single-file-component-3.vue:2:5:7:8 | <p>...</> |
|
||||
| single-file-component-3.vue:4:1:5:9 | <script>...</> |
|
||||
| single-file-component-3.vue:6:1:7:8 | <style>...</> |
|
||||
@@ -0,0 +1,3 @@
|
||||
import javascript
|
||||
|
||||
select any(Vue::Template::Element e)
|
||||
@@ -0,0 +1,2 @@
|
||||
| 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 |
|
||||
| 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 |
|
||||
@@ -0,0 +1,6 @@
|
||||
import javascript
|
||||
import semmle.javascript.security.dataflow.DomBasedXss
|
||||
|
||||
from DomBasedXss::VHtmlSourceWrite w, DataFlow::Node pred, DataFlow::Node succ
|
||||
where w.step(pred, succ)
|
||||
select w, pred, succ
|
||||
@@ -1,2 +1,5 @@
|
||||
| single-component-file-1.vue:2:8:2:21 | v-html=dataA |
|
||||
| single-file-component-2.vue:2:8:2:21 | v-html=dataA |
|
||||
| single-file-component-3.vue:2:8:2:21 | v-html=dataA |
|
||||
| tst.js:5:13:5:13 | a |
|
||||
| tst.js:38:12:38:17 | danger |
|
||||
|
||||
@@ -95,3 +95,11 @@ new Vue({
|
||||
data: f,
|
||||
});
|
||||
});
|
||||
(function() {
|
||||
new Vue({
|
||||
data: { dataA: 42 },
|
||||
methods: {
|
||||
m: function() { this.dataA; }
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -178,6 +178,8 @@ nodes
|
||||
| tst.js:282:9:282:29 | tainted |
|
||||
| tst.js:282:19:282:29 | window.name |
|
||||
| tst.js:285:59:285:65 | tainted |
|
||||
| v-html.vue:2:8:2:23 | v-html=tainted |
|
||||
| v-html.vue:6:42:6:58 | document.location |
|
||||
| winjs.js:2:7:2:53 | tainted |
|
||||
| winjs.js:2:17:2:33 | document.location |
|
||||
| winjs.js:2:17:2:40 | documen ... .search |
|
||||
@@ -318,6 +320,7 @@ edges
|
||||
| tst.js:272:16:272:32 | document.location | tst.js:272:9:272:32 | loc3 |
|
||||
| tst.js:282:9:282:29 | tainted | tst.js:285:59:285:65 | tainted |
|
||||
| tst.js:282:19:282:29 | window.name | tst.js:282:9:282:29 | tainted |
|
||||
| v-html.vue:6:42:6:58 | document.location | v-html.vue:2:8:2:23 | v-html=tainted |
|
||||
| winjs.js:2:7:2:53 | tainted | winjs.js:3:43:3:49 | tainted |
|
||||
| winjs.js:2:7:2:53 | tainted | winjs.js:4:43:4:49 | tainted |
|
||||
| winjs.js:2:17:2:33 | document.location | winjs.js:2:17:2:40 | documen ... .search |
|
||||
@@ -394,5 +397,6 @@ edges
|
||||
| tst.js:285:59:285:65 | tainted | tst.js:282:9:282:29 | tainted | tst.js:285:59:285:65 | tainted | Cross-site scripting vulnerability due to $@. | tst.js:282:9:282:29 | tainted | user-provided value |
|
||||
| tst.js:285:59:285:65 | tainted | tst.js:282:19:282:29 | window.name | tst.js:285:59:285:65 | tainted | Cross-site scripting vulnerability due to $@. | tst.js:282:19:282:29 | window.name | user-provided value |
|
||||
| tst.js:285:59:285:65 | tainted | tst.js:285:59:285:65 | tainted | tst.js:285:59:285:65 | tainted | Cross-site scripting vulnerability due to $@. | tst.js:285:59:285:65 | tainted | user-provided value |
|
||||
| v-html.vue:2:8:2:23 | v-html=tainted | v-html.vue:6:42:6:58 | document.location | v-html.vue:2:8:2:23 | v-html=tainted | Cross-site scripting vulnerability due to $@. | v-html.vue:6:42:6:58 | document.location | user-provided value |
|
||||
| winjs.js:3:43:3:49 | tainted | winjs.js:2:17:2:33 | document.location | winjs.js:3:43:3:49 | tainted | Cross-site scripting vulnerability due to $@. | winjs.js:2:17:2:33 | document.location | user-provided value |
|
||||
| winjs.js:4:43:4:49 | tainted | winjs.js:2:17:2:33 | document.location | winjs.js:4:43:4:49 | tainted | Cross-site scripting vulnerability due to $@. | winjs.js:2:17:2:33 | document.location | user-provided value |
|
||||
|
||||
10
javascript/ql/test/query-tests/Security/CWE-079/v-html.vue
Normal file
10
javascript/ql/test/query-tests/Security/CWE-079/v-html.vue
Normal file
@@ -0,0 +1,10 @@
|
||||
<template>
|
||||
<p v-html="tainted"/>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
data: function() { return { tainted: document.location } }
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
</style>
|
||||
Reference in New Issue
Block a user