Skip to content

Commit

Permalink
Merge branch 'amstel-gold-race-2.1.x' into feature/content-security-p…
Browse files Browse the repository at this point in the history
…olicy
  • Loading branch information
designfrontier committed Oct 30, 2015
2 parents 6b2ec33 + 5b18075 commit e564deb
Show file tree
Hide file tree
Showing 30 changed files with 945 additions and 305 deletions.
5 changes: 2 additions & 3 deletions gulpfile_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,20 @@ var assert = require('chai').assert

describe('gulpfile tests', function(){
'use strict';

it('should have an "incrementVersion" function', function(){
assert.isFunction(gulpfile.incrementVersion);
});

it('should increment only patch when incrementing patch', function() {
assert.strictEqual( '1.2.4', gulpfile.incrementVersion('1.2.3', 'patch'));
});

it('should increment minor and roll patch when incrementing minor', function() {
assert.strictEqual( '1.3.0', gulpfile.incrementVersion('1.2.3', 'minor'));
});

it('should increment major and roll minor and patch when incrementing major', function() {
assert.strictEqual( '2.0.0', gulpfile.incrementVersion('1.2.3', 'major'));
});

});
54 changes: 54 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,19 @@ Current state of the config object is right below this. Explanations about the n
action: 'SAMEORIGIN' //the default allows iframes from same domain
, domain: '' //defaults to not used. Only used for 'ALLOW-ORIGIN'
}
, hsts: {
maxAge: 86400 //defaults to 1 day in seconds. All times in seconds
, includeSubDomains: true //optional. Defaults to true
, preload: true //optional. Defaults to true
}
, noCahce: false //defaults to off. This is the nuclear option for caching
, publicKeyPin: { //default is off. This one is complicated read below...
sha256s: ['keynumberone', 'keynumbertwo'] //an array of SHA-256 public key pins see below for how to obtain
, maxAge: 100 //time in seconds for the pin to be in effect
, includeSubdomains: false //whether or not to pin for sub domains as well defaults to false
, reportUri: false //whether or not to report problems to a URL more details below. Defaults to false
, reportOnly: false //if a reportURI is passed and this is set to true it reports and terminates connection
}
}
}
```
Expand All @@ -53,6 +66,47 @@ If set to false this turns off the X-Content-Type-Options header for all browser
Guard is a weird looking word.
Not that we have that out of the way frameguard allows you to specify under what conditions your application may be wrapped in an `iframe`. Setting `action: 'DENY'` means that your site may never be wrapped in an `iframe`. The default is 'SAMEORIGIN' which allows wrapping of your site by essentially your app. The last allowed setting, `action: 'ALLOW-ORIGIN'`, requires that you pass a `domain` value as well. It allows the specified domain to wrap your application in an iframe. All the calculations for `SAMEORIGIN` and `ALLOW-ORIGIN` follow the CORS rules for determining origin. So `www.designfrontier.net` and `designfrontier.net` are different origins.

#### hsts (HTTP Strict Transport Security)
This tells browsers to require HTTPS security if the connection started out as an HTTPS connection. It does not force the connection to switch, it just requires all subsequent requests by the page to use HTTPS if the page was requested with HTTPS. To disable it set `config.security.hsts` to `false`. It is set with a `maxAge` much like caching. The `maxAge` is set in seconds (not ms) and must be a positive number.

The two optional settings: `includeSubDomains` and `preload` are turned on by default. `includeSubDomains` requires any request to a subdmain of the current domain to be HTTPS as well. `preload` is a Google Chrome specific extension that allows you to submit your site for baked-into-the-browser HSTS. With it set you can submit your site to [this page](https://hstspreload.appspot.com/). Both of these can be individually turned off by setting them to false in the config object.

For more information the spec is [available](http://tools.ietf.org/html/draft-ietf-websec-strict-transport-sec-04).

#### noCache
Before using this think long and hard about it. It shuts down all client side caching for the server. All of it. As best as it can be shut down. You can set it to an object `{noEtag: true}` if you want to remove etags as well. If you merely set it to true then all no cache headers will be set but the ETag header will not be removed.

There is now also a `res.noCache` function that allows you to do the same thing but on a per request/route/user (however you program it) basis. This is a much better option then setting noCache server wide.

#### publicKeyPin
This one is a bit of a beast. Before setting it and using it please read: [the spec](https://tools.ietf.org/html/rfc7469), [this mdn article](https://developer.mozilla.org/en-US/docs/Web/Security/Public_Key_Pinning) and [this tutorial](https://timtaubert.de/blog/2014/10/http-public-key-pinning-explained/). It's a great security feature to help prevent man in the middle attacks, but it is also complex.

Enough of the warnings! How do you configure it? The config object above explains it pretty well. Some details about `includeSubdomains`: it pins all sub domains of your site if it is set to true. Turned off by setting it to false.

`reportUri` takes a URL and changes the header so that the browser can corretly handle the reporting of mismatches between pins and your certificate keys. If this is set without `reportOnly` being set to false then it only reports it does not also terminate the connection. Setting `reportOnly` to false means that the connection will be terminated if it does not match the pins as well as reporting.

If you specify a report URI it should be ready to recieve a POST from browsers in the form (described here)[https://tools.ietf.org/html/rfc7469#section-3]. The object you should expect looks like this (sourced from previous link):

```
{
"date-time": date-time,
"hostname": hostname,
"port": port,
"effective-expiration-date": expiration-date,
"include-subdomains": include-subdomains,
"noted-hostname": noted-hostname,
"served-certificate-chain": [
pem1, ... pemN
],
"validated-certificate-chain": [
pem1, ... pemN
],
"known-pins": [
known-pin1, ... known-pinN
]
}
```

## v2.0.0!
Despite it being a major release this is actually a pretty bland one. It's a major release because monument 2+ requires you to be running on node > 4.0.0. It is a rewrite and cleanup in ES6 syntax.

Expand Down
26 changes: 13 additions & 13 deletions routes/isWildCardRoute_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,20 @@ const assert = require('chai').assert
, stubRoutes = parseRoutes(require('../test_stubs/routes_stub.json'));

describe('isWildCardRoute Tests', () => {
it('should return a function', () => {
assert.isFunction(isWildCardRoute);
});
it('should return a function', () => {
assert.isFunction(isWildCardRoute);
});

it('should return true if a wildcard route is passed in', () => {
assert.isTrue(isWildCardRoute('/john-wayne', 'get', stubRoutes.wildcard));
assert.isTrue(isWildCardRoute('/api/articles/1234', 'get', stubRoutes.wildcard));
});
it('should return true if a wildcard route is passed in', () => {
assert.isTrue(isWildCardRoute('/john-wayne', 'get', stubRoutes.wildcard));
assert.isTrue(isWildCardRoute('/api/articles/1234', 'get', stubRoutes.wildcard));
});

it('should return false if a wildcard route is passed in but the verb mismatches', () => {
assert.isFalse(isWildCardRoute('/john-wayne', 'post', stubRoutes.wildcard));
});
it('should return false if a wildcard route is passed in but the verb mismatches', () => {
assert.isFalse(isWildCardRoute('/john-wayne', 'post', stubRoutes.wildcard));
});

it('should return false if the route doesn\'t match any patterns', () => {
assert.isFalse(isWildCardRoute('/api/search', 'get', stubRoutes.wildcard));
});
it('should return false if the route doesn\'t match any patterns', () => {
assert.isFalse(isWildCardRoute('/api/search', 'get', stubRoutes.wildcard));
});
});
48 changes: 24 additions & 24 deletions routes/matchSimpleRoute_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,35 +6,35 @@ const assert = require('chai').assert
, stubRoutes = parseRoutes(require('../test_stubs/routes_stub.json'));

describe('matchSimpleRoute Tests', () => {
it('should return a function', () => {
assert.isFunction(matchSimpleRoute);
});
it('should return a function', () => {
assert.isFunction(matchSimpleRoute);
});

it('should return null for a simple route', () => {
assert.isNotNull(matchSimpleRoute('/api/search', 'get', stubRoutes.standard));
});
it('should return null for a simple route', () => {
assert.isNotNull(matchSimpleRoute('/api/search', 'get', stubRoutes.standard));
});

it('should return null for a non-simple route', () => {
assert.isNull(matchSimpleRoute('/api/article/1234', 'get', stubRoutes.standard));
});
it('should return null for a non-simple route', () => {
assert.isNull(matchSimpleRoute('/api/article/1234', 'get', stubRoutes.standard));
});

it('should return null for a simple route and wrong verb', () => {
assert.isNull(matchSimpleRoute('/api/search', 'post', stubRoutes.standard));
});
it('should return null for a simple route and wrong verb', () => {
assert.isNull(matchSimpleRoute('/api/search', 'post', stubRoutes.standard));
});

it('should return true for /about', function () {
assert.ok(matchSimpleRoute('/about', 'get', stubRoutes.standard));
});
it('should return true for /about', function () {
assert.ok(matchSimpleRoute('/about', 'get', stubRoutes.standard));
});

it('should return true for /', function () {
assert.ok(matchSimpleRoute('/', 'get', stubRoutes.standard));
});
it('should return true for /', function () {
assert.ok(matchSimpleRoute('/', 'get', stubRoutes.standard));
});

it('should not match for /:id', function () {
assert.notOk(matchSimpleRoute('/1234', 'get', stubRoutes.standard));
});
it('should not match for /:id', function () {
assert.notOk(matchSimpleRoute('/1234', 'get', stubRoutes.standard));
});

it('should return true for /about/ handling trailing slash', function () {
assert.ok(matchSimpleRoute('/about/', 'get', stubRoutes.standard));
});
it('should return true for /about/ handling trailing slash', function () {
assert.ok(matchSimpleRoute('/about/', 'get', stubRoutes.standard));
});
});
1 change: 1 addition & 0 deletions routes/router.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ module.exports = (routesJson, config) => {

//add .send to the response
res.send = utils.send(req, config);
res.redirect = utils.redirect(req);

res = setSecurityHeaders(config, req, res);

Expand Down
2 changes: 1 addition & 1 deletion security/frameguard_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ describe('frameguard', () => {
res = {};
res.headers = {};
res.setHeader = function (key, value) {
this.headers[key] = value;
this.headers[key] = value;
};

config.security = {};
Expand Down
35 changes: 35 additions & 0 deletions security/hsts.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
'use strict';

const oneDay = 86400
, isDefined = require('../utils').isDefined;

module.exports = (config, res) => {
const hstsOptions = (isDefined(config.security) && isDefined(config.security.hsts)) ? config.security.hsts : {}
, maxAge = (isDefined(hstsOptions.maxAge)) ? hstsOptions.maxAge : oneDay;

let header = '';

if(hstsOptions !== false) {
if(maxAge < 0) {
throw new Error('maxAge must be a positive number of seconds for the HSTS header');
}

if(typeof maxAge !== 'number'){
throw new Error('maxAge must be a number for the HSTS header');
}

header = 'max-age=' + maxAge;

if(hstsOptions.includeSubDomains !== false) {
header += '; includeSubdomains';
}

if(hstsOptions.preload !== false) {
header += '; preload';
}

res.setHeader('Strict-Transport-Security', header);
}

return res;
};
128 changes: 128 additions & 0 deletions security/hsts_test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
'use strict';

const assert = require('chai').assert
, hsts = require('./hsts');

let res = {}
, config = {};

describe('Security Headers: Strict-Transport-Security Tests', () => {
beforeEach(() => {
res.headers = {};
res.setHeader = function (key, value) {
this.headers[key] = value;
};

config.security = {};
});

it('should return a function', () => {
assert.isFunction(hsts);
});

it('should set a header if there is no option in config', () => {
hsts(config, res);

assert.strictEqual(res.headers['Strict-Transport-Security'], 'max-age=86400; includeSubdomains; preload');
});

it('should set a header if the config is true', () => {
config.security.hsts = true;
hsts(config, res);

assert.strictEqual(res.headers['Strict-Transport-Security'], 'max-age=86400; includeSubdomains; preload');
});

it('should set a header with a different max age if passed in', () => {
config.security.hsts = {
maxAge: 100
};
hsts(config, res);

assert.strictEqual(res.headers['Strict-Transport-Security'], 'max-age=100; includeSubdomains; preload');
});

it('should set a header with a different max age and no includeSubdomains if passed in', () => {
config.security.hsts = {
maxAge: 100
, includeSubDomains: false
};
hsts(config, res);

assert.strictEqual(res.headers['Strict-Transport-Security'], 'max-age=100; preload');
});

it('should set a header with no includeSubdomains if passed in', () => {
config.security.hsts = {
includeSubDomains: false
};
hsts(config, res);

assert.strictEqual(res.headers['Strict-Transport-Security'], 'max-age=86400; preload');
});

it('should set a header with no preload if passed in', () => {
config.security.hsts = {
preload: false
};
hsts(config, res);

assert.strictEqual(res.headers['Strict-Transport-Security'], 'max-age=86400; includeSubdomains');
});

it('should set a header with a different max age and no preloads if passed in', () => {
config.security.hsts = {
maxAge: 100
, preload: false
};
hsts(config, res);

assert.strictEqual(res.headers['Strict-Transport-Security'], 'max-age=100; includeSubdomains');
});

it('should set a header with a different max age and no includeSubdomains or preload if passed in', () => {
config.security.hsts = {
maxAge: 100
, includeSubDomains: false
, preload: false
};
hsts(config, res);

assert.strictEqual(res.headers['Strict-Transport-Security'], 'max-age=100');
});

it('should set a header with a default max age and no includeSubdomains or preload if passed in', () => {
config.security.hsts = {
includeSubDomains: false
, preload: false
};
hsts(config, res);

assert.strictEqual(res.headers['Strict-Transport-Security'], 'max-age=86400');
});

it('should not set a header if the config is false', () => {
config.security.hsts = false;
hsts(config, res);

assert.isUndefined(res.headers['Strict-Transport-Security']);
});

it('should throw an error if a bad value for maxAge is passed in', () => {
config.security.hsts = {
maxAge: -1000
};

assert.throws(() => {hsts(config, res);});

config.security.hsts = {
maxAge: 'Sam I am'
};

assert.throws(() => {hsts(config, res);});
});

it('should return res when executed', () => {
assert.strictEqual(res, hsts(config, res));
});
});
8 changes: 7 additions & 1 deletion security/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@
const poweredBy = require('./poweredByHeader')
, xssHeader = require('./xssHeader')
, noSniff = require('./noSniff')
, noOpen = require('./noOpen');
, noOpen = require('./noOpen')
, hsts = require('./hsts')
, noCache = require('./noCache')
, publicKeyPin = require('./publicKeyPinning');

module.exports = (config, reqIn, resIn) => {
let res = resIn
Expand All @@ -13,6 +16,9 @@ module.exports = (config, reqIn, resIn) => {
res = xssHeader(config, res, req);
res = noSniff(config, res);
res = noOpen(config, res);
res = hsts(config, res);
res = noCache(config, res);
res = publicKeyPin(config, res);

return res;
};

0 comments on commit e564deb

Please sign in to comment.