Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Security headers #1461

Merged
merged 10 commits into from
May 1, 2014
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