security: cross-site request forgery mitigation

2020-10-20

 | 

~3 min read

 | 

506 words

Security for applications is a huge topic and it’s only growing in importance. Today, I was reviewing the OWASP Cheat Sheet for Cross-Site Request Forgery Prevention.

A few things I learned:

  1. SameSite cookies are effective, but can be too restrictive (using the Strict value) if you want to allow users to navigate to your site from an external link and be logged in (Lax provides a nice balance).
  2. The forbidden headers list is a list of headers that can only be set by the browser and are thus useful for verifying the source and target origin of a request.
  3. The “Double Submit Cookie” solution works if you only accept HTTPS traffic on your server by setting pseudorandom (cryptographically strong) cookies on the client before login.
  4. Custom Request Headers are well suited for AJAX or API endpoints. Employment of this relies on same-origin policy restrictions of browsers. This browser security measure means that sites can access data in another web page only if they share the same origin (hence the name).

The last lesson is when a light bulb clicked. I’d seen wrappers around Axios (though the principle could apply to Fetch too) that had a custom header that finally made sense! It’s there to protect against CSRF attacks!

An example of the wrapper:1

axiosWrapper.ts
import axios, { AxiosInstance, AxiosRequestConfig } from "axios"

export interface IAxiosFactory {
  (config?: AxiosRequestConfig): AxiosInstance
}

export class AxiosService {
  protected _axios: AxiosInstance

  public constructor(
    baseUrl: string,
    urlSegment: string,
    axiosFactory: IAxiosFactory = axios.create,
  ) {
    const headers: any = {
      /**
       * This `X-Custom-Header-Request` header must be implemented in the API your service intends to consume.
       */
      "X-Custom-Header-Request": "1",
    }

    this._axios = axiosFactory({
      baseURL: baseUrl + urlSegment,
      headers,
    })
  }
}

export default AxiosService

On the server side, we can use a custom middleware to check whether the header is present and if not respond with a 400 status code:

antiForgeryMiddleware.ts
function antiForgeryCheck(req, res, next) {
  if (!req.headers[`X-Custom-Header-Request`]) {
    return res.status(400).json(`Missing Custom Header`)
  }
  next()
}

Conclusion

There’s always more to learn about security, but it can often take a few attempts for something to click. When I first saw the use of the custom header, I asked a few questions about it but went away unsatisfied. It was not until I came across the OWASP cheat sheet that the pieces fell in place and the picture came into focus.

Footnotes

  • 1 While this is using a wrapping class around Axios, it’s really using the axios.create by default, which is exactly what the docs suggest:

    const instance = axios.create({
      baseURL: "https://some-domain.com/api/",
      timeout: 1000,
      headers: { "X-Custom-Header": "foobar" },
    })

    It’s a shame that I’ve seen these docs as many times as I have and didn’t recognize that Axios was setting me up for success with that custom header… if only I’d known!

  • 2 Full disclosure: I have not tested this code, however it should be directionally correct at a minimum.

Hi there and thanks for reading! My name's Stephen. I live in Chicago with my wife, Kate, and dog, Finn. Want more? See about and get in touch!