SyntaxStudy
Sign Up
Web Security CORS Pitfalls, Null Origin, and Private Network Access
Web Security Beginner 2 min read

CORS Pitfalls, Null Origin, and Private Network Access

The `null` origin is a little-known CORS pitfall. Some browsers set the `Origin` header to the literal string "null" for requests from sandboxed iframes, data URLs, and file:// URLs. Developers who test locally or see "null" in logs sometimes add it to their allowlist — but attackers can trigger a null origin from a sandboxed iframe on a malicious page, gaining access to any endpoint that trusts the null origin. Never include `null` in your CORS allowlist. Private Network Access (PNA, formerly CORS-RFC1918) is a newer browser security feature that requires public websites to obtain CORS permission before sending requests to private network addresses (192.168.x.x, 10.x.x.x, localhost). Without PNA, a malicious public website could use the victim's browser as a proxy to attack internal services on the victim's home or corporate network. PNA adds a `Access-Control-Request-Private-Network: true` header to preflight requests; internal servers must respond with `Access-Control-Allow-Private-Network: true`. Common CORS debugging mistakes: CORS errors in the browser console indicate a browser-enforced block, not a server error — the request DID reach the server. Check the response headers, not the request. Postman and curl bypass CORS entirely, so they cannot reproduce CORS errors. Use browser DevTools Network tab, filtering for OPTIONS requests, to debug CORS. Overly broad `Access-Control-Allow-Origin: *` on public static assets is fine; never use it on authenticated API endpoints.
Example
// Node.js / Express: robust CORS with allowlist and null-origin protection

const express = require('express');
const app     = express();

const ALLOWED_ORIGINS = new Set([
  'https://app.example.com',
  'https://admin.example.com',
  // Never add 'null' here!
]);

function corsMiddleware(req, res, next) {
  const origin = req.headers.origin;

  // Only set CORS headers if origin is in the allowlist
  // and not the literal string 'null'
  if (origin && origin !== 'null' && ALLOWED_ORIGINS.has(origin)) {
    res.setHeader('Access-Control-Allow-Origin',      origin);
    res.setHeader('Access-Control-Allow-Credentials', 'true');
    res.setHeader('Vary', 'Origin'); // critical: tell caches this varies per origin
  }

  if (req.method === 'OPTIONS') {
    // Preflight response
    res.setHeader('Access-Control-Allow-Methods', 'GET,POST,PUT,PATCH,DELETE,OPTIONS');
    res.setHeader('Access-Control-Allow-Headers', 'Content-Type,Authorization');
    res.setHeader('Access-Control-Max-Age',       '86400');
    // Private Network Access support for internal services
    if (req.headers['access-control-request-private-network']) {
      res.setHeader('Access-Control-Allow-Private-Network', 'true');
    }
    return res.sendStatus(204);
  }

  next();
}

app.use(corsMiddleware);
app.get('/api/data', (req, res) => res.json({ status: 'ok' }));
app.listen(3000);