Skip to content

Package

remix/middleware/cop

Cross-origin protection middleware for Remix. It mirrors Go's CrossOriginProtection by rejecting unsafe cross-origin browser requests without synchronizer tokens.

Features

  • Browser Provenance Checks - Uses Sec-Fetch-Site when present and falls back to Origin
  • Trusted Origins - Allow specific cross-origin callers by exact origin
  • Explicit Escape Hatches - Support insecure bypass patterns for endpoints like webhooks
  • No Session State - Does not require synchronizer tokens or server-side CSRF storage

Installation

npm i remix

Usage

import { createRouter } from 'remix/router'
import { cop } from 'remix/middleware/cop'

let router = createRouter({
  middleware: [cop()],
})

Behavior

For requests whose original method is unsafe (POST, PUT, PATCH, DELETE), cop() follows the same broad model as Go's CrossOriginProtection. Routing middleware may change context.method, but method overrides do not change whether cop() checks the request:

  • Allow Sec-Fetch-Site: same-origin
  • Allow Sec-Fetch-Site: none
  • Reject other Sec-Fetch-Site values unless the request matches a trusted origin or insecure bypass
  • If Sec-Fetch-Site is missing, compare Origin to the request host
  • If both Sec-Fetch-Site and Origin are missing, allow the request

This middleware is intentionally tokenless. If you cannot guarantee the deployment assumptions behind that model, prefer csrf-middleware.

Caveats

  • cop() is a browser-origin guard, not a universal CSRF solution. It is designed for deployments that can rely on modern browser provenance signals and same-origin request handling.
  • If both Sec-Fetch-Site and Origin are missing on an unsafe request, cop() allows the request to continue. This is intentional so older clients and non-browser callers do not fail closed by default.
  • If Sec-Fetch-Site is missing, cop() only rejects when Origin is present and does not match the request host.
  • If you need stronger guarantees for session-backed form workflows, mixed deployment environments, or requests that should not fall through when browser provenance headers are missing, use csrf-middleware or layer both middlewares together.

Using with csrf-middleware

You can also layer cop() in front of csrf() when you want both browser provenance checks and session-backed synchronizer tokens.

import { createCookie } from 'remix/cookie'
import { createRouter } from 'remix/router'
import { createCookieSessionStorage } from 'remix/session-storage/cookie'
import { session } from 'remix/middleware/session'
import { cop } from 'remix/middleware/cop'
import { csrf } from 'remix/middleware/csrf'

let sessionCookie = createCookie('__session', { secrets: ['secret1'] })
let sessionStorage = createCookieSessionStorage()

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

In this setup, cop() runs first and rejects unsafe cross-origin browser requests early using Sec-Fetch-Site and Origin. Requests that pass cop() continue into csrf(), which still enforces synchronizer-token validation and origin checks for the remaining traffic.

Trusted Origins

import { createRouter } from 'remix/router'
import { cop } from 'remix/middleware/cop'

let router = createRouter({
  middleware: [
    cop({
      trustedOrigins: ['https://admin.example.com'],
    }),
  ],
})

Trusted origins must be exact origin values in the form scheme://host[:port].

Insecure Bypass Patterns

Bypass patterns intentionally weaken protection for specific endpoints. Method prefixes match the original request method, even when routing middleware overrides context.method. They support:

  • Optional method prefixes, for example POST /webhooks/{provider}
  • Exact paths, for example /healthz
  • Trailing-slash subtree patterns, for example /webhooks/
  • Single-segment wildcards with {name}
  • Tail wildcards with {name...}
import { createRouter } from 'remix/router'
import { cop } from 'remix/middleware/cop'

let router = createRouter({
  middleware: [
    cop({
      insecureBypassPatterns: ['POST /webhooks/{provider}', '/healthz'],
    }),
  ],
})

License

See LICENSE