Skip to content

Commit 170a069

Browse files
authored
Merge pull request github#6403 from asgerf/js/handlebars-extraction
Approved by erik-krogh
2 parents abdf993 + 87843a3 commit 170a069

File tree

90 files changed

+8657
-1588
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

90 files changed

+8657
-1588
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
lgtm,codescanning
2+
* Added support for more templating languages.
3+
- EJS, Mustache, Handlebars, Nunjucks, Hogan, and Swig are now supported.
4+
- Template tags from the above dialects are now recognized as sinks
5+
when not escaped safely for the context, leading to additional results for `js/xss` and `js/code-injection`.
6+
- Files with the extension `.ejs`, `.hbs`, or `.njk` are now extracted and analyzed.

javascript/extractor/src/com/semmle/jcorn/Options.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ public void call(
3939
Position endLoc);
4040
}
4141

42-
private boolean allowHashBang, allowReturnOutsideFunction, allowImportExportEverywhere;
42+
private boolean allowHashBang, allowReturnOutsideFunction, allowImportExportEverywhere, allowGeneratedCodeExprs;
4343
private boolean preserveParens, mozExtensions, jscript, esnext, v8Extensions, e4x;
4444
private int ecmaVersion;
4545
private AllowReserved allowReserved;
@@ -58,6 +58,7 @@ public Options() {
5858
this.allowReserved = AllowReserved.YES;
5959
this.allowReturnOutsideFunction = false;
6060
this.allowImportExportEverywhere = false;
61+
this.allowGeneratedCodeExprs = true;
6162
this.allowHashBang = false;
6263
this.onToken = null;
6364
this.onComment = null;
@@ -75,6 +76,7 @@ public Options(Options that) {
7576
this.allowHashBang = that.allowHashBang;
7677
this.allowReturnOutsideFunction = that.allowReturnOutsideFunction;
7778
this.allowImportExportEverywhere = that.allowImportExportEverywhere;
79+
this.allowGeneratedCodeExprs = that.allowGeneratedCodeExprs;
7880
this.preserveParens = that.preserveParens;
7981
this.mozExtensions = that.mozExtensions;
8082
this.jscript = that.jscript;
@@ -104,6 +106,10 @@ public boolean allowImportExportEverywhere() {
104106
return allowImportExportEverywhere;
105107
}
106108

109+
public boolean allowGeneratedCodeExprs() {
110+
return allowGeneratedCodeExprs;
111+
}
112+
107113
public boolean preserveParens() {
108114
return preserveParens;
109115
}

javascript/extractor/src/com/semmle/jcorn/Parser.java

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
import com.semmle.js.ast.ForStatement;
5353
import com.semmle.js.ast.FunctionDeclaration;
5454
import com.semmle.js.ast.FunctionExpression;
55+
import com.semmle.js.ast.GeneratedCodeExpr;
5556
import com.semmle.js.ast.IFunction;
5657
import com.semmle.js.ast.INode;
5758
import com.semmle.js.ast.IPattern;
@@ -537,7 +538,7 @@ private Token readToken_question() { // '?'
537538
}
538539
return this.finishOp(TokenType.questionquestion, 2);
539540
}
540-
541+
541542
}
542543
return this.finishOp(TokenType.question, 1);
543544
}
@@ -617,6 +618,15 @@ private Token readToken_lt_gt(int code) { // '<>'
617618
this.skipSpace();
618619
return this.nextToken();
619620
}
621+
if (next == '%' && code == '<' && this.options.allowGeneratedCodeExprs()) {
622+
// `<%`, the beginning of an EJS-style template tag
623+
size = 2;
624+
int nextNext = charAt(this.pos + 2);
625+
if (nextNext == '=' || nextNext == '-') {
626+
++size;
627+
}
628+
return this.finishOp(TokenType.generatedCodeDelimiterEJS, size);
629+
}
620630
if (next == 61) size = 2;
621631
return this.finishOp(TokenType.relational, size);
622632
}
@@ -1688,6 +1698,9 @@ protected Expression parseExprAtom(DestructuringErrors refDestructuringErrors) {
16881698
return this.parseNew();
16891699
} else if (this.type == TokenType.backQuote) {
16901700
return this.parseTemplate(false);
1701+
} else if (this.type == TokenType.generatedCodeDelimiterEJS) {
1702+
String openingDelimiter = (String) this.value;
1703+
return this.parseGeneratedCodeExpr(this.startLoc, openingDelimiter, "%>");
16911704
} else {
16921705
this.unexpected();
16931706
return null;
@@ -1929,10 +1942,16 @@ MethodDefinition.Kind getMethodKind() {
19291942
// Parse an object literal or binding pattern.
19301943
protected Expression parseObj(boolean isPattern, DestructuringErrors refDestructuringErrors) {
19311944
Position startLoc = this.startLoc;
1945+
if (!isPattern && options.allowGeneratedCodeExprs() && charAt(pos) == '{') {
1946+
// Parse mustache-style placeholder expression: {{ ... }} or {{{ ... }}}
1947+
return charAt(pos + 1) == '{'
1948+
? parseGeneratedCodeExpr(startLoc, "{{{", "}}}")
1949+
: parseGeneratedCodeExpr(startLoc, "{{", "}}");
1950+
}
19321951
boolean first = true;
19331952
Map<String, PropInfo> propHash = new LinkedHashMap<>();
19341953
List<Property> properties = new ArrayList<Property>();
1935-
this.next();
1954+
this.next(); // skip '{'
19361955
while (!this.eat(TokenType.braceR)) {
19371956
if (!first) {
19381957
this.expect(TokenType.comma);
@@ -1949,6 +1968,42 @@ protected Expression parseObj(boolean isPattern, DestructuringErrors refDestruct
19491968
return this.finishNode(node);
19501969
}
19511970

1971+
/** Emit a token ranging from the current position until <code>endOfToken</code>. */
1972+
private Token generateTokenEndingAt(int endOfToken, TokenType tokenType) {
1973+
this.lastTokEnd = this.end;
1974+
this.lastTokStart = this.start;
1975+
this.lastTokEndLoc = this.endLoc;
1976+
this.lastTokStartLoc = this.startLoc;
1977+
this.start = this.pos;
1978+
this.startLoc = this.curPosition();
1979+
this.pos = endOfToken;
1980+
return finishToken(tokenType);
1981+
}
1982+
1983+
/** Parse a generated expression. The current token refers to the opening delimiter. */
1984+
protected Expression parseGeneratedCodeExpr(Position startLoc, String openingDelimiter, String closingDelimiter) {
1985+
// Emit a token for what's left of the opening delimiter, if there are any remaining characters
1986+
int startOfBody = startLoc.getOffset() + openingDelimiter.length();
1987+
if (this.pos != startOfBody) {
1988+
this.generateTokenEndingAt(startOfBody, TokenType.generatedCodeDelimiter);
1989+
}
1990+
1991+
// Emit a token for the generated code body
1992+
int endOfBody = this.input.indexOf(closingDelimiter, startOfBody);
1993+
if (endOfBody == -1) {
1994+
this.unexpected(startLoc);
1995+
}
1996+
Token bodyToken = this.generateTokenEndingAt(endOfBody, TokenType.generatedCodeExpr);
1997+
1998+
// Emit a token for the closing delimiter
1999+
this.generateTokenEndingAt(endOfBody + closingDelimiter.length(), TokenType.generatedCodeDelimiter);
2000+
2001+
this.next(); // produce lookahead token
2002+
2003+
return finishNode(new GeneratedCodeExpr(new SourceLocation(startLoc), openingDelimiter, closingDelimiter,
2004+
bodyToken.getValue()));
2005+
}
2006+
19522007
protected Property parseProperty(
19532008
boolean isPattern,
19542009
DestructuringErrors refDestructuringErrors,

javascript/extractor/src/com/semmle/jcorn/TokenType.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
package com.semmle.jcorn;
22

3-
import com.semmle.jcorn.Parser.TokContext;
43
import java.util.LinkedHashMap;
54
import java.util.Map;
65

6+
import com.semmle.jcorn.Parser.TokContext;
7+
78
/// tokentype.js
89

910
// ## Token types
@@ -89,6 +90,9 @@ public void updateContext(Parser parser, TokenType prevType) {
8990
arrow = new TokenType(new Properties("=>").beforeExpr()),
9091
template = new TokenType(new Properties("template")),
9192
invalidTemplate = new TokenType(new Properties("invalidTemplate")),
93+
generatedCodeExpr = new TokenType(new Properties("generatedCodeExpr")),
94+
generatedCodeDelimiter = new TokenType(new Properties("generatedCodeDelimiter")),
95+
generatedCodeDelimiterEJS = new TokenType(new Properties("<%/%>")),
9296
ellipsis = new TokenType(new Properties("...").beforeExpr()),
9397
backQuote =
9498
new TokenType(new Properties("`").startsExpr()) {

javascript/extractor/src/com/semmle/jcorn/jsx/JSXParser.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -420,8 +420,9 @@ protected Token readToken(int code) {
420420
&& code == 60
421421
&& this.exprAllowed
422422
&&
423-
// avoid getting confused on HTML comments
424-
this.charAt(this.pos + 1) != '!') {
423+
// avoid getting confused on HTML comments or EJS-style template tags
424+
this.charAt(this.pos + 1) != '!' &&
425+
this.charAt(this.pos + 1) != '%') {
425426
++this.pos;
426427
return this.finishToken(jsxTagStart);
427428
}

javascript/extractor/src/com/semmle/js/ast/DefaultVisitor.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -777,4 +777,9 @@ public R visit(XMLQualifiedIdentifier nd, C c) {
777777
public R visit(XMLDotDotExpression nd, C c) {
778778
return visit((Expression) nd, c);
779779
}
780+
781+
@Override
782+
public R visit(GeneratedCodeExpr nd, C c) {
783+
return visit((Expression) nd, c);
784+
}
780785
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package com.semmle.js.ast;
2+
3+
/**
4+
* A placeholder for generated code, speculatively parsed as a primary expression.
5+
*
6+
* <p>For example, in this snippet,
7+
*
8+
* <pre>
9+
* let data = {{user_data}};
10+
* </pre>
11+
*
12+
* the expression <code>{{user_data}}</code> is assumed to be filled in by a templating engine so
13+
* that it can be parsed as an expression, and a <code>GeneratedCodeExpr</code> is thus created to
14+
* represent it.
15+
*/
16+
public class GeneratedCodeExpr extends Expression {
17+
private String openingDelimiter;
18+
private String closingDelimiter;
19+
private String body;
20+
21+
public GeneratedCodeExpr(
22+
SourceLocation loc, String openingDelimiter, String closingDelimiter, String body) {
23+
super("GeneratedCodeExpr", loc);
24+
this.openingDelimiter = openingDelimiter;
25+
this.closingDelimiter = closingDelimiter;
26+
this.body = body;
27+
}
28+
29+
public String getOpeningDelimiter() {
30+
return openingDelimiter;
31+
}
32+
33+
public String getClosingDelimiter() {
34+
return closingDelimiter;
35+
}
36+
37+
public String getBody() {
38+
return body;
39+
}
40+
41+
@Override
42+
public <C, R> R accept(Visitor<C, R> v, C c) {
43+
return v.visit(this, c);
44+
}
45+
}

javascript/extractor/src/com/semmle/js/ast/NodeCopier.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -894,4 +894,9 @@ public INode visit(XMLQualifiedIdentifier nd, Void c) {
894894
public INode visit(XMLDotDotExpression nd, Void c) {
895895
return new XMLDotDotExpression(visit(nd.getLoc()), copy(nd.getLeft()), copy(nd.getRight()));
896896
}
897+
898+
@Override
899+
public INode visit(GeneratedCodeExpr nd, Void c) {
900+
return new GeneratedCodeExpr(visit(nd.getLoc()), nd.getOpeningDelimiter(), nd.getClosingDelimiter(), nd.getBody());
901+
}
897902
}

javascript/extractor/src/com/semmle/js/ast/Visitor.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,4 +313,6 @@ public interface Visitor<C, R> {
313313
public R visit(XMLQualifiedIdentifier nd, C c);
314314

315315
public R visit(XMLDotDotExpression nd, C c);
316+
317+
public R visit(GeneratedCodeExpr generatedCodeExpr, C c);
316318
}

0 commit comments

Comments
 (0)