JS: Extract HTML from inline templates

This commit is contained in:
Asger Feldthaus
2020-12-14 10:37:38 +00:00
parent 6bf9345258
commit 8848ee2d10
9 changed files with 170 additions and 41 deletions

View File

@@ -1,5 +1,6 @@
package com.semmle.js.extractor;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@@ -166,8 +167,10 @@ public class ASTExtractor {
private final Label toplevelLabel;
private final LexicalExtractor lexicalExtractor;
private final RegExpExtractor regexpExtractor;
private final ExtractorConfig config;
public ASTExtractor(LexicalExtractor lexicalExtractor, ScopeManager scopeManager) {
public ASTExtractor(ExtractorConfig config, LexicalExtractor lexicalExtractor, ScopeManager scopeManager) {
this.config = config;
this.trapwriter = lexicalExtractor.getTrapwriter();
this.locationManager = lexicalExtractor.getLocationManager();
this.contextManager = new SyntacticContextManager();
@@ -1136,9 +1139,70 @@ public class ASTExtractor {
visit(nd.getDefaultValue(), propkey, 2, IdContext.varBind);
if (nd.isComputed()) trapwriter.addTuple("is_computed", propkey);
if (nd.isMethod()) trapwriter.addTuple("is_method", propkey);
// Extract the value of a property named `template` as HTML, in order to support
// Angular2 components with an inline template.
if (!nd.isComputed() && "template".equals(tryGetIdentifierName(nd.getKey()))) {
extractStringValueAsHtml(nd.getValue());
}
return propkey;
}
/**
* Extracts the string value of <code>expr</code> as an HTML snippet.
*/
private void extractStringValueAsHtml(Expression expr) {
TextualExtractor textualExtractor = lexicalExtractor.getTextualExtractor();
if (textualExtractor.isSnippet()) {
return; // do not create nested snippets
}
String source = tryGetStringValueFromExpression(expr);
if (source == null) {
return;
}
SourceLocation loc = expr.getLoc();
Path originalFile = textualExtractor.getExtractedFile().toPath();
Path vfile = originalFile.resolveSibling(originalFile.getFileName().toString() + "." + loc.getStart().getLine() + "." + loc.getStart().getColumn() + ".html");
LocationManager innerLocationManager = new LocationManager(
locationManager.getSourceFile(),
locationManager.getTrapWriter(),
locationManager.getFileLabel());
innerLocationManager.setStart(loc.getStart().getLine(), loc.getStart().getColumn());
TextualExtractor innerTextualExtractor = new TextualExtractor(
trapwriter,
innerLocationManager,
source,
false,
getMetrics(),
vfile.toFile());
HTMLExtractor html = HTMLExtractor.forEmbeddedHtml(config);
html.extract(innerTextualExtractor);
}
private String tryGetIdentifierName(Expression e) {
return e instanceof Identifier ? ((Identifier)e).getName() : null;
}
private String tryGetStringValueFromExpression(Expression e) {
if (e instanceof Literal) {
Literal lit = (Literal) e;
return lit.isStringLiteral() ? (String) lit.getValue() : null;
}
if (e instanceof TemplateLiteral) {
TemplateLiteral lit = (TemplateLiteral) e;
if (!lit.getExpressions().isEmpty()) {
return null;
}
StringBuilder sb = new StringBuilder();
for (TemplateElement elm : lit.getQuasis()) {
sb.append(elm.getCooked());
}
return sb.toString();
}
return null;
}
@Override
public Label visit(IfStatement nd, Context c) {
Label key = super.visit(nd, c);

View File

@@ -166,10 +166,21 @@ public class HTMLExtractor implements IExtractor {
private final ExtractorConfig config;
private final ExtractorState state;
private final boolean isEmbedded;
public HTMLExtractor(ExtractorConfig config, ExtractorState state) {
public HTMLExtractor(ExtractorConfig config, ExtractorState state, boolean isEmbedded) {
this.config = config.withPlatform(Platform.WEB);
this.state = state;
this.isEmbedded = isEmbedded;
}
public HTMLExtractor(ExtractorConfig config, ExtractorState state) {
this(config, state, false);
}
/** Creates an HTML extractor for embedded HTML snippets. */
public static HTMLExtractor forEmbeddedHtml(ExtractorConfig config) {
return new HTMLExtractor(config, null, true);
}
@Override
@@ -179,12 +190,15 @@ public class HTMLExtractor implements IExtractor {
Attributes.setDefaultMaxErrorCount(100);
JavaScriptHTMLElementHandler eltHandler = new JavaScriptHTMLElementHandler(textualExtractor);
LocationManager locationManager = textualExtractor.getLocationManager();
HtmlPopulator extractor =
new HtmlPopulator(
this.config.getHtmlHandling(),
textualExtractor.getSource(),
textualExtractor.getTrapwriter(),
textualExtractor.getLocationManager().getFileLabel());
locationManager.getFileLabel());
extractor.setStartOffset(locationManager.getStartLine() - 1, locationManager.getStartColumn() - 1);
extractor.doit(Option.some(eltHandler));
@@ -266,6 +280,9 @@ public class HTMLExtractor implements IExtractor {
int column,
boolean isTypeScript) {
if (isTypeScript) {
if (isEmbedded) {
return null; // Do not extract files from HTML embedded in other files.
}
Path file = textualExtractor.getExtractedFile().toPath();
FileSnippet snippet =
new FileSnippet(file, line, column, toplevelKind, config.getSourceType());

View File

@@ -106,7 +106,7 @@ public class JSExtractor {
lexicalExtractor =
new LexicalExtractor(textualExtractor, parserRes.getTokens(), parserRes.getComments());
ASTExtractor scriptExtractor = new ASTExtractor(lexicalExtractor, scopeManager);
ASTExtractor scriptExtractor = new ASTExtractor(config, lexicalExtractor, scopeManager);
toplevelLabel = scriptExtractor.getToplevelLabel();
lexicalExtractor.extractComments(toplevelLabel);
loc = lexicalExtractor.extractLines(parserRes.getSource(), toplevelLabel);
@@ -119,7 +119,7 @@ public class JSExtractor {
} else {
lexicalExtractor =
new LexicalExtractor(textualExtractor, new ArrayList<Token>(), new ArrayList<Comment>());
ASTExtractor scriptExtractor = new ASTExtractor(lexicalExtractor, null);
ASTExtractor scriptExtractor = new ASTExtractor(config, lexicalExtractor, null);
toplevelLabel = scriptExtractor.getToplevelLabel();
trapwriter.addTuple("toplevels", toplevelLabel, toplevelKind.getValue());

View File

@@ -1,5 +1,7 @@
package com.semmle.js.extractor;
import java.util.List;
import com.semmle.js.ast.Comment;
import com.semmle.js.ast.Position;
import com.semmle.js.ast.SourceElement;
@@ -7,7 +9,6 @@ import com.semmle.js.ast.Token;
import com.semmle.js.extractor.ExtractionMetrics.ExtractionPhase;
import com.semmle.util.trap.TrapWriter;
import com.semmle.util.trap.TrapWriter.Label;
import java.util.List;
/**
* Extractor for populating lexical information about a JavaScript file, including comments and
@@ -28,7 +29,11 @@ public class LexicalExtractor {
this.tokens = tokens;
this.comments = comments;
}
public TextualExtractor getTextualExtractor() {
return textualExtractor;
}
public TrapWriter getTrapwriter() {
return trapwriter;
}

View File

@@ -84,6 +84,32 @@ module HTML {
override string getAPrimaryQlClass() { result = "HTML::Element" }
}
/**
* Gets the inline script of the given attribute, if any.
*/
CodeInAttribute getCodeInAttribute(XMLAttribute attribute) {
exists(
string f, Location l1, int sl1, int sc1, int el1, int ec1, Location l2, int sl2, int sc2,
int el2, int ec2
|
l1 = attribute.getLocation() and
l2 = result.getLocation() and
l1.hasLocationInfo(f, sl1, sc1, el1, ec1) and
l2.hasLocationInfo(f, sl2, sc2, el2, ec2)
|
(
sl1 = sl2 and sc1 < sc2
or
sl1 < sl2
) and
(
el1 = el2 and ec1 > ec2
or
el1 > el2
)
)
}
/**
* An attribute of an HTML element.
*
@@ -101,6 +127,13 @@ module HTML {
override Location getLocation() { xmllocations(this, result) }
/**
* Gets the inline script of this attribute, if any.
*/
CodeInAttribute getCodeInAttribute() {
result = getCodeInAttribute(this)
}
/**
* Gets the element to which this attribute belongs.
*/
@@ -127,32 +160,6 @@ module HTML {
override string toString() { result = getName() + "=" + getValue() }
/**
* Gets the inline script of this attribute, if any.
*/
CodeInAttribute getCodeInAttribute() {
exists(
string f, Location l1, int sl1, int sc1, int el1, int ec1, Location l2, int sl2, int sc2,
int el2, int ec2
|
l1 = getLocation() and
l2 = result.getLocation() and
l1.hasLocationInfo(f, sl1, sc1, el1, ec1) and
l2.hasLocationInfo(f, sl2, sc2, el2, ec2)
|
(
sl1 = sl2 and sc1 < sc2
or
sl1 < sl2
) and
(
el1 = el2 and ec1 > ec2
or
el1 > el2
)
)
}
override string getAPrimaryQlClass() { result = "HTML::Attribute" }
}

View File

@@ -355,14 +355,30 @@ module Angular2 {
result = decorator.getOptionArgument(0, "templateUrl").asExpr().(PathExpr).resolve()
}
pragma[noinline]
private Location getInlineTemplateLocation() {
result = decorator.getOptionArgument(0, "template").asExpr().getLocation()
}
private XMLAttribute getAnAttributeInInlineTemplate() {
exists(Location templateLoc, Location attribLoc |
templateLoc = getInlineTemplateLocation() and
attribLoc = result.getLocation() and
templateLoc.getFile() = attribLoc.getFile()
// TODO: check line/column - though in practice checking the file is enough
)
}
/**
* Gets an access to the variable `name` in the template body.
*/
DataFlow::Node getATemplateVarAccess(string name) {
exists(HTML::Attribute attrib |
attrib.getFile() = getTemplateFile() and
exists(XMLAttribute attrib |
attrib.getLocation().getFile() = getTemplateFile() or
attrib = getAnAttributeInInlineTemplate()
|
isAngularExpressionAttribute(attrib) and
result = getAGlobalVarAccessInAttribute(attrib.getCodeInAttribute(), name).flow()
result = getAGlobalVarAccessInAttribute(HTML::getCodeInAttribute(attrib), name).flow()
)
}
}

View File

@@ -0,0 +1,17 @@
import { Input, Component } from '@angular/core';
@Component({
selector: 'mid-component',
template: `
<sink-component [sink7]="taint"></sink-component>
\n<sink-component [sink7]="taint"></sink-component>
`
})
export class InlineComponent {
taint: string;
constructor() {
this.taint = source();
}
}

View File

@@ -12,6 +12,7 @@ export class SinkComponent {
sink4: string;
sink5: string;
sink6: string;
sink7: string;
constructor(private sanitizer: DomSanitizer) {}
@@ -22,5 +23,6 @@ export class SinkComponent {
this.sanitizer.bypassSecurityTrustHtml(this.sink4);
this.sanitizer.bypassSecurityTrustHtml(this.sink5);
this.sanitizer.bypassSecurityTrustHtml(this.sink6);
this.sanitizer.bypassSecurityTrustHtml(this.sink7);
}
}

View File

@@ -22,8 +22,9 @@ pipeClassRef
| TestPipe.ts:4:8:9:1 | class T ... ;\\n }\\n} | source.component.html:5:22:5:29 | testPipe |
| TestPipe.ts:4:8:9:1 | class T ... ;\\n }\\n} | source.component.html:6:19:6:26 | testPipe |
taintFlow
| source.component.ts:13:22:13:29 | source() | sink.component.ts:19:48:19:57 | this.sink1 |
| source.component.ts:13:22:13:29 | source() | sink.component.ts:22:48:22:57 | this.sink4 |
| source.component.ts:13:22:13:29 | source() | sink.component.ts:23:48:23:57 | this.sink5 |
| source.component.ts:13:22:13:29 | source() | sink.component.ts:24:48:24:57 | this.sink6 |
| source.component.ts:14:33:14:40 | source() | sink.component.ts:19:48:19:57 | this.sink1 |
| 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 |