Skip to content

CSRF Protection

Oleg Ivanchenko edited this page Oct 18, 2026 · 2 revisions

CSRF Protection

oCMS implements Cross-Site Request Forgery (CSRF) protection using Fetch metadata headers, providing modern and reliable protection against cross-site attacks.

Overview

CSRF attacks exploit authenticated users by tricking them into submitting malicious requests. oCMS protects against this by validating Fetch metadata headers on all state-changing requests (POST, PUT, DELETE, PATCH).

How It Works

The CSRF protection uses Fetch metadata headers (specifically Sec-Fetch-Site) provided by modern browsers:

  1. Fetch Metadata: Browsers automatically include Sec-Fetch-Site header indicating whether the request is same-origin, same-site, cross-site, or none
  2. Validation: For every method except GET, HEAD and OPTIONS, Sec-Fetch-Site must be same-origin or none; same-site and cross-site requests are rejected unless their Origin header exactly matches a trusted origin
  3. Fallback: When Sec-Fetch-Site is missing (legacy browsers), the host of the Origin header must match the request Host; the Referer header is not used. Requests with neither header are treated as non-browser requests (curl, scripts) and are allowed

This approach is more reliable than cookie-based CSRF protection because:

  • It doesn't depend on cookie settings (SameSite, Secure, etc.)
  • It's immune to cookie-related attacks and browser quirks
  • It's the recommended method by browsers for CSRF protection

TrustedOrigins Format

IMPORTANT: oCMS lists trusted origins in host:port format, not as full URLs. The library treats a scheme-less entry as https://host:port and compares it with the request's Origin header exactly, so such an entry never trusts a plain-HTTP origin. The list comes from DefaultCSRFConfig; there is no environment variable for it.

# host:port format (matched as https://localhost:8080, https://127.0.0.1:8080)
localhost:8080
127.0.0.1:8080

Development vs Production

  • Development (OCMS_ENV=development, the default): localhost:8080 and 127.0.0.1:8080 are trusted
  • Production (OCMS_ENV=production): No default trusted origins (same-origin requests only)

AJAX Requests

AJAX requests from the same origin work automatically without needing to include any CSRF tokens. The browser's Fetch metadata headers provide the necessary protection.

// No CSRF token needed
fetch('/api/endpoint', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(data)
});

Where CSRF Applies

The CSRF middleware is attached to the browser-facing routes: /admin/*, /login, /logout, /language, /account and the public form routes (/forms/{slug}). The REST API (/api/v2/*) and the MCP endpoint (/api/mcp) use API-key authentication instead and are not behind the CSRF middleware. /csp-report, the internal analytics beacon and the modules' public routes (such as the Embed module's POST /embed/dify/chat-messages) are outside the CSRF middleware as well.

Troubleshooting

"Forbidden - CSRF validation failed"

Check the server logs for the specific reason:

Error Reason Cause Solution
cross-origin request detected from Sec-Fetch-Site header Sec-Fetch-Site is cross-site or same-site and the Origin is not trusted Ensure request is from same origin
cross-origin request detected, and/or browser is out of date: Sec-Fetch-Site is missing, and Origin does not match Host Legacy browser (or a client that sets Origin) whose Origin host differs from Host Use a modern browser, or check that the reverse proxy forwards the original Host

Common Mistakes

  1. Leaving out the scheme of a plain-HTTP origin: localhost:8080 is matched as https://localhost:8080; write http://localhost:8080 to trust the plain-HTTP origin
  2. Cross-origin requests without TrustedOrigins: Add the origin if legitimate
  3. HTTPS mismatch: A scheme-less trusted origin is matched as https://, so a request from the plain-HTTP variant of that origin is not trusted
  4. Reverse proxy changing Host: Without Sec-Fetch-Site, the Origin host is compared with the Host the application sees; see Reverse Proxy

Testing

# Test that CSRF is enforced (should return 403 for cross-site requests)
curl -X POST http://localhost:8080/login \
  -H "Sec-Fetch-Site: cross-site" \
  -d "email=test@test.com&password=test"

# Same with only an Origin header that does not match Host (also 403)
curl -X POST http://localhost:8080/login \
  -H "Origin: https://evil.example" \
  -d "email=test@test.com&password=test"

# Test with proper headers (simulating same-origin request)
curl -X POST http://localhost:8080/login \
  -H "Origin: http://localhost:8080" \
  -H "Referer: http://localhost:8080/login" \
  -d "email=admin@example.com&password=changeme"

Note: Real browsers automatically include Sec-Fetch-Site headers. curl doesn't include them, so the library compares the Origin header with Host when one is sent; a request with neither header is treated as a non-browser request and allowed, so a bare curl -X POST is not rejected.

Related

Clone this wiki locally