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.
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.
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 To be backwards compatible with CSP Level 1, <script>
elements.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 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.
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:
Added CSP 1 --> Level 2 section.