Merge pull request #4762 from asgerf/js/template-sinks-in-code-injection

Approved by erik-krogh, mchammer01
This commit is contained in:
CodeQL CI
2020-12-07 04:35:11 -08:00
committed by GitHub
12 changed files with 225 additions and 196 deletions

View File

@@ -30,6 +30,21 @@ for example, steal cookies containing session information.
</p>
<sample src="examples/CodeInjection.js" />
<p>
The following example shows a Pug template being constructed from user input, allowing attackers to run
arbitrary code via a payload such as <code>#{global.process.exit(1)}</code>.
</p>
<sample src="examples/ServerSideTemplateInjection.js" />
<p>
Below is an example of how to use a template engine without any risk of template injection.
The user input is included via an interpolation expression <code>#{username}</code> whose value is provided
as an option to the template, instead of being part of the template string itself:
</p>
<sample src="examples/ServerSideTemplateInjectionSafe.js" />
</example>
<references>
@@ -40,5 +55,9 @@ OWASP:
<li>
Wikipedia: <a href="https://en.wikipedia.org/wiki/Code_injection">Code Injection</a>.
</li>
<li>
PortSwigger Research Blog:
<a href="https://portswigger.net/research/server-side-template-injection">Server-Side Template Injection</a>.
</li>
</references>
</qhelp>

View File

@@ -18,5 +18,6 @@ import DataFlow::PathGraph
from Configuration cfg, DataFlow::PathNode source, DataFlow::PathNode sink
where cfg.hasFlowPath(source, sink)
select sink.getNode(), source, sink, "$@ flows to here and is interpreted as code.",
source.getNode(), "User-provided value"
select sink.getNode(), source, sink,
"$@ flows to " + sink.getNode().(Sink).getMessageSuffix() + ".", source.getNode(),
"User-provided value"

View File

@@ -0,0 +1,20 @@
const express = require('express')
var pug = require('pug');
const app = express()
app.post('/', (req, res) => {
var input = req.query.username;
var template = `
doctype
html
head
title= 'Hello world'
body
form(action='/' method='post')
input#name.form-control(type='text)
button.btn.btn-primary(type='submit') Submit
p Hello `+ input
var fn = pug.compile(template);
var html = fn();
res.send(html);
})

View File

@@ -0,0 +1,20 @@
const express = require('express')
var pug = require('pug');
const app = express()
app.post('/', (req, res) => {
var input = req.query.username;
var template = `
doctype
html
head
title= 'Hello world'
body
form(action='/' method='post')
input#name.form-control(type='text)
button.btn.btn-primary(type='submit') Submit
p Hello #{username}`
var fn = pug.compile(template);
var html = fn({username: input});
res.send(html);
})

View File

@@ -1,56 +0,0 @@
<!DOCTYPE qhelp PUBLIC
"-//Semmle//qhelp//EN"
"qhelp.dtd">
<qhelp>
<overview>
<p>
Server-Side Template Injection vulnerabilities occur when user input is embedded
in a template in an unsafe manner allowing attackers to access the template context and
run arbitrary code on the application server.
</p>
</overview>
<recommendation>
<p>
Avoid including user input in any expression or template which may be dynamically rendered.
If user input must be included, use context-specific escaping before including it or run
the rendering engine with sandbox options.
</p>
</recommendation>
<example>
<p>
The following example shows a page being rendered with user input allowing attackers to access the
template context and run arbitrary code on the application server.
The Pug template engine (and other template engines) provides an interpolation feature - insertion of variable values into a string of some kind.
For example, <code>Hello #{user.username}!</code>, could be used for printing a username from a scoped variable user,
but the <code>user.username</code> expression will be executed as JavaScript.
Unsafe injection of user input in a template therefore allows an attacker to inject arbitrary JavaScript code.
For example, a payload of <code>#{global.process.exit(1)}</code> will cause the below server to crash.
</p>
<sample src="examples/ServerSideTemplateInjection.js" />
</example>
<example>
<p>
The example below provides an example of how to use a template engine without any risk of Server-Side Template Injection.
Instead of concatenating user input onto the template, the template uses a placeholder and safely inserts
the user input.
</p>
<sample src="examples/ServerSideTemplateInjectionSafe.js" />
</example>
<references>
<li>
OWASP:
<a href="https://owasp.org/www-project-web-security-testing-guide/latest/4-Web_Application_Security_Testing/07-Input_Validation_Testing/18-Testing_for_Server_Side_Template_Injection">Server Side Template Injection</a>.
</li>
<li>
PortSwigger Research Blog:
<a href="https://portswigger.net/research/server-side-template-injection">Server-Side Template Injection</a>.
</li>
</references>
</qhelp>

View File

@@ -1,68 +0,0 @@
/**
* @name Server Side Template Injection
* @description Rendering templates with unsanitized user input allows a malicious user arbitrary
* code execution.
* @kind path-problem
* @problem.severity error
* @precision high
* @id js/server-side-template-injection
* @tags security
* external/cwe/cwe-094
*/
import javascript
import DataFlow
import DataFlow::PathGraph
class ServerSideTemplateInjectionConfiguration extends TaintTracking::Configuration {
ServerSideTemplateInjectionConfiguration() { this = "ServerSideTemplateInjectionConfiguration" }
override predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource }
override predicate isSink(DataFlow::Node sink) { sink instanceof ServerSideTemplateInjectionSink }
}
abstract class ServerSideTemplateInjectionSink extends DataFlow::Node { }
class SSTIPugSink extends ServerSideTemplateInjectionSink {
SSTIPugSink() {
exists(CallNode compile, ModuleImportNode renderImport |
renderImport = moduleImport(["pug", "jade"]) and
(
compile = renderImport.getAMemberCall("compile")
or
compile = renderImport.getAMemberCall("render")
) and
this = compile.getArgument(0)
)
}
}
class SSTIDotSink extends ServerSideTemplateInjectionSink {
SSTIDotSink() {
exists(CallNode compile |
compile = moduleImport("dot").getAMemberCall("template") and
this = compile.getArgument(0)
)
}
}
class SSTIEjsSink extends ServerSideTemplateInjectionSink {
SSTIEjsSink() { this = moduleImport("ejs").getAMemberCall("render").getArgument(0) }
}
class SSTINunjucksSink extends ServerSideTemplateInjectionSink {
SSTINunjucksSink() {
this = moduleImport("nunjucks").getAMemberCall("renderString").getArgument(0)
}
}
class LodashTemplateSink extends ServerSideTemplateInjectionSink {
LodashTemplateSink() { this = LodashUnderscore::member("template").getACall().getArgument(0) }
}
from DataFlow::PathNode source, DataFlow::PathNode sink, ServerSideTemplateInjectionConfiguration c
where c.hasFlowPath(source, sink)
select sink.getNode(), source, sink,
"$@ flows to here and unsafely used as part of rendered template", source.getNode(),
"User-provided value"

View File

@@ -1,35 +0,0 @@
const express = require('express')
var bodyParser = require('body-parser');
const app = express()
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
//Dependent of Templating engine
var jade = require('pug');
const port = 5061
function getHTML(input) {
var template = `
doctype
html
head
title= 'Hello world'
body
form(action='/' method='post')
label(for='name') Name:
input#name.form-control(type='text', placeholder='' name='name')
button.btn.btn-primary(type='submit') Submit
p Hello `+ input
var fn = jade.compile(template);
var html = fn();
console.log(html);
return html;
}
app.post('/', (request, response) => {
var input = request.param('name', "")
var html = getHTML(input)
response.send(html);
})
app.listen(port, () => { console.log(`server is listening on ${port}`) })

View File

@@ -1,34 +0,0 @@
const express = require('express')
var bodyParser = require('body-parser');
const app = express()
app.use(bodyParser.urlencoded({ extended: true }));
//Dependent of Templating engine
var jade = require('pug');
const port = 5061
function getHTML(input) {
var template = `
doctype
html
head
title= 'Hello world'
body
form(action='/' method='post')
label(for='name') Name:
input#name.form-control(type='text', placeholder='' name='name')
button.btn.btn-primary(type='submit') Submit
p Hello #{username}`
var fn = jade.compile(template);
var html = fn({username: input});
console.log(html);
return html;
}
app.post('/', (request, response) => {
var input = request.param('name', "")
var html = getHTML(input)
response.send(html);
})
app.listen(port, () => { console.log(`server is listening on ${port}`) })

View File

@@ -15,7 +15,12 @@ module CodeInjection {
/**
* A data flow sink for code injection vulnerabilities.
*/
abstract class Sink extends DataFlow::Node { }
abstract class Sink extends DataFlow::Node {
/**
* Gets the substitute for `X` in the message `User-provided value flows to X`.
*/
string getMessageSuffix() { result = "here and is interpreted as code" }
}
/**
* A sanitizer for code injection vulnerabilities.
@@ -138,4 +143,57 @@ module CodeInjection {
API::moduleImport("module").getInstance().getMember("_compile").getACall().getArgument(0)
}
}
/** A sink for code injection via template injection. */
abstract private class TemplateSink extends Sink {
override string getMessageSuffix() {
result = "here and is interpreted as a template, which may contain code"
}
}
/**
* A value interpreted as as template by the `pug` library.
*/
class PugTemplateSink extends TemplateSink {
PugTemplateSink() {
this =
DataFlow::moduleImport(["pug", "jade"]).getAMemberCall(["compile", "render"]).getArgument(0)
}
}
/**
* A value interpreted as a tempalte by the `dot` library.
*/
class DotTemplateSink extends TemplateSink {
DotTemplateSink() {
this = DataFlow::moduleImport("dot").getAMemberCall("template").getArgument(0)
}
}
/**
* A value interpreted as a template by the `ejs` library.
*/
class EjsTemplateSink extends TemplateSink {
EjsTemplateSink() {
this = DataFlow::moduleImport("ejs").getAMemberCall("render").getArgument(0)
}
}
/**
* A value interpreted as a template by the `nunjucks` library.
*/
class NunjucksTemplateSink extends TemplateSink {
NunjucksTemplateSink() {
this = DataFlow::moduleImport("nunjucks").getAMemberCall("renderString").getArgument(0)
}
}
/**
* A value interpreted as a template by `lodash` or `underscore`.
*/
class LodashUnderscoreTemplateSink extends TemplateSink {
LodashUnderscoreTemplateSink() {
this = LodashUnderscore::member("template").getACall().getArgument(0)
}
}
}

View File

@@ -118,6 +118,25 @@ nodes
| react-native.js:8:32:8:38 | tainted |
| react-native.js:10:23:10:29 | tainted |
| react-native.js:10:23:10:29 | tainted |
| template-sinks.js:12:9:12:31 | tainted |
| template-sinks.js:12:19:12:31 | req.query.foo |
| template-sinks.js:12:19:12:31 | req.query.foo |
| template-sinks.js:14:17:14:23 | tainted |
| template-sinks.js:14:17:14:23 | tainted |
| template-sinks.js:15:16:15:22 | tainted |
| template-sinks.js:15:16:15:22 | tainted |
| template-sinks.js:16:18:16:24 | tainted |
| template-sinks.js:16:18:16:24 | tainted |
| template-sinks.js:17:17:17:23 | tainted |
| template-sinks.js:17:17:17:23 | tainted |
| template-sinks.js:18:18:18:24 | tainted |
| template-sinks.js:18:18:18:24 | tainted |
| template-sinks.js:19:16:19:22 | tainted |
| template-sinks.js:19:16:19:22 | tainted |
| template-sinks.js:20:27:20:33 | tainted |
| template-sinks.js:20:27:20:33 | tainted |
| template-sinks.js:21:21:21:27 | tainted |
| template-sinks.js:21:21:21:27 | tainted |
| tst.js:2:6:2:22 | document.location |
| tst.js:2:6:2:22 | document.location |
| tst.js:2:6:2:27 | documen ... on.href |
@@ -256,6 +275,24 @@ edges
| react-native.js:7:7:7:33 | tainted | react-native.js:10:23:10:29 | tainted |
| react-native.js:7:17:7:33 | req.param("code") | react-native.js:7:7:7:33 | tainted |
| react-native.js:7:17:7:33 | req.param("code") | react-native.js:7:7:7:33 | tainted |
| template-sinks.js:12:9:12:31 | tainted | template-sinks.js:14:17:14:23 | tainted |
| template-sinks.js:12:9:12:31 | tainted | template-sinks.js:14:17:14:23 | tainted |
| template-sinks.js:12:9:12:31 | tainted | template-sinks.js:15:16:15:22 | tainted |
| template-sinks.js:12:9:12:31 | tainted | template-sinks.js:15:16:15:22 | tainted |
| template-sinks.js:12:9:12:31 | tainted | template-sinks.js:16:18:16:24 | tainted |
| template-sinks.js:12:9:12:31 | tainted | template-sinks.js:16:18:16:24 | tainted |
| template-sinks.js:12:9:12:31 | tainted | template-sinks.js:17:17:17:23 | tainted |
| template-sinks.js:12:9:12:31 | tainted | template-sinks.js:17:17:17:23 | tainted |
| template-sinks.js:12:9:12:31 | tainted | template-sinks.js:18:18:18:24 | tainted |
| template-sinks.js:12:9:12:31 | tainted | template-sinks.js:18:18:18:24 | tainted |
| template-sinks.js:12:9:12:31 | tainted | template-sinks.js:19:16:19:22 | tainted |
| template-sinks.js:12:9:12:31 | tainted | template-sinks.js:19:16:19:22 | tainted |
| template-sinks.js:12:9:12:31 | tainted | template-sinks.js:20:27:20:33 | tainted |
| template-sinks.js:12:9:12:31 | tainted | template-sinks.js:20:27:20:33 | tainted |
| template-sinks.js:12:9:12:31 | tainted | template-sinks.js:21:21:21:27 | tainted |
| template-sinks.js:12:9:12:31 | tainted | template-sinks.js:21:21:21:27 | tainted |
| template-sinks.js:12:19:12:31 | req.query.foo | template-sinks.js:12:9:12:31 | tainted |
| template-sinks.js:12:19:12:31 | req.query.foo | template-sinks.js:12:9:12:31 | tainted |
| tst.js:2:6:2:22 | document.location | tst.js:2:6:2:27 | documen ... on.href |
| tst.js:2:6:2:22 | document.location | tst.js:2:6:2:27 | documen ... on.href |
| tst.js:2:6:2:27 | documen ... on.href | tst.js:2:6:2:83 | documen ... t=")+8) |
@@ -315,6 +352,14 @@ edges
| module.js:9:16:9:29 | req.query.code | module.js:9:16:9:29 | req.query.code | module.js:9:16:9:29 | req.query.code | $@ flows to here and is interpreted as code. | module.js:9:16:9:29 | req.query.code | User-provided value |
| react-native.js:8:32:8:38 | tainted | react-native.js:7:17:7:33 | req.param("code") | react-native.js:8:32:8:38 | tainted | $@ flows to here and is interpreted as code. | react-native.js:7:17:7:33 | req.param("code") | User-provided value |
| react-native.js:10:23:10:29 | tainted | react-native.js:7:17:7:33 | req.param("code") | react-native.js:10:23:10:29 | tainted | $@ flows to here and is interpreted as code. | react-native.js:7:17:7:33 | req.param("code") | User-provided value |
| template-sinks.js:14:17:14:23 | tainted | template-sinks.js:12:19:12:31 | req.query.foo | template-sinks.js:14:17:14:23 | tainted | $@ flows to here and is interpreted as a template, which may contain code. | template-sinks.js:12:19:12:31 | req.query.foo | User-provided value |
| template-sinks.js:15:16:15:22 | tainted | template-sinks.js:12:19:12:31 | req.query.foo | template-sinks.js:15:16:15:22 | tainted | $@ flows to here and is interpreted as a template, which may contain code. | template-sinks.js:12:19:12:31 | req.query.foo | User-provided value |
| template-sinks.js:16:18:16:24 | tainted | template-sinks.js:12:19:12:31 | req.query.foo | template-sinks.js:16:18:16:24 | tainted | $@ flows to here and is interpreted as a template, which may contain code. | template-sinks.js:12:19:12:31 | req.query.foo | User-provided value |
| template-sinks.js:17:17:17:23 | tainted | template-sinks.js:12:19:12:31 | req.query.foo | template-sinks.js:17:17:17:23 | tainted | $@ flows to here and is interpreted as a template, which may contain code. | template-sinks.js:12:19:12:31 | req.query.foo | User-provided value |
| template-sinks.js:18:18:18:24 | tainted | template-sinks.js:12:19:12:31 | req.query.foo | template-sinks.js:18:18:18:24 | tainted | $@ flows to here and is interpreted as a template, which may contain code. | template-sinks.js:12:19:12:31 | req.query.foo | User-provided value |
| template-sinks.js:19:16:19:22 | tainted | template-sinks.js:12:19:12:31 | req.query.foo | template-sinks.js:19:16:19:22 | tainted | $@ flows to here and is interpreted as a template, which may contain code. | template-sinks.js:12:19:12:31 | req.query.foo | User-provided value |
| template-sinks.js:20:27:20:33 | tainted | template-sinks.js:12:19:12:31 | req.query.foo | template-sinks.js:20:27:20:33 | tainted | $@ flows to here and is interpreted as a template, which may contain code. | template-sinks.js:12:19:12:31 | req.query.foo | User-provided value |
| template-sinks.js:21:21:21:27 | tainted | template-sinks.js:12:19:12:31 | req.query.foo | template-sinks.js:21:21:21:27 | tainted | $@ flows to here and is interpreted as a template, which may contain code. | template-sinks.js:12:19:12:31 | req.query.foo | User-provided value |
| tst.js:2:6:2:83 | documen ... t=")+8) | tst.js:2:6:2:22 | document.location | tst.js:2:6:2:83 | documen ... t=")+8) | $@ flows to here and is interpreted as code. | tst.js:2:6:2:22 | document.location | User-provided value |
| tst.js:5:12:5:33 | documen ... on.hash | tst.js:5:12:5:28 | document.location | tst.js:5:12:5:33 | documen ... on.hash | $@ flows to here and is interpreted as code. | tst.js:5:12:5:28 | document.location | User-provided value |
| tst.js:14:10:14:74 | documen ... , "$1") | tst.js:14:10:14:26 | document.location | tst.js:14:10:14:74 | documen ... , "$1") | $@ flows to here and is interpreted as code. | tst.js:14:10:14:26 | document.location | User-provided value |

View File

@@ -122,6 +122,25 @@ nodes
| react-native.js:8:32:8:38 | tainted |
| react-native.js:10:23:10:29 | tainted |
| react-native.js:10:23:10:29 | tainted |
| template-sinks.js:12:9:12:31 | tainted |
| template-sinks.js:12:19:12:31 | req.query.foo |
| template-sinks.js:12:19:12:31 | req.query.foo |
| template-sinks.js:14:17:14:23 | tainted |
| template-sinks.js:14:17:14:23 | tainted |
| template-sinks.js:15:16:15:22 | tainted |
| template-sinks.js:15:16:15:22 | tainted |
| template-sinks.js:16:18:16:24 | tainted |
| template-sinks.js:16:18:16:24 | tainted |
| template-sinks.js:17:17:17:23 | tainted |
| template-sinks.js:17:17:17:23 | tainted |
| template-sinks.js:18:18:18:24 | tainted |
| template-sinks.js:18:18:18:24 | tainted |
| template-sinks.js:19:16:19:22 | tainted |
| template-sinks.js:19:16:19:22 | tainted |
| template-sinks.js:20:27:20:33 | tainted |
| template-sinks.js:20:27:20:33 | tainted |
| template-sinks.js:21:21:21:27 | tainted |
| template-sinks.js:21:21:21:27 | tainted |
| tst.js:2:6:2:22 | document.location |
| tst.js:2:6:2:22 | document.location |
| tst.js:2:6:2:27 | documen ... on.href |
@@ -264,6 +283,24 @@ edges
| react-native.js:7:7:7:33 | tainted | react-native.js:10:23:10:29 | tainted |
| react-native.js:7:17:7:33 | req.param("code") | react-native.js:7:7:7:33 | tainted |
| react-native.js:7:17:7:33 | req.param("code") | react-native.js:7:7:7:33 | tainted |
| template-sinks.js:12:9:12:31 | tainted | template-sinks.js:14:17:14:23 | tainted |
| template-sinks.js:12:9:12:31 | tainted | template-sinks.js:14:17:14:23 | tainted |
| template-sinks.js:12:9:12:31 | tainted | template-sinks.js:15:16:15:22 | tainted |
| template-sinks.js:12:9:12:31 | tainted | template-sinks.js:15:16:15:22 | tainted |
| template-sinks.js:12:9:12:31 | tainted | template-sinks.js:16:18:16:24 | tainted |
| template-sinks.js:12:9:12:31 | tainted | template-sinks.js:16:18:16:24 | tainted |
| template-sinks.js:12:9:12:31 | tainted | template-sinks.js:17:17:17:23 | tainted |
| template-sinks.js:12:9:12:31 | tainted | template-sinks.js:17:17:17:23 | tainted |
| template-sinks.js:12:9:12:31 | tainted | template-sinks.js:18:18:18:24 | tainted |
| template-sinks.js:12:9:12:31 | tainted | template-sinks.js:18:18:18:24 | tainted |
| template-sinks.js:12:9:12:31 | tainted | template-sinks.js:19:16:19:22 | tainted |
| template-sinks.js:12:9:12:31 | tainted | template-sinks.js:19:16:19:22 | tainted |
| template-sinks.js:12:9:12:31 | tainted | template-sinks.js:20:27:20:33 | tainted |
| template-sinks.js:12:9:12:31 | tainted | template-sinks.js:20:27:20:33 | tainted |
| template-sinks.js:12:9:12:31 | tainted | template-sinks.js:21:21:21:27 | tainted |
| template-sinks.js:12:9:12:31 | tainted | template-sinks.js:21:21:21:27 | tainted |
| template-sinks.js:12:19:12:31 | req.query.foo | template-sinks.js:12:9:12:31 | tainted |
| template-sinks.js:12:19:12:31 | req.query.foo | template-sinks.js:12:9:12:31 | tainted |
| tst.js:2:6:2:22 | document.location | tst.js:2:6:2:27 | documen ... on.href |
| tst.js:2:6:2:22 | document.location | tst.js:2:6:2:27 | documen ... on.href |
| tst.js:2:6:2:27 | documen ... on.href | tst.js:2:6:2:83 | documen ... t=")+8) |

View File

@@ -0,0 +1,22 @@
import express from 'express';
import * as pug from 'pug';
import * as jade from 'jade';
import * as dot from 'dot';
import * as ejs from 'ejs';
import * as nunjucks from 'nunjucks';
import * as lodash from 'lodash';
var app = express();
app.get('/some/path', function(req, res) {
let tainted = req.query.foo;
pug.compile(tainted); // NOT OK
pug.render(tainted); // NOT OK
jade.compile(tainted); // NOT OK
jade.render(tainted); // NOT OK
dot.template(tainted); // NOT OK
ejs.render(tainted); // NOT OK
nunjucks.renderString(tainted); // NOT OK
lodash.template(tainted); // NOT OK
});