JS: Only extract local vars in TemplateTopLevel

Angular template expressions cannot refer to global variables, any
unqualified identifier is a reference to a property provided by the
component.

We extract them as implicitly declared local variables which the
QL model can then connect with data flow steps.
This commit is contained in:
Asger Feldthaus
2020-12-17 23:16:26 +00:00
parent faad466aa8
commit 2ba98da107
13 changed files with 93 additions and 36 deletions

View File

@@ -704,7 +704,10 @@ public class ASTExtractor {
+ locationManager.getStartLine()
+ ","
+ locationManager.getStartColumn());
scopeManager.enterScope(ScopeKind.module, moduleScopeKey, toplevelLabel);
Scope moduleScope = scopeManager.enterScope(ScopeKind.module, moduleScopeKey, toplevelLabel);
if (sourceType.hasNoGlobalScope()) {
scopeManager.setImplicitVariableScope(moduleScope);
}
scopeManager.addVariables(
sourceType.getPredefinedLocals(platform, locationManager.getSourceFileExtension()));
trapwriter.addTuple("is_module", toplevelLabel);

View File

@@ -113,6 +113,14 @@ public class ExtractorConfig {
return this != SCRIPT;
}
/**
* Returns true if this source type cannot access the global scope directly, and undeclared
* variables are implicitly declared in its local scope. Implies {@link #hasLocalScope()}.
*/
public boolean hasNoGlobalScope() {
return this == ANGULAR_TEMPLATE;
}
/** Returns true if this source is implicitly in strict mode. */
public boolean isStrictMode() {
return this == MODULE;

View File

@@ -118,7 +118,7 @@ public class HTMLExtractor implements IExtractor {
}
}
extractSnippet(
TopLevelKind.eventHandler,
TopLevelKind.angularTemplate,
config.withSourceType(SourceType.ANGULAR_TEMPLATE),
scopeManager,
textualExtractor,

View File

@@ -102,11 +102,21 @@ public class ScopeManager {
private final Scope toplevelScope;
private final ECMAVersion ecmaVersion;
private final Set<String> implicitGlobals = new LinkedHashSet<String>();
private Scope implicitVariableScope;
public ScopeManager(TrapWriter trapWriter, ECMAVersion ecmaVersion) {
this.trapWriter = trapWriter;
this.toplevelScope = enterScope(ScopeKind.global, trapWriter.globalID("global_scope"), null);
this.ecmaVersion = ecmaVersion;
this.implicitVariableScope = toplevelScope;
}
/**
* Sets the scope in which to declare variables that are referenced without
* being declared. This defaults to the global scope.
*/
public void setImplicitVariableScope(Scope implicitVariableScope) {
this.implicitVariableScope = implicitVariableScope;
}
/**
@@ -193,12 +203,12 @@ public class ScopeManager {
/**
* Get the label for a given variable in the current scope; if it cannot be found, add it to the
* global scope.
* implicit variable scope (usually the global scope).
*/
public Label getVarKey(String name) {
Label lbl = curScope.lookupVariable(name);
if (lbl == null) {
lbl = addVariable(name, toplevelScope);
lbl = addVariable(name, implicitVariableScope);
implicitGlobals.add(name);
}
return lbl;

View File

@@ -7,7 +7,8 @@ public enum TopLevelKind {
script(0),
inlineScript(1),
eventHandler(2),
javascriptUrl(3);
javascriptUrl(3),
angularTemplate(4);
private int value;

View File

@@ -300,7 +300,7 @@ class InlineScript extends @inline_script, Script { }
* ```
*/
class CodeInAttribute extends TopLevel {
CodeInAttribute() { this instanceof @event_handler or this instanceof @javascript_url }
CodeInAttribute() { this instanceof @event_handler or this instanceof @javascript_url or this instanceof @angular_template_toplevel }
}
/**

View File

@@ -309,7 +309,8 @@ module SourceNode {
astNode instanceof ImportSpecifier or
astNode instanceof ImportMetaExpr or
astNode instanceof TaggedTemplateExpr or
astNode instanceof Angular2::PipeRefExpr
astNode instanceof Angular2::PipeRefExpr or
astNode instanceof Angular2::TemplateVarRefExpr
)
or
DataFlow::parameterNode(this, _)

View File

@@ -307,6 +307,11 @@ class AnalyzedNegativeConditionGuard extends AnalyzedRefinement {
}
}
/** Holds if `v` is a variable in an Angular template. */
private predicate isAngularTemplateVariable(LocalVariable v) {
v = any(Angular2::TemplateTopLevel tl).getScope().getAVariable()
}
/**
* Gets the abstract value representing the initial value of variable `v`.
*
@@ -325,6 +330,9 @@ private AbstractValue getImplicitInitValue(LocalVariable v) {
then
// model hoisting
result = TAbstractFunction(getAFunDecl(v))
else
if isAngularTemplateVariable(v)
then result = TIndefiniteAbstractValue("heap")
else result = TAbstractUndefined()
}

View File

@@ -241,6 +241,33 @@ module Angular2 {
}
}
/**
* A reference to a variable in a template expression, corresponding
* to a property on the component class.
*/
class TemplateVarRefExpr extends Expr {
TemplateVarRefExpr() {
this = any(TemplateTopLevel tl).getScope().getAVariable().getAnAccess()
}
}
/** The top-level containing an Angular expression. */
class TemplateTopLevel extends TopLevel, @angular_template_toplevel {
/** Gets the expression in this top-level. */
Expr getExpression() {
result = getChildStmt(0).(ExprStmt).getExpr()
}
/** Gets the data flow node representing the initialization of the given variable in this scope. */
DataFlow::Node getVariableInit(string name) {
result = DataFlow::ssaDefinitionNode(SSA::implicitInit(getScope().getVariable(name)))
}
DataFlow::SourceNode getAVariableUse(string name) {
result = getScope().getVariable(name).getAnAccess().flow()
}
}
/** The RHS of a `templateUrl` property, seen as a path expression. */
private class TemplateUrlPath extends PathExpr {
TemplateUrlPath() {
@@ -264,18 +291,8 @@ module Angular2 {
attrib.getName().matches("*ng%")
}
/**
* Gets a global variable access to `name` within the given attribute.
*/
pragma[noinline]
private GlobalVarAccess getAGlobalVarAccessInAttribute(CodeInAttribute code, string name) {
exists(ComponentClass cls) and // do not materialize for non-Angular codebases
result.getTopLevel() = code and
result.getName() = name
}
private DataFlow::Node getAttributeValueAsNode(HTML::Attribute attrib) {
result = attrib.getCodeInAttribute().getChildStmt(0).(ExprStmt).getExpr().flow()
result = attrib.getCodeInAttribute().(TemplateTopLevel).getExpression().flow()
}
/**
@@ -361,11 +378,7 @@ module Angular2 {
* Gets an access to the variable `name` in the template body.
*/
DataFlow::Node getATemplateVarAccess(string name) {
exists(HTML::Attribute attrib |
attrib = getATemplateElement().getAnAttribute() and
isAngularExpressionAttribute(attrib) and
result = getAGlobalVarAccessInAttribute(attrib.getCodeInAttribute(), name).flow()
)
result = getATemplateElement().getAnAttribute().getCodeInAttribute().(TemplateTopLevel).getAVariableUse(name)
}
}
@@ -450,11 +463,7 @@ module Angular2 {
/** Gets a reference to the iterator variable. */
DataFlow::Node getAnIteratorAccess() {
exists(HTML::Attribute attrib |
attrib = getAnElementInScope().getAnAttribute() and
isAngularExpressionAttribute(attrib) and
result = getAGlobalVarAccessInAttribute(attrib.getCodeInAttribute(), getIteratorName()).flow()
)
result = getAnElementInScope().getAnAttribute().getCodeInAttribute().(TemplateTopLevel).getAVariableUse(getIteratorName())
}
}
@@ -474,4 +483,13 @@ module Angular2 {
succ = attrib.getAnIteratorAccess()
}
}
private class AnyCastStep extends TaintTracking::AdditionalTaintStep, DataFlow::CallNode {
AnyCastStep() { this = any(TemplateTopLevel tl).getAVariableUse("$any").getACall() }
override predicate step(DataFlow::Node pred, DataFlow::Node succ) {
pred = getArgument(0) and
succ = this
}
}
}

View File

@@ -124,7 +124,8 @@ case @toplevel.kind of
0 = @script
| 1 = @inline_script
| 2 = @event_handler
| 3 = @javascript_url;
| 3 = @javascript_url
| 4 = @angular_template_toplevel;
is_module (int tl: @toplevel ref);
is_nodejs (int tl: @toplevel ref);

View File

@@ -4,6 +4,7 @@
[sink3]="taint | unknownPipe:'safe'"
[sink4]="taint | testPipe:'safe'"
[sink5]="42 | testPipe:taint"
(someEvent)="methodOnComponent(taint)"
></sink-component>
<div *ngFor="let element of taintedArray">

View File

@@ -1,4 +1,5 @@
import { Component } from "@angular/core";
import { DomSanitizer } from '@angular/platform-browser';
@Component({
selector: "source-component",
@@ -9,9 +10,13 @@ export class Source {
taintedArray: string[];
safeArray: string[];
constructor() {
constructor(private sanitizer: DomSanitizer) {
this.taint = source();
this.taintedArray = [...source()];
this.safeArray = ['a', 'b'];
}
methodOnComponent(x) {
this.sanitizer.bypassSecurityTrustHtml(x);
}
}

View File

@@ -23,8 +23,9 @@ pipeClassRef
| TestPipe.ts:4:8:9:1 | class T ... ;\\n }\\n} | source.component.html:6:19:6:26 | testPipe |
taintFlow
| inline.component.ts:15:22:15:29 | source() | sink.component.ts:26:48:26:57 | this.sink7 |
| source.component.ts:13:22:13:29 | source() | sink.component.ts:20:48:20:57 | this.sink1 |
| source.component.ts:13:22:13:29 | source() | sink.component.ts:23:48:23:57 | this.sink4 |
| source.component.ts:13:22:13:29 | source() | sink.component.ts:24:48:24:57 | this.sink5 |
| source.component.ts:13:22:13:29 | source() | sink.component.ts:25:48:25:57 | this.sink6 |
| source.component.ts:14:33:14:40 | source() | sink.component.ts:20:48:20:57 | this.sink1 |
| source.component.ts:14:22:14:29 | source() | sink.component.ts:20:48:20:57 | this.sink1 |
| source.component.ts:14:22:14:29 | source() | sink.component.ts:23:48:23:57 | this.sink4 |
| source.component.ts:14:22:14:29 | source() | sink.component.ts:24:48:24:57 | this.sink5 |
| source.component.ts:14:22:14:29 | source() | sink.component.ts:25:48:25:57 | this.sink6 |
| source.component.ts:14:22:14:29 | source() | source.component.ts:20:48:20:48 | x |
| source.component.ts:15:33:15:40 | source() | sink.component.ts:20:48:20:57 | this.sink1 |