iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

Origin / Referer Check

The Origin header tells the server which origin initiated the request — https://app.example.com, etc. Browsers send it on cross-origin POSTs. Server-side allowlist check + SameSite cookies kill most CSRF.

Origin vs Referer, allowlist, edge cases

EXAMPLE
# 1) The headers
# Origin    : scheme + host + port (no path). Sent on cross-origin requests AND POSTs.
# Referer   : full URL (scheme + host + path + query). May be omitted for privacy.

# When does the browser send Origin?
#   - All cross-origin requests (CORS)
#   - POST / PUT / DELETE / PATCH (including same-origin)
#   - WebSocket / Server-Sent Events / fetch with credentials
#   - NOT sent on same-origin GET requests in some configs

# When does the browser send Referer?
#   - Most requests, but can be omitted via:
#       <meta name="referrer" content="no-referrer">
#       Referrer-Policy: no-referrer
#       rel="noreferrer" on links
#   - Privacy modes (incognito + strict tracking protection) may strip

# 2) Server-side origin check (Express)
const ALLOWED_ORIGINS = new Set([
    'https://app.example.com',
    'https://admin.example.com',
]);

app.use((req, res, next) => {
    if (['POST', 'PUT', 'PATCH', 'DELETE'].includes(req.method)) {
        const origin = req.headers.origin;
        const referer = req.headers.referer;

        // Origin is more reliable than Referer when present
        if (origin) {
            if (!ALLOWED_ORIGINS.has(origin)) {
                return res.status(403).json({ error: 'invalid origin' });
            }
        } else if (referer) {
            // Fall back to Referer
            try {
                const refOrigin = new URL(referer).origin;
                if (!ALLOWED_ORIGINS.has(refOrigin)) {
                    return res.status(403).json({ error: 'invalid referer' });
                }
            } catch {
                return res.status(403).json({ error: 'invalid referer' });
            }
        } else {
            // No Origin or Referer — possibly stripped by privacy mode
            // Decision: reject (strict) or pass to a downstream check like CSRF token (lax)
            return res.status(403).json({ error: 'missing origin and referer' });
        }
    }
    next();
});

# 3) Per-route origin check
function requireOrigin(allowed) {
    return (req, res, next) => {
        if (!allowed.has(req.headers.origin)) {
            return res.status(403).end();
        }
        next();
    };
}

app.post('/api/transfer',
    requireOrigin(new Set(['https://app.example.com'])),
    requireAuth,
    transfer,
);

# 4) Wildcard / pattern allowlist
// Be careful with patterns — don't allow open subdomains
function isAllowedOrigin(origin) {
    if (!origin) return false;
    try {
        const u = new URL(origin);
        // Match specific origins
        if (ALLOWED_ORIGINS.has(origin)) return true;
        // Match preview deploys: https://pr-123.preview.example.com
        if (u.hostname.endsWith('.preview.example.com')) return true;
        return false;
    } catch { return false; }
}

# 5) Cross-origin POST behaviour
# Browser sends Origin on cross-origin POST automatically.
# Attacker page at evil.com submits a form to https://example.com/transfer.
# Browser sends: Origin: https://evil.com
# Server check rejects → CSRF attempt blocked.

# 6) Combine with SameSite cookies (the strongest layer)
res.cookie('sid', sid, {
    sameSite: 'lax',          // browser blocks cross-site cookie on POST/PUT/DELETE
    secure:   true,
    httpOnly: true,
});

# SameSite=Lax already stops 99% of CSRF without any server check.
# Origin check is BELT + BRACES.

# 7) Edge cases

# a) Same-origin POST — Origin is set to the same origin
Origin: https://app.example.com
# Allow if it matches ALLOWED_ORIGINS.

# b) Same-origin GET — Origin may or may not be set
# Don't enforce on GET (idempotent anyway).

# c) Stripped Origin by browser extension / privacy tool
# Server can fall back to Referer, or require CSRF token, or accept the risk
# (very small fraction of users).

# d) Tor browser sets Origin: null
# Decision: handle 'null' explicitly (reject for security, or allow + require CSRF token).
if (origin === 'null') {
    return res.status(403).end();
}

# e) Redirect chain — Origin doesn't change on cross-origin POST after redirect
# Server still sees the original origin.

# 8) CORS vs Origin check
# CORS  : tells the BROWSER what cross-origin requests to permit
# Origin check : tells the SERVER which origins to trust
# Both are needed — they protect different sides.

# 9) Common bugs
#   ❌ Trusting only Referer (can be stripped by user / privacy)
#   ❌ Allowing any subdomain (subdomain takeover → CSRF via Origin)
#   ❌ Comparing as substring (origin.includes('example.com')) → 'evilexample.com' bypass
#   ❌ Lowercasing origin and comparing — origins are case-sensitive
#   ❌ Trusting Origin without checking method (GET is exempt; that's fine)
#   ❌ Setting Access-Control-Allow-Origin: * with credentials — invalid spec; browsers reject

# 10) Always-do checklist
#   ✅ SameSite=Lax (or Strict) on the session cookie
#   ✅ Origin check on mutating requests with explicit allowlist
#   ✅ Reject 'null' origin or require CSRF token
#   ✅ Strict CORS (origin: specific list, not '*')
#   ✅ HTTPS-only — Origin over HTTP is unreliable in MITM scenarios

# 11) Per-framework patterns

# Express — middleware shown above

# Django
# CSRF_TRUSTED_ORIGINS = ['https://app.example.com']
# CsrfViewMiddleware also validates Origin / Referer on POST

# Rails (5+)
# Configured via protect_from_forgery; verifies authenticity token AND Origin matches

# Laravel
# VerifyCsrfToken middleware checks token; sanctum config has stateful_domains for SPA scope

# Spring Security
# csrf-token mechanism + SameSite enforcement; can configure custom origin/referer checks

# 12) Production patterns

# Defence-in-depth stack for mutating endpoints:
#   1. SameSite=Lax session cookie         (browser-side)
#   2. Strict CORS                          (browser-side)
#   3. Origin header validation (allowlist) (server-side)
#   4. CSRF token (synchronizer or double-submit)
#   5. Re-auth for sensitive actions (password / MFA)
#   6. Audit log of mutations

# Any ONE of layers 1-4 stops a textbook CSRF. Stack them; assume any layer can fail.

# 13) Anti-patterns
#   ❌ Only checking Origin OR only checking Referer OR only using CSRF tokens
#   ❌ Trusting the Origin equals 'null' silently
#   ❌ Allowing wildcard subdomains without monitoring takeover risk
#   ❌ Ignoring Origin on the assumption SameSite is enough — older browsers don't enforce
#   ❌ Mixing 'CORS as security' with 'CORS as a flexibility tool'

# 14) Testing
# curl -X POST https://api.example.com/api/transfer \
#     -H 'Origin: https://evil.com' \
#     -H 'Cookie: sid=abc...' \
#     -d 'amount=1000&to=victim'
# Expected: 403 Forbidden

# curl -X POST https://api.example.com/api/transfer \
#     -H 'Origin: https://app.example.com' \
#     -H 'Cookie: sid=abc...' \
#     -d 'amount=1000&to=victim'
# Expected: 200 (or 401 if cookie isn't valid)

# 15) Migration story
# An app without origin checks:
#   1. Add `SameSite=Lax` first (covers most cases) — no breaking changes for same-site users
#   2. Audit cross-origin needs (legitimate embeds, OAuth callbacks)
#   3. Add Origin allowlist on mutating endpoints
#   4. Reject null / missing Origin (start with logging; switch to reject after monitoring)
#   5. Add CSRF token for SameSite=None scenarios

Why it matters

Origin check + SameSite=Lax + strict CORS is the modern CSRF triple-stack. Each blocks a different attack vector; together they make CSRF practically impossible without an additional XSS or subdomain takeover.

Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.

Example

Example
// On state-changing endpoints, verify Origin (preferred) or Referer.
const allowed = ['https://app.example.com'];
if (!allowed.includes(req.headers.origin)) return res.status(403).end();
Try it Yourself »

Exercise

Header used to verify the calling origin.

if (req.headers. !== expected) return res.status(403).end();

Test yourself

Q1. Origin/Referer checks add…
Q2. Origin is preferred to Referer because…
Q3. You should…

Discussion

Loading…