Skip to content

csrf-middleware

The csrf middleware protects your application against Cross-Site Request Forgery (CSRF) attacks. It uses the double-submit cookie pattern to verify that form submissions and state-changing requests originate from your site.

Installation

The CSRF middleware is included with Remix. No additional installation is needed.

Import

ts
import { csrf, getCsrfToken } from 'remix/csrf-middleware'

API

csrf(options?)

Returns a middleware function that generates and validates CSRF tokens.

ts
let router = createRouter({
  middleware: [
    session(sessionCookie, sessionStorage),
    csrf(),
  ],
})

Session middleware required

The csrf middleware stores the CSRF token in the session. The session middleware must appear before csrf in the middleware chain.

getCsrfToken(context)

Helper function to retrieve the current CSRF token from the request context. Use this to include the token in forms and API requests.

ts
import { getCsrfToken } from 'remix/csrf-middleware'

router.map(route, ({ context }) => {
  let token = getCsrfToken(context)

  return html`
    <form method="post">
      <input type="hidden" name="_csrf" value="${token}" />
      <button type="submit">Submit</button>
    </form>
  `
})

Options

OptionTypeDefaultDescription
fieldNamestring'_csrf'The form field name to check for the CSRF token.
headerNamestring'X-CSRF-Token'The HTTP header name to check for the CSRF token (for API requests).
ignoreMethodsstring[]['GET', 'HEAD', 'OPTIONS']HTTP methods that are exempt from CSRF validation. These are safe methods that should not cause side effects.

How It Works

The middleware uses the double-submit cookie pattern:

  1. Token generation --- On safe method requests (GET, HEAD, OPTIONS), the middleware generates a cryptographically random token and stores it in the session.

  2. Token validation --- On unsafe method requests (POST, PUT, PATCH, DELETE), the middleware reads the token from either:

    • The form body field (default: _csrf)
    • The request header (default: X-CSRF-Token)

    It then compares this token against the one stored in the session. If they do not match, the request is rejected with a 403 Forbidden response.

  3. Safe methods are exempt --- GET, HEAD, and OPTIONS requests are never validated because they should not produce side effects.

Examples

Basic Form Protection

ts
import { createRouter } from 'remix/fetch-router'
import { session } from 'remix/session-middleware'
import { csrf, getCsrfToken } from 'remix/csrf-middleware'
import { formData } from 'remix/form-data-middleware'

let router = createRouter({
  middleware: [
    session(sessionCookie, sessionStorage),
    csrf(),
    formData(),
  ],
})

// Render a form with the CSRF token
router.map(routes.newPost, ({ context }) => {
  let token = getCsrfToken(context)

  return html`
    <form method="post" action="/posts">
      <input type="hidden" name="_csrf" value="${token}" />
      <input type="text" name="title" />
      <textarea name="body"></textarea>
      <button type="submit">Create Post</button>
    </form>
  `
})

API Requests with CSRF Header

For JavaScript-initiated requests, send the token in a header instead of a form field:

ts
// Server: expose the token to the client
router.map(routes.app, ({ context }) => {
  let token = getCsrfToken(context)

  return html`
    <html>
      <head>
        <meta name="csrf-token" content="${token}" />
      </head>
      <body><!-- ... --></body>
    </html>
  `
})

// Client: read the token and include it in fetch requests
let csrfToken = document.querySelector('meta[name="csrf-token"]').content

fetch('/api/posts', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-CSRF-Token': csrfToken,
  },
  body: JSON.stringify({ title: 'Hello' }),
})

Custom Field and Header Names

ts
csrf({
  fieldName: 'authenticity_token',
  headerName: 'X-Authenticity-Token',
})

Released under the MIT License.