Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
CSP + SPDY Push

SPDY Push for Externalizing Dynamic Inline Scripts for CSP

This is a rough outline of some thoughts on how to leverage SPDY Push externalize inline <script>s that contain dynamic, page/user-specific configuration and state data to align with CSP.

Problem

We are building web apps where the initial rendering (HTML) is built server-side, and in order for the client-side JavaScript app to take over, the app's configuration and initial state are written to an inline <script> element.

We want to align with CSP and remove all inline <script> elements — or nonce/hash our inline <script> elements for CSP Level 2.

When we create the initial rendering of app on the server, we are using Express State to pass configuration data and the inital-state used to render the initial view to the client. Express State does this by dynamically generating JavaScript which is then put into an inline <script> element.

Our front-end server do not use sessions, and this configuration and initial state data we need to pass to the client needs to be tied to the HTML document. Therefore we need a way to externalize our dynamiclly generated per-page/user JavaScript, while keeping it tightly linked to the HTML document response we're sending the to browser.

CSP 1 --> Level 2

There exist issues and non-backwards compatible changes to CSP in Level 2 that will adversely affect CSP Level 1 browsers. CSP Level 2 allows for inline <script> elements that contain a nonce attribute or the hash of their contents as a source in the Content-Security-Policy HTTP header. This is not backwards compatible with CSP Level 1 since there is no way to whitelist these <script> elements. To be backwards compatible with CSP Level 1, script-src 'unsafe-inline' can be added to the CSP header and if there also is a 'nonce-' or 'sha256-' source, it will negate 'unsafe-inline' in CSP Level 2 browsers.

To us, this means that we must choose a solution that is compatible with both CSP Level 1 and Level 2 in order to have the greatest assurance that our app's will function properly. The approach of using both 'unsafe-inline' with 'sha265-' should work.

Is there a way to detect a client's CSP level of support on the server?

SPDY Push

SPDY and HTTP/2 specify a mechanism for servers to pre-emptively push responses for resources that will be requested from the initially requested resource; e.g., a server can push the response body of an external <script> that the original HTML document being requested will end up requesting.

This server push mechanism allows us to externalize our dynamically generated JavaScript for CSP, while also tying it to the original request for the HTML document — all without needing to use sessions or hacks.

Example

Here is some example code for an Express app using node-spdy and a modified version of Express State:

server.js:

'use strict';

var express  = require('express'),
    exphbs   = require('express-handlebars'),
    expstate = require('express-state'),
    csp      = require('content-security-policy'),
    fs       = require('fs'),
    logger   = require('morgan'),
    spdy     = require('spdy');

// -- Setup Express App --------------------------------------------------------

var app    = express(),
    router = express.Router();

expstate.extend(app);

app.engine('hbs', exphbs({
    defaultLayout: 'main',
    extname      : 'hbs'
}));

app.set('view engine', 'hbs');
app.set('state namespace', 'APP');
app.set('title', 'Express SPDY Test');

// -- Middleware ---------------------------------------------------------------

app.use(logger('tiny'));
app.use(csp.getCSP({'default-src': csp.SRC_SELF}));
app.use(router);
app.use(express.static('./public'));

// -- Routes -------------------------------------------------------------------

router.get('/', function (req, res, next) {
    if (req.isSpdy) {
        res.locals.isSpdy = true;

        res.expose('This content was SPDY Push-ed!', 'spdy');

        res.push('/bootstrap.js', {
            'Content-Type': 'application/javascript'
        }).on('acknowledge', function () {
            this.end(res.exposed.state.toString());
        });
    }

    res.render('home');
});

// -- Locals -------------------------------------------------------------------

app.locals.title = app.get('title');
app.expose(app.get('title'), 'title');

// -- SPDY Server --------------------------------------------------------------

var server = spdy.createServer({
    key : fs.readFileSync('./keys/spdy-key.pem'),
    cert: fs.readFileSync('./keys/spdy-cert.pem')
}, app);

server.listen(3000, function () {
    console.log('express-spdy listening on: %d', 3000);
});

main.hbs:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <title>{{title}}</title>
</head>
<body>

    {{{body}}}

  {{#if isSpdy}}
    <script src="/bootstrap.js"></script>
  {{/if}}
    <script src="/app.js"></script>

</body>
</html>
@ericf

This comment has been minimized.

Copy link
Owner Author

ericf commented Aug 21, 2014

@ericf

This comment has been minimized.

Copy link
Owner Author

ericf commented Aug 22, 2014

Edited CSP 1 --> Level 2 section, based on @oreoshake's comments. When using a nonce or hash source, 'unsafe-inline' will be ignored in CSP Level 2. <-- Which is great!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.