pug-plugin-trusted-types

2.0.2 • Public • Published
Sisyphus Logo

Safe Pug Templates (Pug Trusted Types Plugin)

Build Status Dependencies Status npm Coverage Status Install Size Known Vulnerabilities

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.

Package Sidebar

Install

npm i pug-plugin-trusted-types

Weekly Downloads

12

Version

2.0.2

License

(MIT OR Apache-2.0)

Unpacked Size

43.8 kB

Total Files

4

Last publish

Collaborators

  • mikesamuel