pug-plugin-trusted-types
v2.0.2
Published
XSS prevention for Pug templates with typesafe exceptions
Downloads
50
Maintainers
Readme
Safe Pug Templates (Pug Trusted Types Plugin)
Hooks into Pug to add Trusted Types checks to key attributes to reduce the risk of XSS.
This plugin focuses on checking URLs, to prevent, e.g. arbitrary strings from reaching <script src>
or
javascript:
URLs from reaching <a href>
.
Without this plugin, the below can lead to XSS.
// Attacker controls x
const x = 'javascript:alert(document.domain)';
// Declare a template
const pug = require('pug');
const template = pug.compile('a(href=x) Link', {});
// Use the template
const html = template({ x });
console.log(html);
//! <a href="javascript:alert(document.domain)">Link</a>
This plugin cannot, by itself, prevent XSS due to intentionally unsafe features but when it finds a use of unsafe features, it warns on them and refuses to output TrustedHTML.
Usage
There are several ways to use safe Pug templates.
The Trusted Types plugin adds require
calls which only work with
code loaded in a CommonJS module context.
Pug compiles templates to JavaScript which it loads by
calling new Function()
so does not
load in a module context.
Webpack integration via pug-loader
Pug-loader makes it easy to compile templates when webpacking.
You do need to configure pug-loader to use this plugin though.
If you're using pug-loader
, your webpack.config.js should probably have something like:
({
rules: [
// When loading Pug, run pug-loader.
{
test: /\.pug$/,
use: [
{
loader: path.resolve('node_modules/pug-loader/index.js'),
options: {
plugins: [
// Any other plugins you use should ideally go first.
require('pug-plugin-trusted-types'),
],
},
// Optionally, configure the plugin. You probably won't need to do this.
filterOptions: {
trustedTypes: {
// See "Plugin Configuration" below.
},
},
},
],
},
// This runs the module-keys babel processor on all JavaScript sources.
{
test: /\.js$/,
use: [
{
loader: path.resolve('node_modules/babel-loader/lib/index.js'),
options: {
plugins: ['module-keys/babel'],
},
},
],
exclude: /node_modules\/(webpack\/buildin|module-keys|path-browserify|process)/,
},
],
})
Requiring Templates
First you need a dependency:
npm install --save pug-require
Then you can load Pug templates by calling require
.
// Adds hooks so that requiring a .pug file loads it as a template.
// Even if you use the default config, you still need to require
// this module before you require the first .pug file.
const { configurePug } = require('pug-require');
configurePug({ /* pug options */ });
// Load a simple template `a(href=x) Link`.
const myTemplate = require('./templates/link.pug');
console.log(myTemplate({ x: 'https://example.com/' }));
//! <a href="https://example.com/">Link</a>
console.log(myTemplate({ x: 'javascript:evil()' }));
//! <a href="about:invalid#TrustedURL">Link</a>
See pug-require for more details.
Inline Templates
First you need a dependency:
npm install --save pug-template-tag
Then you can declare Pug templates inline in JS or TS code.
const pug = require('pug-template-tag');
const myTemplate = pug`a(href=x) Link`;
console.log(myTemplate({ x: 'https://example.com/' }));
//! <a href="https://example.com/">Link</a>
console.log(myTemplate({ x: 'javascript:evil()' }));
//! <a href="about:invalid#TrustedURL">Link</a>
See pug-template-tag for more details including how to configure templates.
Pre-compiled or manually compiled Templates
First you need to install Pug and the Trusted Types Plugin.
npm install --save pug pug-plugin-trusted-types
Then add the plugin to the plugins
field of your Pug options object.
Before
const pug = require('pug');
const myTemplate = pug.compile(
templateCode,
{
// Options
});
After
const pug = require('pug');
const pugPluginTT = require('pug-plugin-trusted-types/plugin');
const myTemplate = pug.compile(
templateCode,
{
plugins: [ pugPluginTT ],
// Options
});
Since the Trusted Types Plugin provides security checks, it should ideally
run after plugins that do not aim to provide security guarantees.
Putting it at the end of any existing plugins
array should suffice.
postCodeGen stage plugins could undo security guarantees even if the trusted types plugin runs late.
Double checking expressions
Expressions in Pug templates, whether for attribute values or for text nodes, are double-checked as described below.
| Pug Example | Value of X | Policy |
| ------------------ | ------------------------------- | ------------------------------ |
| div(title=x)
| Ordinary attribute value | |
| | Any value | No change |
| a(href=x)
| External URL attribute | TrustedURL.sanitize |
| | Constant expression | No change |
| | http:
... | No change |
| | https:
... | No change |
| | mailto:
... | No change |
| | TrustedURL | No change |
| | TrustedResourceURL | No change |
| | Other | Replaced with about:invalid
|
| script(src=x)
| URL loaded into same origin | |
| | Constant expression | No change |
| | TrustedResourceURL | No change |
| | Other | Replaced with about:invalid
|
| p =x
| Text in a normal element | |
| | Constant expression | Auto-escaped unless !=
|
| | TrustedHTML | No change |
| | Other | Auto-escaped |
| script =x
| Text in <script>
element | |
| | Constant expression | No change |
| | TrustedScript | No change |
| | Other | Replaced with space |
| iframe(srcdoc=x)
| HTML in attribute | |
| | Constant expression | No change |
| | TrustedHTML | Escaped once to embed as value |
| | Other | Escaped twice to embed in HTML |
It doesn't matter whether an attribute value appears via assignment as
in element(attribute=expression)
or in an attribute block like
element()&attributes({ attribute: expression })
.
Automagic
CSRF (Cross-Site Request Forgery) Protection
CSRF Protection works by putting enough information in <form>
s so
that the server can double check that it served the form.
Configure the plugin with options like
{
"csrfInputName": "csrf",
"csrfInputValueExpression": "csrfTokenValue"
}
When rendering HTML, pass a value to the template for the CSRF input value expression:
let templateInput = {
"csrfTokenValue": "r4Nd0M_NuM83R"
};
Any form in your PUG template like:
form(action='delete' method='POST')
button(type='submit') Delete
will have a hidden input added:
<form action="delete" method="POST">
<input name="csrf" type="hidden" value="r4Nd0M_NuM83R"/>
<button type="submit">Delete</button>
</form>
Configuring with csrf-crypto
If you use csrf-crypto and you're plugging in via pug-require, then the pieces fit together like:
// Configure pug-require to thread
const pugRequire = require('pug-require');
pugRequire.configurePug({
filterOptions: {
trustedTypes: {
csrfInputName: '_csrf',
// You could use 'res.getFormToken()' as the value expression
// if you pass res to Pug.
csrfInputValueExpression: 'csrfToken',
},
},
});
// Load a pug template after configuring pug-require
const template = require('./path/to/template.pug');
// Setup csrf-crypto to define res.getFormToken().
const csrfCrypt = require('csrf-crypto');
app.use(csrfCrypto({ key: applicationLevelSecret, /* ... */ }));
app.use(csrfCrypto.enforcer());
// When rendering HTML output using pug, provide access to the form token.
function handle(req, res) {
// ...
res.end(
template({
get csrfToken() {
// Lazily generate a form token.
delete this.csrfToken;
this.csrfToken = res.getFormToken();
return this.csrfToken;
},
}));
}
Content-Security-Policy
Strict CSP explains how to use the Content-Security-Policy header to protect against XSS:
To enable a strict CSP policy, most applications will need to make the following changes:
- Add a nonce attribute to all
<script>
elements. Some template systems can do this automatically.- Refactor any markup with inline event handlers (onclick, etc.) and javascript: URIs (details).
- For every page load, generate a new nonce, pass it the to the template system, and use the same value in the policy.
To automatically add nonce
attributes,
configure the plugin with options like
{
"nonceValueExpression": "sessionScopedRandomString"
}
And then generate a strong nonce for each HTTP response, and pass it to your template:
let templateInput = {
// https://csp.withgoogle.com/docs/faq.html#generating-nonces says > 128b = 16B
sessionScopedRandomString: require('uid-safe').sync(18),
};
Caveat: Do not use npmjs.com/package/nonce. It does not provide strong nonces, nor does it claim to.
Pug that loads CSS or JavaScript will have nonces automatically added.
head
link(rel='stylesheet' src='/styles.css')
script(src='/script.js')
script main()
The output HTML will look like:
<head>
<link rel="stylesheet" src="/styles.css" nonce="7QgTXZjEaat5wrC8JAn0FsBq"/>
<script src="/script.js" nonce="7QgTXZjEaat5wrC8JAn0FsBq"></script>
<script nonce="7QgTXZjEaat5wrC8JAn0FsBq">main()</script>
</head>
If your HTTP response has a header like the below then those CSS and
JavaScript will load, but ones lacking the nonce
attribute will not.
Content-Security-Policy: default-src 'nonce-7QgTXZjEaat5wrC8JAn0FsBq'
Plugin Configuration
Pug doesn't provide a way to directly configure plugins, but this plugin takes into account
({
filtersOptions: {
trustedTypes: {
report() {
// ...
}
}
}
})
csrfInputName
A value for an <input name>
attribute that is automatically added to
<form>
elements to protect against Cross-Site Request Forgery
(CSRF).
Defaults to csrfToken
.
See also CSRF (Cross-Site Request Forgery) Protection.
csrfInputValueExpression
A string containing a JavaScript expression for the value
corresponding to the csrfInputName
.
Defaults to null
. If null
, then <form>
s have no hidden input added.
See also CSRF (Cross-Site Request Forgery) Protection.
nonceValueExpression
A string containing a JavaScript expression for the value of nonce
attribute automatically added to <script>
and <style>
elements.
Defaults to null
. If null
, then nonce
attributes are not added.
See also Content-Security-Policy.
report(message)
Called if the plugin finds a problem with the template.
By default, this is console.warn
.