keystone
Version:
Web Application Framework and Admin GUI / Content Management System built on Express.js and Mongoose
101 lines (90 loc) • 3.65 kB
JavaScript
var crypto = require('crypto');
var scmp = require('scmp');
var utils = require('keystone-utils');
// The DISABLE_CSRF environment variable is available to automatically pass
// CSRF validation. This is useful in development scenarios where you want to
// restart the node process and aren't using a persistent session store, but
// should NEVER be set in production environments!!
var DISABLE_CSRF = process.env.DISABLE_CSRF === 'true';
exports.TOKEN_KEY = '_csrf';
exports.LOCAL_KEY = 'csrf_token_key';
exports.LOCAL_VALUE = 'csrf_token_value';
exports.SECRET_KEY = exports.TOKEN_KEY + '_secret';
exports.SECRET_LENGTH = 10;
exports.CSRF_HEADER_KEY = 'x-csrf-token';
exports.XSRF_HEADER_KEY = 'x-xsrf-token';
exports.XSRF_COOKIE_KEY = 'XSRF-TOKEN';
function tokenize (salt, secret) {
return salt + crypto.createHash('sha1').update(salt + secret).digest('hex');
}
exports.createSecret = function () {
return crypto.pseudoRandomBytes(exports.SECRET_LENGTH).toString('base64');
};
exports.getSecret = function (req) {
return req.session[exports.SECRET_KEY] || (req.session[exports.SECRET_KEY] = exports.createSecret());
};
exports.createToken = function (req) {
return tokenize(utils.randomString(exports.SECRET_LENGTH), exports.getSecret(req));
};
exports.getToken = function (req, res) {
res.locals[exports.LOCAL_VALUE] = res.locals[exports.LOCAL_VALUE] || exports.createToken(req);
res.cookie(exports.XSRF_COOKIE_KEY, res.locals[exports.LOCAL_VALUE]);
return res.locals[exports.LOCAL_VALUE];
};
exports.requestToken = function (req) {
if (req.body && req.body[exports.TOKEN_KEY]) {
return req.body[exports.TOKEN_KEY];
} else if (req.query && req.query[exports.TOKEN_KEY]) {
return req.query[exports.TOKEN_KEY];
} else if (req.headers && req.headers[exports.XSRF_HEADER_KEY]) {
return req.headers[exports.XSRF_HEADER_KEY];
} else if (req.headers && req.headers[exports.CSRF_HEADER_KEY]) {
return req.headers[exports.CSRF_HEADER_KEY];
}
// JM: If you think we should be checking the req.cookie here you don't understand CSRF.
// On pages loaded from this app (on the same origin) JS will have access to the cookie and should add the CSRF value as one of the headers above.
// Other pages, like those created by an attacker, can still create requests to this app (to which the browser will add cookie information) but,
// since the calling page itself can't access the cookie, it will be unable to add the CSRF header, body or query param to the request.
// The fact that we *don't* check the CSRF value that comes in with the cookie is what makes this CSRF implementation work.
// See.. https://en.wikipedia.org/wiki/Cross-site_request_forgery#Cookie-to-header_token
return '';
};
exports.validate = function (req, token) {
// Allow environment variable to disable check
if (DISABLE_CSRF) return true;
if (arguments.length === 1) {
token = exports.requestToken(req);
}
if (typeof token !== 'string') {
return false;
}
return scmp(
token,
tokenize(
token.slice(0, exports.SECRET_LENGTH),
req.session[exports.SECRET_KEY]
)
);
};
exports.middleware = {
init: function (req, res, next) {
res.locals[exports.LOCAL_KEY] = exports.LOCAL_VALUE;
exports.getToken(req, res);
next();
},
validate: function (req, res, next) {
// Allow environment variable to disable check
if (DISABLE_CSRF) return next();
// Bail on safe methods
if (req.method === 'GET' || req.method === 'HEAD' || req.method === 'OPTIONS') {
return next();
}
// Validate token
if (exports.validate(req)) {
next();
} else {
res.statusCode = 403;
next(new Error('CSRF token mismatch'));
}
},
};