Skip to content

Commit

Permalink
Fixes #93 - Update lasso to add support for CSP nonce
Browse files Browse the repository at this point in the history
  • Loading branch information
patrick-steele-idem committed Jan 12, 2016
1 parent f54c2a3 commit 3df3083
Show file tree
Hide file tree
Showing 26 changed files with 406 additions and 101 deletions.
78 changes: 77 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ There's also a JavaScript API, taglib and a collection of plugins to make your j
- [Complete Configuration](#complete-configuration)
- [Node.js-style Module Support](#nodejs-style-module-support)
- [No Conflict Builds](#no-conflict-builds)
- [Content Security Policy Support](#content-security-policy-support)
- [Available Plugins](#available-plugins)
- [Extending Lasso.js](#extending-lassojs)
- [Custom Plugins](#custom-plugins)
Expand Down Expand Up @@ -161,7 +162,6 @@ There's also a JavaScript API, taglib and a collection of plugins to make your j
* Optional Base64 image encoding inside CSS files
* Custom output transforms
* Declarative browser-side package dependencies using simple `browser.json` files
* Generates the HTML markup required to include bundled resources
* Conditional dependencies
* Image minification
* etc.
Expand All @@ -174,6 +174,7 @@ There's also a JavaScript API, taglib and a collection of plugins to make your j
* Full support for [browserify](http://browserify.org/) shims and transforms
* Maintains line numbers in wrapped code
* Developer Friendly
* Generates the HTML markup required to include bundled resources
* Disable bundling and minification in development
* Line numbers are maintained for Node.js modules source
* Extremely fast _incremental builds_!
Expand All @@ -199,6 +200,8 @@ There's also a JavaScript API, taglib and a collection of plugins to make your j
* Integrate with build tools
* Use with Express or any other web development framework
* JavaScript API, CLI and taglib
* Security
* Supports the [nonce attribute](https://www.w3.org/TR/CSP2/#script-src-the-nonce-attribute) when using Content Security Policy for extra security.
* _Future_
* Automatic image sprites

Expand Down Expand Up @@ -1359,6 +1362,79 @@ require('lasso').create({
See [Configuration](#configuration) for full list of configuration options.
# Content Security Policy Support
Newer browsers support a web standard called Content Security Policy that prevents, among other things, cross-site scripting attacks by whitelisting inline `<script>` and `<style>` tags (see [HTML5 Rocks: An Introduction to Content Security Policy](http://www.html5rocks.com/en/tutorials/security/content-security-policy/)). The Lasso.js taglib for Marko is used to inject the `<script>` and `<style>` tags into the HTML output and Lasso.js provides support for injecting a nonce attribute. When Lasso.js is configured you just need to register a `cspNonceProvider` as shown below:
```javascript
require('lasso').configure({
cspNonceProvider: function(out) {
// Logic for determining the nonce will vary, but the following is one option:
var res = out.stream;
var nonce = res.csp && res.csp.nonce;

// NOTE:
// The code above assumes that there is some middleware that
// stores the nonce into a [non-standard] `res.csp.nonce` variable.
// Use whatever is appropriate for your app.
return nonce; // A string value
}
});
```
A Lasso.js plugin can also be used to register the CSP nonce provider as shown below:
```javascript
module.exports = function(lasso, pluginConfig) {
lasso.setCSPNonceProvider(function(out) {
return 'abc123';
})
};
```
Registering a `cspNonceProvider` will result in a `nonce` attribute being added to all inline `<script>` and `<style>` tags rendered in either the `head` slot (`<lasso-head/>`) or the `body` slot (`<lasso-body/>`).
With a CSP nonce enable, the HTML output for a page rendered using Marko might be similar to the following:
```html
<html>
<head>
<!-- BEGIN head slot: -->
<link rel="stylesheet" type="text/css" href="/static/page1-8b866529.css">
<style type="text/css" nonce="abc123">
body .inline {
background-color: red;
}
</style>
<!-- END head slot -->
</head>
<body>
<!-- BEGIN body slot: -->
<script type="text/javascript" src="/static/page1-1097e0f6.js"></script>
<script type="text/javascript" nonce="abc123">
console.log('hello-inline');
</script>
<!-- END body slot -->
</body>
</html>
```
NOTE: A `nonce` attribute is only added to inline `<script>` and `<style>` tags.
As an extra convenience, Lasso.js also supports a custom `lasso-nonce` attribute that can be dropped onto any HTML tag in your Marko template files as shown below:
```xml
<script type="text/javascript" lasso-nonce>console.log('My inline script')</script>
<style type="text/css" lasso-nonce>.my-inline-style { }</style>
```
The output HTML will be similar to the following:
```html
<script type="text/javascript" nonce="abc123">console.log('My inline script')</script>
<style type="text/css" nonce="abc123">.my-inline-style { }</style>
```
# Available Plugins
Expand Down
5 changes: 5 additions & 0 deletions lib/Config.js
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ function Config(params) {
this.cacheDir = null;
this._requirePluginConfig = {};
this._imagePluginConfig = {};
this.cspNonceProvider = null;

if (params) {
extend(this.params, params);
Expand Down Expand Up @@ -383,6 +384,10 @@ Config.prototype = {

getBundleReadTimeout: function() {
return this.bundleReadTimeout;
},

setCSPNonceProvider: function(func) {
this.cspNonceProvider = func;
}
};

Expand Down
4 changes: 2 additions & 2 deletions lib/Lasso.js
Original file line number Diff line number Diff line change
Expand Up @@ -576,11 +576,11 @@ Lasso.prototype = {
},

getJavaScriptDependencyHtml: function(url) {
return '<script type="text/javascript" src="' + escapeXmlAttr(url) + '"></script>';
return '<script type="text/javascript" src="' + escapeXmlAttr(url) + '" attrs="data.externalScriptAttrs"></script>';
},

getCSSDependencyHtml: function(url) {
return '<link rel="stylesheet" type="text/css" href="' + escapeXmlAttr(url) + '">';
return '<link rel="stylesheet" type="text/css" href="' + escapeXmlAttr(url) + '" attrs="data.externalStyleAttrs">';
},

_resolveflags: function(options) {
Expand Down
85 changes: 73 additions & 12 deletions lib/LassoPageResult.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,30 @@
var extend = require('raptor-util/extend');
var marko = require('marko');
var nodePath = require('path');
var EMPTY_OBJECT = {};

function generateTempFilename(slotName) {
// Generates a unique filename based on date/time and, process ID and a random number
var now = new Date();
return [
slotName,
now.getYear(),
now.getMonth(),
now.getDate(),
process.pid,
(Math.random() * 0x100000000 + 1).toString(36)
].join('-') + '.marko';
}

function LassoPageResult() {
this.htmlBySlot = {};
this.urlsBySlot = {};
this.urlsByContentType = {};
this.filesByContentType = {};
this.infoByBundleName = {};
this.infoByAsyncBundleName = {};
this._htmlBySlot = {};

this._htmlTemplatesBySlot = {};
}

LassoPageResult.deserialize = function(reader, callback) {
Expand Down Expand Up @@ -51,8 +69,16 @@ LassoPageResult.prototype = {
*
* @return {Object} An object with slot names as property names and slot HTML as property values.
*/
getHtmlBySlot: function() {
return this.htmlBySlot;
get htmlBySlot() {
var htmlBySlot = {};
for (var slotName in this._htmlBySlot) {
if (this._htmlBySlot.hasOwnProperty(slotName)) {
var slotHtml = this.getHtmlForSlot(slotName);
htmlBySlot[slotName] = slotHtml;
}
}

return htmlBySlot;
},

/**
Expand All @@ -64,26 +90,54 @@ LassoPageResult.prototype = {
* </js>
*
* @param {String} slotName The name of the slot (e.g. "head" or "body")
* @param {Object} data Input data to the slot that is used to render the actual slot HTML
* @return {String} The HTML for the slot or an empty string if there is no HTML defined for the slot.
*/
getHtmlForSlot: function(slotName) {
return this.htmlBySlot[slotName] || '';
getHtmlForSlot: function(slotName, data) {
var template = this._getSlotTemplate(slotName);
if (!template) {
return '';
}
return template.renderSync(data || EMPTY_OBJECT);
},

_getSlotTemplate: function(slotName) {
var template = this._htmlTemplatesBySlot[slotName];
if (!template) {
var templateSrc = this._htmlBySlot[slotName];
if (!templateSrc) {
return null;
}

getHeadHtml: function() {
return this.getHtmlForSlot('head');
// In order to compile the HTML for the slot into a Marko template, we need to provide a faux
// template path. The path doesn't really matter unless the compiled template needs to import
// external tags or templates.
var templatePath = nodePath.resolve(__dirname, '..', generateTempFilename(slotName));
template = marko.load(templatePath, templateSrc, { preserveWhitespace: true });
// Cache the loaded template:
this._htmlTemplatesBySlot[slotName] = template;

// The Marko template compiled to JS and required. Let's delete it out of the require cache
// to avoid a memory leak
delete require.cache[templatePath + '.js'];
}

return template;
},

getBodyHtml: function() {
return this.getHtmlForSlot('body');
getHeadHtml: function(data) {
return this.getHtmlForSlot('head', data);
},

getBodyHtml: function(data) {
return this.getHtmlForSlot('body', data);
},

/**
* Synonym for {@Link raptor/lasso/LassoPageResult#getHtmlForSlot}
*/
getSlotHtml: function(slot) {
return this.getHtmlForSlot(slot);
getSlotHtml: function(slotName, data) {
return this.getHtmlForSlot(slotName, data);
},

/**
Expand All @@ -94,8 +148,15 @@ LassoPageResult.prototype = {
return JSON.stringify(this.htmlBySlot, null, indentation);
},

toJSON: function() {
var clone = extend({}, this);
// Don't include the loaded templates when generating a JSON string
delete clone._htmlTemplatesBySlot;
return clone;
},

setHtmlBySlot: function(htmlBySlot) {
this.htmlBySlot = htmlBySlot;
this._htmlBySlot = htmlBySlot;
},

registerBundle: function(bundle, async, lassoContext) {
Expand Down
7 changes: 3 additions & 4 deletions lib/Slot.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ function Slot(contentType) {
ok(contentType, 'contentType is required');
this.contentType = contentType;
this.content = [];

}

Slot.prototype = {
Expand All @@ -30,16 +29,16 @@ Slot.prototype = {
code: content
});
},

buildHtml: function() {
var output = [];
for (var i=0, len=this.content.length; i<len; i++) {
var content = this.content[i];
if (content.inline) {
if (this.contentType === 'js') {
output.push('<script type="text/javascript">' + content.code + '</script>');
output.push('<script type="text/javascript" attrs="data.inlineScriptAttrs">' + content.code + '</script>');
} else if (this.contentType === 'css') {
output.push('<style type="text/css">' + content.code + '</style>');
output.push('<style type="text/css" attrs="data.inlineStyleAttrs">' + content.code + '</style>');
}
} else {
output.push(content.code);
Expand Down
8 changes: 8 additions & 0 deletions lib/config-loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,14 @@ function load(options, baseDir, filename, configDefaults) {
// dependencies.
config._requirePluginConfig.unbundledTargetPrefix = value;
}
},

cspNonceProvider: function(value) {
if (typeof value !== 'function') {
throw new Error('"cspNonceProvider" should be a function');
}

config.setCSPNonceProvider(value);
}
};

Expand Down
10 changes: 10 additions & 0 deletions marko-taglib.json
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,16 @@
"transformer": {
"path": "./taglib/lasso-resource-tag-transformer"
}
},
"*": {
"attributes": {
"lasso-nonce": {
"ignore": true
}
},
"transformer": {
"path": "./taglib/lasso-nonce-attr-transformer"
}
}
}
}
Loading

0 comments on commit 3df3083

Please sign in to comment.