333
schangxiang@126.com
2025-09-19 18966e02fb573c7e2bb0c6426ed792b38b910940
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
'use strict';
 
const safeCurl = require('../../lib/extend/safe_curl');
const isSafeDomainUtil = require('../../lib/utils').isSafeDomain;
const nanoid = require('nanoid/non-secure');
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');
 
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];
    const { useSession, cookieName, sessionName } = this.app.config.security.csrf;
    // get secret from session or cookie
    if (useSession) {
      this[_CSRF_SECRET] = this.session[sessionName] || '';
    } else {
      this[_CSRF_SECRET] = this.cookies.get(cookieName, { signed: false }) || '';
    }
    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;
    const { useSession, sessionName, cookieDomain, cookieName } = this.app.config.security.csrf;
 
    if (useSession) {
      this.session[sessionName] = secret;
    } else {
      const cookieOpts = {
        domain: cookieDomain && cookieDomain(this),
        signed: false,
        httpOnly: false,
        overwrite: true,
      };
      this.cookies.set(cookieName, 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 is present
   * @public
   */
  assertCsrf() {
    if (utils.checkIfIgnore(this.app.config.security.csrf, this)) {
      debug('%s, ignore by csrf options', this.path);
      return;
    }
 
    if (!this[CSRF_SECRET]) {
      debug('missing csrf token');
      this[LOG_CSRF_NOTICE]('missing csrf token');
      this.throw(403, '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');
      this.throw(403, 'invalid csrf token');
    }
  },
 
  [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,
};