123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230 |
- 'use strict';
- const safeCurl = require('../../lib/extend/safe_curl');
- const isSafeDomainUtil = require('../../lib/utils').isSafeDomain;
- const nanoid = require('nanoid/non-secure').nanoid;
- const Tokens = require('csrf');
- const debug = require('debug')('egg-security:context');
- const utils = require('../../lib/utils');
- const tokens = new Tokens();
- const CSRF_SECRET = Symbol('egg-security#CSRF_SECRET');
- const _CSRF_SECRET = Symbol('egg-security#_CSRF_SECRET');
- const NEW_CSRF_SECRET = Symbol('egg-security#NEW_CSRF_SECRET');
- const LOG_CSRF_NOTICE = Symbol('egg-security#LOG_CSRF_NOTICE');
- const INPUT_TOKEN = Symbol('egg-security#INPUT_TOKEN');
- const NONCE_CACHE = Symbol('egg-security#NONCE_CACHE');
- const SECURITY_OPTIONS = Symbol('egg-security#SECURITY_OPTIONS');
- const CSRF_REFERER_CHECK = Symbol('egg-security#CSRF_REFERER_CHECK');
- const CSRF_CTOKEN_CHECK = Symbol('egg-security#CSRF_CTOKEN_CHECK');
- function findToken(obj, keys) {
- if (!obj) return;
- if (!keys || !keys.length) return;
- if (typeof keys === 'string') return obj[keys];
- for (const key of keys) {
- if (obj[key]) return obj[key];
- }
- }
- module.exports = {
- get securityOptions() {
- if (!this[SECURITY_OPTIONS]) {
- this[SECURITY_OPTIONS] = {};
- }
- return this[SECURITY_OPTIONS];
- },
- /**
- * Check whether the specific `domain` is in / matches the whiteList or not.
- * @param {string} domain The assigned domain.
- * @return {boolean} If the domain is in / matches the whiteList, return true;
- * otherwise false.
- */
- isSafeDomain(domain) {
- const domainWhiteList = this.app.config.security.domainWhiteList;
- return isSafeDomainUtil(domain, domainWhiteList);
- },
- // Add nonce, random characters will be OK.
- // https://w3c.github.io/webappsec/specs/content-security-policy/#nonce_source
- get nonce() {
- if (!this[NONCE_CACHE]) {
- this[NONCE_CACHE] = nanoid(16);
- }
- return this[NONCE_CACHE];
- },
- /**
- * get csrf token, general use in template
- * @return {String} csrf token
- * @public
- */
- get csrf() {
- // csrfSecret can be rotate, use NEW_CSRF_SECRET first
- const secret = this[NEW_CSRF_SECRET] || this[CSRF_SECRET];
- debug('get csrf token, NEW_CSRF_SECRET: %s, _CSRF_SECRET: %s', this[NEW_CSRF_SECRET], this[CSRF_SECRET]);
- // In order to protect against BREACH attacks,
- // the token is not simply the secret;
- // a random salt is prepended to the secret and used to scramble it.
- // http://breachattack.com/
- return secret ? tokens.create(secret) : '';
- },
- /**
- * get csrf secret from session or cookie
- * @return {String} csrf secret
- * @private
- */
- get [CSRF_SECRET]() {
- if (this[_CSRF_SECRET]) return this[_CSRF_SECRET];
- let { useSession, cookieName, sessionName } = this.app.config.security.csrf;
- // get secret from session or cookie
- if (useSession) {
- this[_CSRF_SECRET] = this.session[sessionName] || '';
- } else {
- // cookieName support array. so we can change csrf cookie name smoothly
- if (!Array.isArray(cookieName)) cookieName = [ cookieName ];
- for (const name of cookieName) {
- this[_CSRF_SECRET] = this.cookies.get(name, { signed: false }) || '';
- if (this[_CSRF_SECRET]) break;
- }
- }
- return this[_CSRF_SECRET];
- },
- /**
- * ensure csrf secret exists in session or cookie.
- * @param {Boolean} rotate reset secret even if the secret exists
- * @public
- */
- ensureCsrfSecret(rotate) {
- if (this[CSRF_SECRET] && !rotate) return;
- debug('ensure csrf secret, exists: %s, rotate; %s', this[CSRF_SECRET], rotate);
- const secret = tokens.secretSync();
- this[NEW_CSRF_SECRET] = secret;
- let { useSession, sessionName, cookieDomain, cookieName, cookieOptions = {} } = this.app.config.security.csrf;
- if (useSession) {
- this.session[sessionName] = secret;
- } else {
- const defaultOpts = {
- domain: cookieDomain && cookieDomain(this),
- signed: false,
- httpOnly: false,
- overwrite: true,
- };
- const cookieOpts = utils.merge(defaultOpts, cookieOptions);
- // cookieName support array. so we can change csrf cookie name smoothly
- if (!Array.isArray(cookieName)) cookieName = [ cookieName ];
- for (const name of cookieName) {
- this.cookies.set(name, secret, cookieOpts);
- }
- }
- },
- get [INPUT_TOKEN]() {
- const { headerName, bodyName, queryName } = this.app.config.security.csrf;
- const token = findToken(this.query, queryName) || findToken(this.request.body, bodyName) ||
- (headerName && this.get(headerName));
- debug('get token %s, secret', token, this[CSRF_SECRET]);
- return token;
- },
- /**
- * rotate csrf secret exists in session or cookie.
- * must rotate the secret when user login
- * @public
- */
- rotateCsrfSecret() {
- if (!this[NEW_CSRF_SECRET] && this[CSRF_SECRET]) {
- this.ensureCsrfSecret(true);
- }
- },
- /**
- * assert csrf token/referer is present
- * @public
- */
- assertCsrf() {
- if (utils.checkIfIgnore(this.app.config.security.csrf, this)) {
- debug('%s, ignore by csrf options', this.path);
- return;
- }
- const { type } = this.app.config.security.csrf;
- let message;
- const messages = [];
- switch (type) {
- case 'ctoken':
- message = this[CSRF_CTOKEN_CHECK]();
- if (message) this.throw(403, message);
- break;
- case 'referer':
- message = this[CSRF_REFERER_CHECK]();
- if (message) this.throw(403, message);
- break;
- case 'all':
- message = this[CSRF_CTOKEN_CHECK]();
- if (message) this.throw(403, message);
- message = this[CSRF_REFERER_CHECK]();
- if (message) this.throw(403, message);
- break;
- case 'any':
- message = this[CSRF_CTOKEN_CHECK]();
- if (!message) return;
- messages.push(message);
- message = this[CSRF_REFERER_CHECK]();
- if (!message) return;
- messages.push(message);
- this.throw(403, `both ctoken and referer check error: ${messages.join(', ')}`);
- break;
- default:
- this.throw(`invalid type ${type}`);
- }
- },
- [CSRF_CTOKEN_CHECK]() {
- if (!this[CSRF_SECRET]) {
- debug('missing csrf token');
- this[LOG_CSRF_NOTICE]('missing csrf token');
- return 'missing csrf token';
- }
- const token = this[INPUT_TOKEN];
- // AJAX requests get csrf token from cookie, in this situation token will equal to secret
- // synchronize form requests' token always changing to protect against BREACH attacks
- if (token !== this[CSRF_SECRET] && !tokens.verify(this[CSRF_SECRET], token)) {
- debug('verify secret and token error');
- this[LOG_CSRF_NOTICE]('invalid csrf token');
- return 'invalid csrf token';
- }
- },
- [CSRF_REFERER_CHECK]() {
- const { refererWhiteList } = this.app.config.security.csrf;
- const referer = (this.headers.referer || '').toLowerCase();
- if (!referer) {
- debug('missing csrf referer');
- this[LOG_CSRF_NOTICE]('missing csrf referer');
- return 'missing csrf referer';
- }
- const host = utils.getFromUrl(referer, 'host');
- const domainList = refererWhiteList.concat(this.host);
- if (!host || !utils.isSafeDomain(host, domainList)) {
- debug('verify referer error');
- this[LOG_CSRF_NOTICE]('invalid csrf referer');
- return 'invalid csrf referer';
- }
- },
- [LOG_CSRF_NOTICE](msg) {
- if (this.app.config.env === 'local') {
- this.logger.warn(`${msg}. See https://eggjs.org/zh-cn/core/security.html#安全威胁csrf的防范`);
- }
- },
- safeCurl,
- };
|