mirror of
https://github.com/github/codeql.git
synced 2026-05-02 12:15:17 +02:00
JS: Extract HTML from inline templates
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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" }
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 |
|
||||
|
||||
Reference in New Issue
Block a user