context.js 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230
  1. 'use strict';
  2. const safeCurl = require('../../lib/extend/safe_curl');
  3. const isSafeDomainUtil = require('../../lib/utils').isSafeDomain;
  4. const nanoid = require('nanoid/non-secure').nanoid;
  5. const Tokens = require('csrf');
  6. const debug = require('debug')('egg-security:context');
  7. const utils = require('../../lib/utils');
  8. const tokens = new Tokens();
  9. const CSRF_SECRET = Symbol('egg-security#CSRF_SECRET');
  10. const _CSRF_SECRET = Symbol('egg-security#_CSRF_SECRET');
  11. const NEW_CSRF_SECRET = Symbol('egg-security#NEW_CSRF_SECRET');
  12. const LOG_CSRF_NOTICE = Symbol('egg-security#LOG_CSRF_NOTICE');
  13. const INPUT_TOKEN = Symbol('egg-security#INPUT_TOKEN');
  14. const NONCE_CACHE = Symbol('egg-security#NONCE_CACHE');
  15. const SECURITY_OPTIONS = Symbol('egg-security#SECURITY_OPTIONS');
  16. const CSRF_REFERER_CHECK = Symbol('egg-security#CSRF_REFERER_CHECK');
  17. const CSRF_CTOKEN_CHECK = Symbol('egg-security#CSRF_CTOKEN_CHECK');
  18. function findToken(obj, keys) {
  19. if (!obj) return;
  20. if (!keys || !keys.length) return;
  21. if (typeof keys === 'string') return obj[keys];
  22. for (const key of keys) {
  23. if (obj[key]) return obj[key];
  24. }
  25. }
  26. module.exports = {
  27. get securityOptions() {
  28. if (!this[SECURITY_OPTIONS]) {
  29. this[SECURITY_OPTIONS] = {};
  30. }
  31. return this[SECURITY_OPTIONS];
  32. },
  33. /**
  34. * Check whether the specific `domain` is in / matches the whiteList or not.
  35. * @param {string} domain The assigned domain.
  36. * @return {boolean} If the domain is in / matches the whiteList, return true;
  37. * otherwise false.
  38. */
  39. isSafeDomain(domain) {
  40. const domainWhiteList = this.app.config.security.domainWhiteList;
  41. return isSafeDomainUtil(domain, domainWhiteList);
  42. },
  43. // Add nonce, random characters will be OK.
  44. // https://w3c.github.io/webappsec/specs/content-security-policy/#nonce_source
  45. get nonce() {
  46. if (!this[NONCE_CACHE]) {
  47. this[NONCE_CACHE] = nanoid(16);
  48. }
  49. return this[NONCE_CACHE];
  50. },
  51. /**
  52. * get csrf token, general use in template
  53. * @return {String} csrf token
  54. * @public
  55. */
  56. get csrf() {
  57. // csrfSecret can be rotate, use NEW_CSRF_SECRET first
  58. const secret = this[NEW_CSRF_SECRET] || this[CSRF_SECRET];
  59. debug('get csrf token, NEW_CSRF_SECRET: %s, _CSRF_SECRET: %s', this[NEW_CSRF_SECRET], this[CSRF_SECRET]);
  60. // In order to protect against BREACH attacks,
  61. // the token is not simply the secret;
  62. // a random salt is prepended to the secret and used to scramble it.
  63. // http://breachattack.com/
  64. return secret ? tokens.create(secret) : '';
  65. },
  66. /**
  67. * get csrf secret from session or cookie
  68. * @return {String} csrf secret
  69. * @private
  70. */
  71. get [CSRF_SECRET]() {
  72. if (this[_CSRF_SECRET]) return this[_CSRF_SECRET];
  73. let { useSession, cookieName, sessionName } = this.app.config.security.csrf;
  74. // get secret from session or cookie
  75. if (useSession) {
  76. this[_CSRF_SECRET] = this.session[sessionName] || '';
  77. } else {
  78. // cookieName support array. so we can change csrf cookie name smoothly
  79. if (!Array.isArray(cookieName)) cookieName = [ cookieName ];
  80. for (const name of cookieName) {
  81. this[_CSRF_SECRET] = this.cookies.get(name, { signed: false }) || '';
  82. if (this[_CSRF_SECRET]) break;
  83. }
  84. }
  85. return this[_CSRF_SECRET];
  86. },
  87. /**
  88. * ensure csrf secret exists in session or cookie.
  89. * @param {Boolean} rotate reset secret even if the secret exists
  90. * @public
  91. */
  92. ensureCsrfSecret(rotate) {
  93. if (this[CSRF_SECRET] && !rotate) return;
  94. debug('ensure csrf secret, exists: %s, rotate; %s', this[CSRF_SECRET], rotate);
  95. const secret = tokens.secretSync();
  96. this[NEW_CSRF_SECRET] = secret;
  97. let { useSession, sessionName, cookieDomain, cookieName, cookieOptions = {} } = this.app.config.security.csrf;
  98. if (useSession) {
  99. this.session[sessionName] = secret;
  100. } else {
  101. const defaultOpts = {
  102. domain: cookieDomain && cookieDomain(this),
  103. signed: false,
  104. httpOnly: false,
  105. overwrite: true,
  106. };
  107. const cookieOpts = utils.merge(defaultOpts, cookieOptions);
  108. // cookieName support array. so we can change csrf cookie name smoothly
  109. if (!Array.isArray(cookieName)) cookieName = [ cookieName ];
  110. for (const name of cookieName) {
  111. this.cookies.set(name, secret, cookieOpts);
  112. }
  113. }
  114. },
  115. get [INPUT_TOKEN]() {
  116. const { headerName, bodyName, queryName } = this.app.config.security.csrf;
  117. const token = findToken(this.query, queryName) || findToken(this.request.body, bodyName) ||
  118. (headerName && this.get(headerName));
  119. debug('get token %s, secret', token, this[CSRF_SECRET]);
  120. return token;
  121. },
  122. /**
  123. * rotate csrf secret exists in session or cookie.
  124. * must rotate the secret when user login
  125. * @public
  126. */
  127. rotateCsrfSecret() {
  128. if (!this[NEW_CSRF_SECRET] && this[CSRF_SECRET]) {
  129. this.ensureCsrfSecret(true);
  130. }
  131. },
  132. /**
  133. * assert csrf token/referer is present
  134. * @public
  135. */
  136. assertCsrf() {
  137. if (utils.checkIfIgnore(this.app.config.security.csrf, this)) {
  138. debug('%s, ignore by csrf options', this.path);
  139. return;
  140. }
  141. const { type } = this.app.config.security.csrf;
  142. let message;
  143. const messages = [];
  144. switch (type) {
  145. case 'ctoken':
  146. message = this[CSRF_CTOKEN_CHECK]();
  147. if (message) this.throw(403, message);
  148. break;
  149. case 'referer':
  150. message = this[CSRF_REFERER_CHECK]();
  151. if (message) this.throw(403, message);
  152. break;
  153. case 'all':
  154. message = this[CSRF_CTOKEN_CHECK]();
  155. if (message) this.throw(403, message);
  156. message = this[CSRF_REFERER_CHECK]();
  157. if (message) this.throw(403, message);
  158. break;
  159. case 'any':
  160. message = this[CSRF_CTOKEN_CHECK]();
  161. if (!message) return;
  162. messages.push(message);
  163. message = this[CSRF_REFERER_CHECK]();
  164. if (!message) return;
  165. messages.push(message);
  166. this.throw(403, `both ctoken and referer check error: ${messages.join(', ')}`);
  167. break;
  168. default:
  169. this.throw(`invalid type ${type}`);
  170. }
  171. },
  172. [CSRF_CTOKEN_CHECK]() {
  173. if (!this[CSRF_SECRET]) {
  174. debug('missing csrf token');
  175. this[LOG_CSRF_NOTICE]('missing csrf token');
  176. return 'missing csrf token';
  177. }
  178. const token = this[INPUT_TOKEN];
  179. // AJAX requests get csrf token from cookie, in this situation token will equal to secret
  180. // synchronize form requests' token always changing to protect against BREACH attacks
  181. if (token !== this[CSRF_SECRET] && !tokens.verify(this[CSRF_SECRET], token)) {
  182. debug('verify secret and token error');
  183. this[LOG_CSRF_NOTICE]('invalid csrf token');
  184. return 'invalid csrf token';
  185. }
  186. },
  187. [CSRF_REFERER_CHECK]() {
  188. const { refererWhiteList } = this.app.config.security.csrf;
  189. const referer = (this.headers.referer || '').toLowerCase();
  190. if (!referer) {
  191. debug('missing csrf referer');
  192. this[LOG_CSRF_NOTICE]('missing csrf referer');
  193. return 'missing csrf referer';
  194. }
  195. const host = utils.getFromUrl(referer, 'host');
  196. const domainList = refererWhiteList.concat(this.host);
  197. if (!host || !utils.isSafeDomain(host, domainList)) {
  198. debug('verify referer error');
  199. this[LOG_CSRF_NOTICE]('invalid csrf referer');
  200. return 'invalid csrf referer';
  201. }
  202. },
  203. [LOG_CSRF_NOTICE](msg) {
  204. if (this.app.config.env === 'local') {
  205. this.logger.warn(`${msg}. See https://eggjs.org/zh-cn/core/security.html#安全威胁csrf的防范`);
  206. }
  207. },
  208. safeCurl,
  209. };