The CSRF problem is an old one in the field of front-end security, and there are many technical solutions for it. Today we follow egg-Security to see how mature Web frameworks handle it.

Introduction to CSRF issues

Cross-site request forgery: initiate a request from B.com, it will automatically bring the cookie of a.com. If there is a sensitive ticket in the cookie, there will be an attacker to forge the security problem of the user to send the request

Solution 1: Verify the requestReferrer

In most cases, verifying that the request Referrer is in a valid domain name list prevents 90% of CSRF problems.

But there are some special cases, such as:

  1. HTTPSRelegated to theHTTP.ReferrerGet lost,No Referrer When Downgrade)(searchableReferrer PolicyFor details)
  2. Business requirements, need to support empty Referrer access

Verifying the Referrer is not 100% useful in these scenarios.

At this point, we need to introduce CSRF Token for further verification

Solution 2:CSRF Token

The solution to the problem is that the request carries a token that the attacker cannot obtain. The server checks whether the request carries a valid token to determine whether it is a legitimate request

To sum up, the core logic mainly consists of three parts: token generation, token transmission and token verification

Let’s take a look at how egg-security implements these three main components

File entry analysis

Check from the entry JS index.js and find the CSRF related logical entry:

Then enter. / lib/middlewares/CSRF. Js:

As you can see, the logic of the middleware is very simple, except for some branch judgments, the main two methods are ctx.ensurecSRFSecret and CTx.AsserTCSRf

Seeing CTX., we know that the core processing logic must be in app/extend/context.js, which extends the context object provided by egg.js

ensureCsrfSecret

We found the implementation of the above two core methods (the core method will be interpreted by pasting source code instead of screenshots for easy reading) :

/** * 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 } = this.app.config.security.csrf; if (useSession) { this.session[sessionName] = secret; } else { const cookieOpts = { domain: cookieDomain && cookieDomain(this), signed: false, httpOnly: false, overwrite: true, }; // 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); }}},Copy the code

EnsureCsrfSecret calls ensureCsrfSecret () to generate and cache secret. When useSession is enabled, secret will be cached in session; otherwise, it will be cached in cookies

This is where we find a new tokens object, find its definition

It is clear that the egg-Security core computing logic relies on the CSRF library implementation

/**
 * Create a new secret key synchronously.
 * @public
 */

Tokens.prototype.secretSync = function secretSync () {
  return uid.sync(this.secretLength)
}
Copy the code

secretSyncThe method is relatively simple, also a fixed length random

assertCsrf

/** * 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}`); }},Copy the code

AssertCsrf, as its name implies, does some assertion processing.

Looking directly at the Ctoken branch, we call the this[CSRF_CTOKEN_CHECK]() method

[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'; }},Copy the code
  1. AJAX requests get CSRF tokens from cookies, in which case token === secret (the actual business can be more flexible, as summarized below)
  2. The token for synchronous form requests is always changing (by refreshing the page) to preventBREACHattack

At the same time, we can see that getters for multiple variables are triggered in the [CSRF_CTOKEN_CHECK] method. Let’s look at this in more detail

Client transfertoken[INPUT_TOKEN]

  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;
  },
Copy the code

As you can see, the logic of [INPUT_TOKEN] is very simple: fetch the desired token or secret from the request Query/ request Body/ request Header

Server accesssecretCache –[CSRF_SECRET]

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]; },Copy the code

The server obtains the cache in the same way as the ensureCsrfSecret method: When useSession is enabled, it obtains the cache from the session. Otherwise, the specified value is taken from the cookie

Check than

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'; }Copy the code

This step involves a number of methods in the tokens object. Let’s look at that again

Tokens.prototype.verify = function verify (secret, token) { if (! secret || typeof secret ! == 'string') { return false } if (! token || typeof token ! == 'string') { return false } var index = token.indexOf('-') if (index === -1) { return false } var salt = token.substr(0, index) var expected = this._tokenize(secret, salt) return compare(token, expected) } Tokens.prototype._tokenize = function tokenize (secret, salt) { return salt + '-' + hash(salt + '-' + secret) }Copy the code

As you can see, the verify method recalculates the token from the passed secret and compares it to the passed token

${salt}-${hash(salt-secret)}

Now that we have a clear understanding of the CSRF Token passing, caching, and verification logic, we still have two questions: when will the Token be generated? How do I inject pages?

To generate the token

With Egg-Security’s READMEmd, the answer to this question is obvious

  • tokenGenerated in thectx.csrfVariables on the
  • Inject through a template, attach toFormSubmit the form

When we see ctx.csrf, we know to go to context.js and look for its getter, as follows:

  /**
   * 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) : '';
  },
Copy the code

Get cached secret, call tokens. Create (secret) to generate tokens, and return

Tokens.prototype.create = function create(secret) { if (! secret || typeof secret ! == "string") { throw new TypeError("argument secret is required"); } return this._tokenize(secret, rndm(this.saltLength)); };Copy the code

The create method differs from the verify method in that the salt passed by _tokenize is randomly generated; The salt passed by the _tokenize call to verify is inversely resolved by token.

According to the above analysis, we finally understand the complete process of token generation > transmission > acquisition > verification

Thinking in combination with business practice

Let’s summarize the entire CSRF defense process of Egg-Security based on business practice

  • Token generation mode: Dynamic salt + encryption algorithm (Secret + SALT)

    Salt is randomly generated for each token generated, secret is bound to the login state (re-generated for each login) and cached in the session or written into the cookie

  • Token transfer: * Request Query/request Body/request Header can be carried

  • Token authentication: The server obtains secret from the session or cookie, reversely extracts the salt value from the token, calculates the value using the same encryption algorithm, and compares the calculated result with the transferred token

Combined with the actual business, we need to pay attention to two points:

  1. incsrfIn the source code ofsecretIt’s also a random way of generating. Combined with our business, we canSelect the ones that are strongly related to the login statecookieAnd it is also convenient to communicate with items separated from the front and back ends
  2. inegg-securitytheREADME.mdIn the medium,ctx.csrfThe variable is just injectedformIn the form template,The actual business could be more flexible and take each asynchronous request along with it through a unified encapsulated request librarytokenInstead of asynchronous requests just strap oncookieIn thesecret