Skip to content

Commit

Permalink
Merge pull request #1461 from nlf/security
Browse files Browse the repository at this point in the history
Security headers
  • Loading branch information
Eran Hammer committed May 1, 2014
2 parents 9f1c62b + 70a2014 commit 9045b3d
Show file tree
Hide file tree
Showing 6 changed files with 465 additions and 1 deletion.
16 changes: 16 additions & 0 deletions docs/Reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,22 @@ When creating a server instance, the following options configure the server's be
- `additionalExposedHeaders` - a strings array of additional headers to `exposedHeaders`. Use this to keep the default headers in place.
- `credentials` - if `true`, allows user credentials to be sent ('Access-Control-Allow-Credentials'). Defaults to `false`.

- `security` - sets some common security related headers. All headers are disabled by default. To enable set `security` to `true` or to an object with
the following options:
- `hsts` - controls the 'Strict-Transport-Security' header. If set to `true` the header will be set to `max-age=15768000`, if specified as a number
the maxAge parameter will be set to that number. Defaults to `true`. You may also specify an object with the following fields:
- `maxAge` - the max-age portion of the header, as a number. Default is `15768000`.
- `includeSubdomains` - a boolean specifying whether to add the `includeSubdomains` flag to the header.
- `xframe` - controls the 'X-Frame-Options' header. When set to `true` the header will be set to `DENY`, you may also specify a string value of
'deny' or 'sameorigin'. To use the 'allow-from' rule, you must set this to an object with the following fields:
- `rule` - either 'deny', 'sameorigin', or 'allow-from'
- `source` - when `rule` is 'allow-from' this is used to form the rest of the header, otherwise this field is ignored. If `rule` is 'allow-from'
but `source` is unset, the rule will be automatically changed to 'sameorigin'.
- `xss` - boolean that controls the 'X-XSS-PROTECTION' header for IE. Defaults to `true` which sets the header to equal '1; mode=block'.
- `noOpen` - boolean controlling the 'X-Download-Options' header for IE, preventing downloads from executing in your context. Defaults to `true` setting
the header to 'noopen'.
- `noSniff` - boolean controlling the 'X-Content-Type-Options' header. Defaults to `true` setting the header to its only and default option, 'nosniff'.

- `debug` - controls the error types sent to the console:
- `request` - a string array of request log tags to be displayed via `console.error()` when the events are logged via `request.log()`. Defaults
to uncaught errors thrown in external code (these errors are handled automatically and result in an Internal Server Error (500) error response) or
Expand Down
9 changes: 9 additions & 0 deletions lib/defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ exports.server = {
// Optional components

cors: false, // CORS headers on responses and OPTIONS requests (defaults: exports.cors): false -> null, true -> defaults, {} -> override defaults
security: false, // Security headers on responses (defaults exports.security): false -> null, true -> defaults, {} -> override defaults
views: undefined // Views engine
};

Expand Down Expand Up @@ -140,6 +141,14 @@ exports.cors = {
credentials: false
};

exports.security = {
hsts: 15768000,
xframe: 'deny',
xss: true,
noOpen: true,
noSniff: true
};


// Server caching

Expand Down
29 changes: 29 additions & 0 deletions lib/response/headers.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ exports.apply = function (request, next) {
}

internals.cors(response, request);
internals.security(response, request);
internals.content(response);
internals.state(response, request, function (err) {

Expand Down Expand Up @@ -128,6 +129,34 @@ internals.matchOrigin = function (origin, cors) {
};


internals.security = function (response, request) {

var security = request.route.security === undefined ? request.server.settings.security : request.route.security;

if (security) {
if (security._hsts) {
response._header('strict-transport-security', security._hsts, { override: false });
}

if (security._xframe) {
response._header('x-frame-options', security._xframe, { override: false });
}

if (security.xss) {
response._header('x-xss-protection', '1; mode=block', { override: false });
}

if (security.noOpen) {
response._header('x-download-options', 'noopen', { override: false });
}

if (security.noSniff) {
response._header('x-content-type-options', 'nosniff', { override: false });
}
}
};


internals.content = function (response) {

var type = response.headers['content-type'];
Expand Down
21 changes: 20 additions & 1 deletion lib/schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,24 @@ internals.viewSchema = function (base) {
};


internals.security = Joi.object({
hsts: [Joi.object({
maxAge: Joi.number(),
includeSubdomains: Joi.boolean()
}), Joi.boolean(), Joi.number()],
xframe: [Joi.boolean(),
Joi.string().valid('sameorigin', 'deny'),
Joi.object({
rule: Joi.string().valid('sameorigin', 'deny', 'allow-from'),
source: Joi.string()
})
],
xss: Joi.boolean(),
noOpen: Joi.boolean(),
noSniff: Joi.boolean()
}).allow(null, false, true);


internals.server = {
app: Joi.object().allow(null),
cache: Joi.alternatives(Joi.string(), internals.cache, Joi.array().includes(internals.cache)).allow(null),
Expand All @@ -64,6 +82,7 @@ internals.server = {
additionalExposedHeaders: Joi.array(),
credentials: Joi.boolean()
}).allow(null, false, true),
security: internals.security,
debug: Joi.object({
request: Joi.array().allow(false)
}).allow(false),
Expand Down Expand Up @@ -113,7 +132,6 @@ internals.server = {
maxSockets: Joi.number().positive().allow(false)
};


internals.route = {
method: Joi.alternatives(Joi.string(), Joi.array().includes(Joi.string()).min(1)).required(),
path: Joi.string().required(),
Expand Down Expand Up @@ -183,6 +201,7 @@ internals.routeConfig = {
expiresAt: Joi.string()
}),
cors: Joi.boolean(),
security: internals.security,
jsonp: Joi.string(),
app: Joi.object().allow(null),
plugins: Joi.object(),
Expand Down
36 changes: 36 additions & 0 deletions lib/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,42 @@ exports = module.exports = internals.Server = function (/* host, port, options *
}
}

// Generate security headers

this.settings.security = Utils.applyToDefaults(Defaults.security, this.settings.security);
if (this.settings.security) {
if (this.settings.security.hsts) {
if (this.settings.security.hsts === true) {
this.settings.security._hsts = 'max-age=15768000';
} else if (typeof this.settings.security.hsts === 'number') {
this.settings.security._hsts = 'max-age=' + this.settings.security.hsts;
} else {
this.settings.security._hsts = 'max-age=' + (this.settings.security.hsts.maxAge || 15768000);
if (this.settings.security.hsts.includeSubdomains) {
this.settings.security._hsts += '; includeSubdomains';
}
}
}

if (this.settings.security.xframe) {
if (this.settings.security.xframe === true) {
this.settings.security._xframe = 'DENY';
} else if (typeof this.settings.security.xframe === 'string') {
this.settings.security._xframe = this.settings.security.xframe.toUpperCase()
} else {
if (this.settings.security.xframe.rule === 'allow-from') {
if (!this.settings.security.xframe.source) {
this.settings.security._xframe = 'SAMEORIGIN';
} else {
this.settings.security._xframe = 'ALLOW-FROM ' + this.settings.security.xframe.source;
}
} else {
this.settings.security._xframe = this.settings.security.xframe.rule.toUpperCase();
}
}
}
}

// Initialize Views

if (this.settings.views) {
Expand Down
Loading

0 comments on commit 9045b3d

Please sign in to comment.