diff --git a/javascript/ql/lib/ext/default-threat-models-fixup.model.yml b/javascript/ql/lib/ext/default-threat-models-fixup.model.yml index 31363571544b..185c12392d4c 100644 --- a/javascript/ql/lib/ext/default-threat-models-fixup.model.yml +++ b/javascript/ql/lib/ext/default-threat-models-fixup.model.yml @@ -6,3 +6,4 @@ extensions: # Since responses are enabled by default in the shared threat-models configuration, # we need to disable it here to keep existing behavior for the javascript analysis. - ["response", false, -2147483647] + - ["view-component-input", true, 100] # For testing diff --git a/javascript/ql/lib/semmle/javascript/ViewComponentInput.qll b/javascript/ql/lib/semmle/javascript/ViewComponentInput.qll new file mode 100644 index 000000000000..bc80826de5c9 --- /dev/null +++ b/javascript/ql/lib/semmle/javascript/ViewComponentInput.qll @@ -0,0 +1,47 @@ +/** + * Provides a classes and predicates for contributing to the `view-component-input` threat model. + */ + +private import javascript + +/** + * An input to a view component, such as React props. + */ +abstract class ViewComponentInput extends DataFlow::Node { + /** Gets a string that describes the type of this threat-model source. */ + abstract string getSourceType(); +} + +private class ViewComponentInputAsThreatModelSource extends ThreatModelSource::Range instanceof ViewComponentInput +{ + ViewComponentInputAsThreatModelSource() { not isSafeType(this.asExpr().getType()) } + + final override string getThreatModel() { result = "view-component-input" } + + final override string getSourceType() { result = ViewComponentInput.super.getSourceType() } +} + +private predicate isSafeType(Type t) { + t instanceof NumberLikeType + or + t instanceof BooleanLikeType + or + t instanceof UndefinedType + or + t instanceof NullType + or + t instanceof VoidType + or + hasSafeTypes(t, t.(UnionType).getNumElementType()) + or + isSafeType(t.(IntersectionType).getAnElementType()) +} + +/** Hold if the first `n` components of `t` are safe types. */ +private predicate hasSafeTypes(UnionType t, int n) { + isSafeType(t.getElementType(0)) and + n = 1 + or + isSafeType(t.getElementType(n - 1)) and + hasSafeTypes(t, n - 1) +} diff --git a/javascript/ql/lib/semmle/javascript/frameworks/Angular2.qll b/javascript/ql/lib/semmle/javascript/frameworks/Angular2.qll index 16430ff0475a..11b96ba03c5a 100644 --- a/javascript/ql/lib/semmle/javascript/frameworks/Angular2.qll +++ b/javascript/ql/lib/semmle/javascript/frameworks/Angular2.qll @@ -8,6 +8,7 @@ private import semmle.javascript.security.dataflow.CodeInjectionCustomizations private import semmle.javascript.security.dataflow.ClientSideUrlRedirectCustomizations private import semmle.javascript.DynamicPropertyAccess private import semmle.javascript.dataflow.internal.PreCallGraphStep +private import semmle.javascript.ViewComponentInput /** * Provides classes for working with Angular (also known as Angular 2.x) applications. @@ -554,4 +555,17 @@ module Angular2 { this = API::Node::ofType("@angular/core", "ElementRef").getMember("nativeElement").asSource() } } + + private class InputFieldAsViewComponentInput extends ViewComponentInput { + InputFieldAsViewComponentInput() { + this = + API::moduleImport("@angular/core") + .getMember("Input") + .getReturn() + .getADecoratedMember() + .asSource() + } + + override string getSourceType() { result = "Angular component input field" } + } } diff --git a/javascript/ql/lib/semmle/javascript/frameworks/React.qll b/javascript/ql/lib/semmle/javascript/frameworks/React.qll index 64c9f5feb782..5ac0e419e264 100644 --- a/javascript/ql/lib/semmle/javascript/frameworks/React.qll +++ b/javascript/ql/lib/semmle/javascript/frameworks/React.qll @@ -5,6 +5,7 @@ import javascript private import semmle.javascript.dataflow.internal.FlowSteps as FlowSteps private import semmle.javascript.dataflow.internal.PreCallGraphStep +private import semmle.javascript.ViewComponentInput /** * Gets a reference to the 'React' object. @@ -868,3 +869,9 @@ private class PropsFlowStep extends PreCallGraphStep { ) } } + +private class ReactPropAsViewComponentInput extends ViewComponentInput { + ReactPropAsViewComponentInput() { this = any(ReactComponent c).getADirectPropsAccess() } + + override string getSourceType() { result = "React props" } +} diff --git a/javascript/ql/lib/semmle/javascript/frameworks/Vue.qll b/javascript/ql/lib/semmle/javascript/frameworks/Vue.qll index ebfe042f4d06..faba601df52d 100644 --- a/javascript/ql/lib/semmle/javascript/frameworks/Vue.qll +++ b/javascript/ql/lib/semmle/javascript/frameworks/Vue.qll @@ -3,6 +3,7 @@ */ import javascript +import semmle.javascript.ViewComponentInput module Vue { /** The global variable `Vue`, as an API graph entry point. */ @@ -85,17 +86,16 @@ module Vue { * A class with a `@Component` decorator, making it usable as an "options" object in Vue. */ class ClassComponent extends DataFlow::ClassNode { + private ClassDefinition cls; DataFlow::Node decorator; ClassComponent() { - exists(ClassDefinition cls | - this = cls.flow() and - cls.getADecorator().getExpression() = decorator.asExpr() and - ( - componentDecorator().flowsTo(decorator) - or - componentDecorator().getACall() = decorator - ) + this = cls.flow() and + cls.getADecorator().getExpression() = decorator.asExpr() and + ( + componentDecorator().flowsTo(decorator) + or + componentDecorator().getACall() = decorator ) } @@ -105,6 +105,9 @@ module Vue { * These options correspond to the options one would pass to `new Vue({...})` or similar. */ API::Node getDecoratorOptions() { result = decorator.(API::CallNode).getParameter(0) } + + /** Gets the AST node for the class definition. */ + ClassDefinition getClassDefinition() { result = cls } } private string memberKindVerb(DataFlow::MemberKind kind) { @@ -460,6 +463,12 @@ module Vue { SingleFileComponent() { this = MkSingleFileComponent(file) } + /** Gets a call to `defineProps` in this component. */ + DataFlow::CallNode getDefinePropsCall() { + result = DataFlow::globalVarRef("defineProps").getACall() and + result.getFile() = file + } + override Template::Element getTemplateElement() { exists(HTML::Element e | result.(Template::HtmlElement).getElement() = e | e.getFile() = file and @@ -697,4 +706,68 @@ module Vue { override ClientSideRemoteFlowKind getKind() { result = kind } } + + /** + * Holds if the given type annotation indicates a value that is not typically considered taintable. + */ + private predicate isSafeType(TypeAnnotation type) { + type.isBooleany() or + type.isNumbery() or + type.isRawFunction() or + type instanceof FunctionTypeExpr + } + + /** + * Holds if the given field has a type that indicates that is can not contain a taintable value. + */ + private predicate isSafeField(FieldDeclaration field) { isSafeType(field.getTypeAnnotation()) } + + private DataFlow::Node getPropSpec(Component component) { + result = component.getOption("props") + or + result = component.(SingleFileComponent).getDefinePropsCall().getArgument(0) + } + + /** + * Holds if `component` has an input prop with the given name, that is of a taintable type. + */ + private predicate hasTaintableProp(Component component, string name) { + exists(DataFlow::SourceNode spec | spec = getPropSpec(component).getALocalSource() | + spec.(DataFlow::ArrayCreationNode).getAnElement().getStringValue() = name + or + exists(DataFlow::PropWrite write | + write = spec.getAPropertyWrite(name) and + not DataFlow::globalVarRef(["Number", "Boolean"]).flowsTo(write.getRhs()) + ) + ) + or + exists(FieldDeclaration field | + field = component.getAsClassComponent().getClassDefinition().getField(name) and + DataFlow::moduleMember("vue-property-decorator", "Prop") + .getACall() + .flowsToExpr(field.getADecorator().getExpression()) and + not isSafeField(field) + ) + or + // defineProps() can be called with only type arguments and then the Vue compiler will + // infer the prop types. + exists(CallExpr call, FieldDeclaration field | + call = component.(SingleFileComponent).getDefinePropsCall().asExpr() and + field = call.getTypeArgument(0).(InterfaceTypeExpr).getMember(name) and + not isSafeField(field) + ) + } + + private class PropAsViewComponentInput extends ViewComponentInput { + PropAsViewComponentInput() { + exists(Component component, string name | hasTaintableProp(component, name) | + this = component.getAnInstanceRef().getAPropertyRead(name) + or + // defineProps() returns the props + this = component.(SingleFileComponent).getDefinePropsCall().getAPropertyRead(name) + ) + } + + override string getSourceType() { result = "Vue prop" } + } } diff --git a/javascript/ql/src/meta/alerts/TaintSources.ql b/javascript/ql/src/meta/alerts/TaintSources.ql index 95daa2a71e28..327aca84460c 100644 --- a/javascript/ql/src/meta/alerts/TaintSources.ql +++ b/javascript/ql/src/meta/alerts/TaintSources.ql @@ -11,13 +11,6 @@ import javascript import meta.internal.TaintMetrics -string getName(DataFlow::Node node) { - result = node.(RemoteFlowSource).getSourceType() - or - not node instanceof RemoteFlowSource and - result = "Taint source" -} - -from DataFlow::Node node -where node = relevantTaintSource() -select node, getName(node) +from ThreatModelSource node +where node = relevantTaintSource() and node.getThreatModel() = "remote" +select node, getTaintSourceName(node) diff --git a/javascript/ql/src/meta/alerts/ThreatModelSources.ql b/javascript/ql/src/meta/alerts/ThreatModelSources.ql new file mode 100644 index 000000000000..99cd7b342a02 --- /dev/null +++ b/javascript/ql/src/meta/alerts/ThreatModelSources.ql @@ -0,0 +1,19 @@ +/** + * @name Threat model sources + * @description Sources of possibly untrusted input that can be configured via threat models. + * @kind problem + * @problem.severity recommendation + * @id js/meta/alerts/threat-model-sources + * @tags meta + * @precision very-low + */ + +import javascript +import meta.internal.TaintMetrics + +from ThreatModelSource node, string threatModel +where + node = relevantTaintSource() and + threatModel = node.getThreatModel() and + threatModel != "remote" // "remote" is reported by TaintSources.ql +select node, getTaintSourceName(node) + " (\"" + threatModel + "\" threat model)" diff --git a/javascript/ql/src/meta/internal/TaintMetrics.qll b/javascript/ql/src/meta/internal/TaintMetrics.qll index f6eae2eaa6e9..70d63a8cc3aa 100644 --- a/javascript/ql/src/meta/internal/TaintMetrics.qll +++ b/javascript/ql/src/meta/internal/TaintMetrics.qll @@ -75,9 +75,9 @@ DataFlow::Node relevantTaintSink(string kind) { DataFlow::Node relevantTaintSink() { result = relevantTaintSink(_) } /** - * Gets a relevant remote flow source. + * Gets a relevant threat model source. */ -RemoteFlowSource relevantTaintSource() { not result.getFile() instanceof IgnoredFile } +ThreatModelSource relevantTaintSource() { not result.getFile() instanceof IgnoredFile } /** * Gets the output of a call that shows intent to sanitize a value @@ -100,3 +100,10 @@ DataFlow::Node relevantSanitizerInput() { result = any(HtmlSanitizerCall call).getInput() and not result.getFile() instanceof IgnoredFile } + +string getTaintSourceName(DataFlow::Node node) { + result = node.(ThreatModelSource).getSourceType() + or + not node instanceof ThreatModelSource and + result = "Taint source" +} diff --git a/javascript/ql/test/library-tests/frameworks/Angular2/sink.component.ts b/javascript/ql/test/library-tests/frameworks/Angular2/sink.component.ts index 4db18a7e2d6d..7210e235d5a7 100644 --- a/javascript/ql/test/library-tests/frameworks/Angular2/sink.component.ts +++ b/javascript/ql/test/library-tests/frameworks/Angular2/sink.component.ts @@ -1,4 +1,4 @@ -import { Component } from "@angular/core"; +import { Component, Input } from "@angular/core"; import { DomSanitizer } from '@angular/platform-browser'; @Component({ @@ -6,17 +6,17 @@ import { DomSanitizer } from '@angular/platform-browser'; template: "not important" }) export class SinkComponent { - sink1: string; - sink2: string; - sink3: string; - sink4: string; - sink5: string; - sink6: string; - sink7: string; - sink8: string; - sink9: string; + @Input() sink1: string; + @Input() sink2: string; + @Input() sink3: string; + @Input() sink4: string; + @Input() sink5: string; + @Input() sink6: string; + @Input() sink7: string; + @Input() sink8: string; + @Input() sink9: string; - constructor(private sanitizer: DomSanitizer) {} + constructor(private sanitizer: DomSanitizer) { } foo() { this.sanitizer.bypassSecurityTrustHtml(this.sink1); diff --git a/javascript/ql/test/library-tests/frameworks/Angular2/test.expected b/javascript/ql/test/library-tests/frameworks/Angular2/test.expected index f09f0aed3b45..2eda4c17ee29 100644 --- a/javascript/ql/test/library-tests/frameworks/Angular2/test.expected +++ b/javascript/ql/test/library-tests/frameworks/Angular2/test.expected @@ -35,3 +35,13 @@ taintFlow | source.component.ts:16:33:16:40 | source() | sink.component.ts:22:48:22:57 | this.sink1 | testAttrSourceLocation | inline.component.ts:8:43:8:60 | [testAttr]=taint | inline.component.ts:8:55:8:59 | | +threatModelSource +| sink.component.ts:22:48:22:57 | this.sink1 | view-component-input | +| sink.component.ts:23:48:23:57 | this.sink2 | view-component-input | +| sink.component.ts:24:48:24:57 | this.sink3 | view-component-input | +| sink.component.ts:25:48:25:57 | this.sink4 | view-component-input | +| sink.component.ts:26:48:26:57 | this.sink5 | view-component-input | +| sink.component.ts:27:48:27:57 | this.sink6 | view-component-input | +| sink.component.ts:28:48:28:57 | this.sink7 | view-component-input | +| sink.component.ts:29:48:29:57 | this.sink8 | view-component-input | +| sink.component.ts:30:48:30:57 | this.sink9 | view-component-input | diff --git a/javascript/ql/test/library-tests/frameworks/Angular2/test.ql b/javascript/ql/test/library-tests/frameworks/Angular2/test.ql index 5ff996111211..bc03de14b3bd 100644 --- a/javascript/ql/test/library-tests/frameworks/Angular2/test.ql +++ b/javascript/ql/test/library-tests/frameworks/Angular2/test.ql @@ -32,3 +32,7 @@ query predicate testAttrSourceLocation(HTML::Attribute attrib, Angular2::Templat attrib.getName() = "[testAttr]" and top = attrib.getCodeInAttribute() } + +query predicate threatModelSource(ThreatModelSource source, string kind) { + kind = source.getThreatModel() +} diff --git a/javascript/ql/test/library-tests/frameworks/ReactJS/tests.expected b/javascript/ql/test/library-tests/frameworks/ReactJS/tests.expected index 491c75275988..48c548d7d802 100644 --- a/javascript/ql/test/library-tests/frameworks/ReactJS/tests.expected +++ b/javascript/ql/test/library-tests/frameworks/ReactJS/tests.expected @@ -309,3 +309,28 @@ test_JsxName_this | thisAccesses.js:61:19:61:41 | | thisAccesses.js:61:20:61:23 | this | locationSource | importedComponent.jsx:3:32:3:39 | location | +threatModelSource +| es5.js:4:24:4:33 | this.props | view-component-input | +| es5.js:20:24:20:33 | this.props | view-component-input | +| es6.js:1:37:1:36 | args | view-component-input | +| es6.js:3:24:3:33 | this.props | view-component-input | +| exportedComponent.jsx:1:29:1:33 | props | view-component-input | +| importedComponent.jsx:3:24:3:40 | {color, location} | view-component-input | +| importedComponent.jsx:3:32:3:39 | location | remote | +| namedImport.js:3:27:3:26 | args | view-component-input | +| namedImport.js:5:19:5:18 | args | view-component-input | +| plainfn.js:1:16:1:20 | props | view-component-input | +| plainfn.js:5:17:5:21 | props | view-component-input | +| plainfn.js:9:17:9:21 | props | view-component-input | +| plainfn.js:20:28:20:32 | props | view-component-input | +| preact.js:1:38:1:37 | args | view-component-input | +| preact.js:2:12:2:16 | props | view-component-input | +| preact.js:9:38:9:37 | args | view-component-input | +| probably-a-component.js:1:31:1:30 | args | view-component-input | +| probably-a-component.js:3:9:3:18 | this.props | view-component-input | +| props.js:2:37:2:36 | args | view-component-input | +| props.js:26:16:26:20 | props | view-component-input | +| rare-lifecycle-methods.js:1:33:1:32 | args | view-component-input | +| statePropertyWrites.js:38:24:38:33 | this.props | view-component-input | +| thisAccesses.js:31:12:31:16 | props | view-component-input | +| thisAccesses.js:48:18:48:18 | y | view-component-input | diff --git a/javascript/ql/test/library-tests/frameworks/ReactJS/tests.ql b/javascript/ql/test/library-tests/frameworks/ReactJS/tests.ql index b97e69b2526a..4d20306d4ed4 100644 --- a/javascript/ql/test/library-tests/frameworks/ReactJS/tests.ql +++ b/javascript/ql/test/library-tests/frameworks/ReactJS/tests.ql @@ -11,3 +11,7 @@ import ReactComponent_getAPropRead import ReactName query DataFlow::SourceNode locationSource() { result = DOM::locationSource() } + +query predicate threatModelSource(ThreatModelSource source, string kind) { + kind = source.getThreatModel() +} diff --git a/javascript/ql/test/library-tests/frameworks/Vue/single-component-file-1.vue b/javascript/ql/test/library-tests/frameworks/Vue/single-component-file-1.vue index 4f7e4b9c47fe..02045c31d29a 100644 --- a/javascript/ql/test/library-tests/frameworks/Vue/single-component-file-1.vue +++ b/javascript/ql/test/library-tests/frameworks/Vue/single-component-file-1.vue @@ -3,7 +3,8 @@ diff --git a/javascript/ql/test/library-tests/frameworks/Vue/single-file-component-7.vue b/javascript/ql/test/library-tests/frameworks/Vue/single-file-component-7.vue new file mode 100644 index 000000000000..0d099dc8b9b5 --- /dev/null +++ b/javascript/ql/test/library-tests/frameworks/Vue/single-file-component-7.vue @@ -0,0 +1,12 @@ + + + diff --git a/javascript/ql/test/library-tests/frameworks/Vue/single-file-component-8.vue b/javascript/ql/test/library-tests/frameworks/Vue/single-file-component-8.vue new file mode 100644 index 000000000000..03a7b838f2bf --- /dev/null +++ b/javascript/ql/test/library-tests/frameworks/Vue/single-file-component-8.vue @@ -0,0 +1,12 @@ + + + diff --git a/javascript/ql/test/library-tests/frameworks/Vue/tests.expected b/javascript/ql/test/library-tests/frameworks/Vue/tests.expected index 7f0ea1aa9ac5..fc5283738a01 100644 --- a/javascript/ql/test/library-tests/frameworks/Vue/tests.expected +++ b/javascript/ql/test/library-tests/frameworks/Vue/tests.expected @@ -1,12 +1,14 @@ component_getAPropertyValue | 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 | | compont-with-route.vue:0:0:0:0 | compont-with-route.vue | message | compont-with-route.vue:19:15:19:22 | 'Hello!' | -| 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 | -| 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 | -| 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 | -| single-file-component-4.vue:0:0:0:0 | single-file-component-4.vue | message | single-file-component-4.vue:12:23:12:30 | 'Hello!' | -| single-file-component-5.vue:0:0:0:0 | single-file-component-5.vue | dataA | single-file-component-5.vue:13:14:13:15 | 42 | -| single-file-component-5.vue:0:0:0:0 | single-file-component-5.vue | message | single-file-component-5.vue:10:23:10:30 | 'Hello!' | +| single-component-file-1.vue:0:0:0:0 | single-component-file-1.vue | dataA | single-component-file-1.vue:7:40:7:54 | 42 + this.input | +| single-file-component-3.vue:0:0:0:0 | single-file-component-3.vue | dataA | single-file-component-3-script.js:5:37:5:51 | 42 + this.input | +| single-file-component-4.vue:0:0:0:0 | single-file-component-4.vue | dataA | single-file-component-4.vue:17:14:17:15 | 42 | +| single-file-component-4.vue:0:0:0:0 | single-file-component-4.vue | dataB | single-file-component-4.vue:21:14:21:23 | this.input | +| single-file-component-4.vue:0:0:0:0 | single-file-component-4.vue | message | single-file-component-4.vue:13:23:13:30 | 'Hello!' | +| single-file-component-5.vue:0:0:0:0 | single-file-component-5.vue | dataA | single-file-component-5.vue:15:14:15:15 | 42 | +| single-file-component-5.vue:0:0:0:0 | single-file-component-5.vue | dataB | single-file-component-5.vue:19:14:19:23 | this.input | +| single-file-component-5.vue:0:0:0:0 | single-file-component-5.vue | message | single-file-component-5.vue:11:23:11:30 | 'Hello!' | | tst.js:3:1:10:2 | new Vue ... 2\\n\\t}\\n}) | dataA | tst.js:8:10:8:11 | 42 | | tst.js:12:1:16:2 | new Vue ... \\t}),\\n}) | dataA | tst.js:14:10:14:11 | 42 | | tst.js:18:1:27:2 | Vue.com ... }\\n\\t}\\n}) | dataA | tst.js:20:10:20:11 | 42 | @@ -35,9 +37,11 @@ component_getAPropertyValue | tst.js:113:17:117:2 | base.ex ... 0\\n\\t}\\n}) | fromSubclass2 | tst.js:115:18:115:20 | 100 | component_getOption | compont-with-route.vue:0:0:0:0 | compont-with-route.vue | watch | compont-with-route.vue:10:12:16:5 | {\\n ... }\\n } | -| 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 } } | -| 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 } } | -| 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) => { } | +| single-component-file-1.vue:0:0:0:0 | single-component-file-1.vue | data | single-component-file-1.vue:7:11:7:58 | functio ... put } } | +| single-component-file-1.vue:0:0:0:0 | single-component-file-1.vue | props | single-component-file-1.vue:6:12:6:20 | ['input'] | +| single-file-component-3.vue:0:0:0:0 | single-file-component-3.vue | data | single-file-component-3-script.js:5:8:5:55 | functio ... put } } | +| single-file-component-3.vue:0:0:0:0 | single-file-component-3.vue | props | single-file-component-3-script.js:4:9:4:17 | ['input'] | +| single-file-component-4.vue:0:0:0:0 | single-file-component-4.vue | render | single-file-component-4.vue:10:13:10:22 | (h) => { } | | tst.js:3:1:10:2 | new Vue ... 2\\n\\t}\\n}) | data | tst.js:7:8:9:2 | {\\n\\t\\tdataA: 42\\n\\t} | | tst.js:3:1:10:2 | new Vue ... 2\\n\\t}\\n}) | render | tst.js:4:10:6:2 | functio ... c);\\n\\t} | | tst.js:12:1:16:2 | new Vue ... \\t}),\\n}) | data | tst.js:13:8:15:3 | () => ( ... 42\\n\\t}) | @@ -76,6 +80,9 @@ component | single-file-component-3.vue:0:0:0:0 | single-file-component-3.vue | | single-file-component-4.vue:0:0:0:0 | single-file-component-4.vue | | single-file-component-5.vue:0:0:0:0 | single-file-component-5.vue | +| single-file-component-6.vue:0:0:0:0 | single-file-component-6.vue | +| single-file-component-7.vue:0:0:0:0 | single-file-component-7.vue | +| single-file-component-8.vue:0:0:0:0 | single-file-component-8.vue | | special-syntax.vue:0:0:0:0 | special-syntax.vue | | tst.js:3:1:10:2 | new Vue ... 2\\n\\t}\\n}) | | tst.js:12:1:16:2 | new Vue ... \\t}),\\n}) | @@ -97,10 +104,10 @@ component | tst.js:113:17:117:2 | base.ex ... 0\\n\\t}\\n}) | viewComponentStep | compont-with-route.vue:31:14:31:34 | this.$r ... ery.foo | compont-with-route.vue:2:8:2:21 | v-html=dataA | -| 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.vue:2:8:2:21 | v-html=dataA | -| single-file-component-4.vue:15:14:15:15 | 42 | single-file-component-4.vue:2:8:2:21 | v-html=dataA | -| single-file-component-5.vue:13:14:13:15 | 42 | single-file-component-5.vue:2:8:2:21 | v-html=dataA | +| single-component-file-1.vue:7:40:7:54 | 42 + this.input | single-component-file-1.vue:2:8:2:21 | v-html=dataA | +| single-file-component-3-script.js:5:37:5:51 | 42 + this.input | single-file-component-3.vue:2:8:2:21 | v-html=dataA | +| single-file-component-4.vue:17:14:17:15 | 42 | single-file-component-4.vue:2:8:2:21 | v-html=dataA | +| single-file-component-5.vue:15:14:15:15 | 42 | single-file-component-5.vue:2:8:2:21 | v-html=dataA | | tst.js:100:18:100:19 | 42 | tst.js:102:20:102:29 | this.dataA | templateElement | compont-with-route.vue:1:1:3:11 |