Repository navigation
CSRF Protection
oCMS implements Cross-Site Request Forgery (CSRF) protection using Fetch metadata headers, providing modern and reliable protection against cross-site attacks.
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).
The CSRF protection uses Fetch metadata headers (specifically Sec-Fetch-Site) provided by modern browsers:
-
Fetch Metadata: Browsers automatically include
Sec-Fetch-Siteheader indicating whether the request is same-origin, same-site, cross-site, or none -
Validation: For every method except GET, HEAD and OPTIONS,
Sec-Fetch-Sitemust besame-originornone;same-siteandcross-siterequests are rejected unless theirOriginheader exactly matches a trusted origin -
Fallback: When
Sec-Fetch-Siteis missing (legacy browsers), the host of theOriginheader must match the requestHost; 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
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 (
OCMS_ENV=development, the default):localhost:8080and127.0.0.1:8080are trusted -
Production (
OCMS_ENV=production): No default trusted origins (same-origin requests only)
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)
});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.
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
|
-
Leaving out the scheme of a plain-HTTP origin:
localhost:8080is matched ashttps://localhost:8080; writehttp://localhost:8080to trust the plain-HTTP origin - Cross-origin requests without TrustedOrigins: Add the origin if legitimate
-
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 -
Reverse proxy changing
Host: WithoutSec-Fetch-Site, theOriginhost is compared with theHostthe application sees; see Reverse Proxy
# 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.
- Login Security — Rate limiting and account lockout
- hCaptcha — Bot protection on login
- Security — Full security overview
Getting Started
Core Features
- Content Management
- Media Library
- Taxonomy
- Menu Builder
- Forms
- Multi-Language
- SEO
- Feeds
- Video Embedding
- Agent-Ready
Administration
API & Integrations
Security
Theme Development
Module Development
Built-in Modules
- Module: Sentinel
- Module: Developer
- Module: DB Manager
- Module: Migrator
- Module: Informer
- Module: Demo Mode
- Module: Internal Analytics
- Module: External Analytics
- Module: Privacy
- Module: Embed
- Module: MCP Server
Deployment