Skip to content

Commit 23eeb49

Browse files
committed
JS: Detect relevant templating syntax, and add sinks
1 parent f3b97f0 commit 23eeb49

3 files changed

Lines changed: 170 additions & 0 deletions

File tree

javascript/ql/src/semmle/javascript/frameworks/Templating.qll

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,47 @@ module Templating {
6666

6767
/** Gets the top-level containing the template expression to be inserted at this placeholder. */
6868
TemplateTopLevel getInnerTopLevel() { toplevel_parent_xml_node(result, this) }
69+
70+
/**
71+
* Holds if this performs raw interpolation, that is, inserts its result
72+
* in the output without escaping it.
73+
*/
74+
predicate isRawInterpolation() {
75+
getRawText().regexpMatch(getLikelyTemplateSyntax(getFile()).getRawInterpolationRegexp())
76+
}
77+
78+
/** Holds if this occurs in a `script` tag. */
79+
predicate isInScriptTag() {
80+
getParent() instanceof HTML::ScriptElement
81+
}
82+
83+
/**
84+
* Holds if this occurs in an attribute value that is interepted as JavaScript.
85+
*
86+
* Unlike in script tags, HTML entities in attributes are expanded prior to JS parsing,
87+
* which cancels out the benefit of HTML escaping.
88+
*/
89+
predicate isInCodeAttribute() {
90+
exists(TopLevel code |
91+
code = getParent().(HTML::Attribute).getCodeInAttribute()
92+
|
93+
code instanceof EventHandlerCode or
94+
code instanceof JavaScriptURL
95+
)
96+
}
97+
98+
/** Holds if this placeholder occurs in JS code. */
99+
predicate isInCodeContext() {
100+
isInScriptTag() or isInCodeAttribute()
101+
}
102+
103+
/**
104+
* Holds if this occurs in generated code as an expression or statement,
105+
* that is, without being enclosed in a string literal or similar.
106+
*/
107+
predicate isInPlainCodeContext() {
108+
this = any(GeneratedCodeExpr e).getPlaceholderTag()
109+
}
69110
}
70111

71112
/**
@@ -387,4 +428,107 @@ module Templating {
387428
private TemplateFile getBestMatchingTarget(TemplateFileReferenceString ref) {
388429
result = max(getAMatchingTarget(ref) as f order by getRankOfMatchingTarget(f, ref))
389430
}
431+
432+
/**
433+
* A syntax type implemented by one or more supported templating engines.
434+
*
435+
* The syntax type determines which templating tags perform implicit escaping.
436+
*
437+
* Since this varies between templating engines, it is important to recognize the
438+
* templating engine correctly.
439+
*/
440+
abstract class TemplateSyntax extends string {
441+
bindingset[this]
442+
TemplateSyntax() { this = this }
443+
444+
/**
445+
* Gets a regular expression matching the full text of a placeholder tag
446+
* using raw interpolation, that is, without HTML escaping.
447+
*/
448+
abstract string getRawInterpolationRegexp();
449+
450+
/**
451+
* Gets a regular expression matching the full text of a placeholder tag
452+
* that performs HTML escaping on its output.
453+
*/
454+
abstract string getEscapingInterpolationRegexp();
455+
456+
/** Gets a file extension that is specific to this templating engine. */
457+
abstract string getAFileExtension();
458+
459+
/** Gets the name of an NPM package providing this templating syntax. */
460+
abstract string getAPackageName();
461+
}
462+
463+
/**
464+
* Mustache-style syntax, using `{{ }}` for safe interpolation, and (in some dialects)
465+
* `{{{ x }}}` for raw interpolation.
466+
*/
467+
private class MustacheStyleSyntax extends TemplateSyntax {
468+
MustacheStyleSyntax() { this = "mustache" }
469+
470+
override string getRawInterpolationRegexp() {
471+
result = "(?s)\\{\\{\\{(.*?)\\}\\}\\}"
472+
}
473+
474+
override string getEscapingInterpolationRegexp() {
475+
result = "(?s)\\{\\{[^{](.*?)\\}\\}"
476+
}
477+
478+
override string getAFileExtension() {
479+
result = "hbs"
480+
}
481+
482+
override string getAPackageName() {
483+
result = ["mustache", "handlebars", "hbs", "express-hbs", "swig", "swig-templates", "hogan", "hogan.js", "nunjucks"]
484+
}
485+
}
486+
487+
/**
488+
* EJS-style syntax, using `<%= x %>` for safe interpolation, and `<%- x %>` for
489+
* unsafe interpolation.
490+
*/
491+
private class EjsStyleSyntax extends TemplateSyntax {
492+
EjsStyleSyntax() { this = "ejs" }
493+
494+
override string getRawInterpolationRegexp() {
495+
result = "(?s)<%-(.*?)%>"
496+
}
497+
498+
override string getEscapingInterpolationRegexp() {
499+
result = "(?s)<%=(.*?)%>"
500+
}
501+
502+
override string getAFileExtension() {
503+
result = "ejs"
504+
}
505+
506+
override string getAPackageName() {
507+
result = "ejs"
508+
}
509+
}
510+
511+
private TemplateSyntax getOwnTemplateSyntaxInFolder(Folder f) {
512+
exists(PackageDependencies deps |
513+
deps.getADependency(result.getAPackageName(), _) and
514+
f = deps.getFile().getParentContainer()
515+
)
516+
}
517+
518+
private TemplateSyntax getTemplateSyntaxInFolder(Folder f) {
519+
result = getOwnTemplateSyntaxInFolder(f)
520+
or
521+
not exists(getOwnTemplateSyntaxInFolder(f)) and
522+
result = getTemplateSyntaxInFolder(f.getParentContainer())
523+
}
524+
525+
/**
526+
* Gets a template syntax likely to be used in the given file.
527+
*/
528+
TemplateSyntax getLikelyTemplateSyntax(TemplateFile file) {
529+
result.getAFileExtension() = file.getExtension()
530+
or
531+
not file.getExtension() = any(TemplateSyntax s).getAFileExtension() and
532+
result = getTemplateSyntaxInFolder(file.getParentContainer())
533+
}
390534
}

javascript/ql/src/semmle/javascript/security/dataflow/CodeInjectionCustomizations.qll

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,20 @@ module CodeInjection {
5151
}
5252
}
5353

54+
/**
55+
* A template tag occuring in JS code, viewed as a code injection sink.
56+
*/
57+
class TemplateTagInScriptSink extends Sink {
58+
TemplateTagInScriptSink() {
59+
// Note: currently viewing all tags in code as sinks, but this can lead to
60+
// some FPs when values are escaped correctly.
61+
exists(Templating::TemplatePlaceholderTag tag |
62+
tag.isInCodeContext() and
63+
this = tag.asDataFlowNode()
64+
)
65+
}
66+
}
67+
5468
/**
5569
* Gets a reference to a `<script />` tag created using `document.createElement`.
5670
*/

javascript/ql/src/semmle/javascript/security/dataflow/Xss.qll

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -369,6 +369,18 @@ module DomBasedXss {
369369
*/
370370
class VHtmlSink extends Vue::VHtmlAttribute, DomBasedXss::Sink { }
371371

372+
/**
373+
* A raw interpolation tag in a template file, viewed as an XSS sink.
374+
*/
375+
class TemplateSink extends DomBasedXss::Sink {
376+
TemplateSink() {
377+
exists(Templating::TemplatePlaceholderTag tag |
378+
tag.isRawInterpolation() and
379+
this = tag.asDataFlowNode()
380+
)
381+
}
382+
}
383+
372384
/**
373385
* A property read from a safe property is considered a sanitizer.
374386
*/

0 commit comments

Comments
 (0)