Skip to content

Tutorial: Add COP to Your Server

In this tutorial you will add Cross-Origin Protection (COP) to a Remix server, configure it for different scenarios, and use report-only mode to test before enforcing.

Prerequisites

Step 1: Add the COP Middleware

ts
import { createRouter } from 'remix/fetch-router'
import { cop } from 'remix/cop-middleware'
import { route } from 'remix/fetch-router/routes'

let routes = route({
  home: '/',
  settings: '/settings',
})

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

router.get(routes.home, () => {
  return new Response('Welcome')
})

router.post(routes.settings, () => {
  return new Response('Settings updated')
})

With the default configuration, the middleware allows only same-origin requests. A form submission from https://evil.com targeting your server is blocked with a 403 Forbidden response.

Step 2: Test with Report-Only Mode

Before enforcing COP in production, start with reportOnly mode. This logs violations to the console without blocking requests:

ts
cop({
  reportOnly: true,
})

Check your server logs for entries about blocked requests. Once you are confident no legitimate traffic is being flagged, remove reportOnly to enforce.

Step 3: Allow Same-Site Requests

If your application spans subdomains (e.g., app.mysite.com and api.mysite.com), allow same-site requests:

ts
cop({
  allowedSites: ['same-origin', 'same-site'],
})

The same-site value permits requests from any subdomain of the same registerable domain.

Step 4: Configure Allowed Modes

The Sec-Fetch-Mode header tells you what type of request is being made. The defaults allow navigate (page loads), same-origin (same-origin fetch), and no-cors (images, stylesheets):

ts
cop({
  allowedModes: ['navigate', 'same-origin', 'no-cors', 'cors'],
  allowedDests: ['document', 'empty', 'script', 'style', 'image'],
})

This configuration is more permissive and allows loading scripts, styles, and images cross-origin.

Step 5: Combine with CSRF for Defense in Depth

For maximum protection, use COP as the first line of defense and CSRF tokens as a fallback:

ts
import { cop } from 'remix/cop-middleware'
import { csrf } from 'remix/csrf-middleware'
import { session } from 'remix/session-middleware'

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

COP catches cross-origin requests at the header level. CSRF catches anything that slips through (e.g., from browsers that do not support Sec-Fetch-* headers).

What You Learned

  • How to add COP middleware for tokenless cross-origin protection.
  • How to use report-only mode for safe testing.
  • How to allow same-site or cross-origin requests when needed.
  • How to combine COP with CSRF for layered security.

Next Steps

Released under the MIT License.