Credentialed CORS responses allowed for a null origin

Credentialed CORS responses allowed for a null origin

Description

Returning Access-Control-Allow-Origin: null with Access-Control-Allow-Credentials: true can allow opaque origins serialized as null to read responses. Attackers can create such origins with sandboxed iframes or data URLs. If client credential options and cookie policies permit transmission, they may read sensitive authenticated responses.

CORS headers set conditions for reading responses. They do not create credentials or provide server access control that blocks every cross-origin request.

Potential impact

  • Reading a response to a request carrying a victim's cookies may expose account data, personal information or CSRF tokens.
  • Trusting null as an access boundary may weaken protection for internal APIs or administrative endpoints.
  • Response access can make CSRF attacks easier to automate and combine with other access-control failures.

Remediation

  • Do not allow null origins to read sensitive responses. Return a specific allowed origin for credentialed requests; * is not an alternative that permits credentialed response access.
  • Match an exact allow-list of trusted origins before setting Access-Control-Allow-Origin, and return Vary: Origin when the response varies by origin.
  • For unmatched origins, omit the allow-origin and allow-credentials headers and reject OPTIONS preflights with 403.
  • Allow credentialed responses only for necessary routes and origins. Apply server authentication, authorization and CSRF defenses separately.
  • Use SameSite to restrict cross-site cookie transmission, Secure for transport protection and HttpOnly to limit script access.

Examples

These excerpts compare headers and preflight handling for /me. Authentication, authorization and CSRF handling are omitted.

Before

javascript
const express = require("express");
const app = express();

// Allow a null origin together with credentials
app.use((req, res, next) => {
  res.header("Access-Control-Allow-Origin", "null"); // BAD
  res.header("Access-Control-Allow-Credentials", "true"); // BAD
  next();
});

app.get("/me", (req, res) => {
  res.json({ user: "alice", email: "alice@example.com" });
});

After

javascript
const express = require("express");
const app = express();

const ALLOWLIST = new Set([
  "https://app.example.com",
  "https://admin.example.com",
]);

app.use((req, res, next) => {
  const origin = req.headers.origin;
  if (origin && ALLOWLIST.has(origin)) {
    res.setHeader("Access-Control-Allow-Origin", origin);
    res.setHeader("Access-Control-Allow-Credentials", "true");
    res.setHeader("Vary", "Origin");
  } else {
    // Unapproved origin: omit CORS permission and disallow credentialed reading
    res.removeHeader("Access-Control-Allow-Credentials");
  }
  next();
});

// Preflight handling example
app.options("/me", (req, res) => {
  const origin = req.headers.origin;
  if (origin && ALLOWLIST.has(origin)) {
    res.set({
      "Access-Control-Allow-Origin": origin,
      "Access-Control-Allow-Credentials": "true",
      "Access-Control-Allow-Methods": "GET",
      "Access-Control-Allow-Headers": "Content-Type",
      Vary: "Origin",
    });
    return res.sendStatus(204);
  }
  return res.sendStatus(403);
});

app.get("/me", (req, res) => {
  res.json({ user: "alice", email: "alice@example.com" });
});

Explanation:

  • Before: Attackers can obtain a null origin through sandboxed iframes or data URLs. Allowing that origin and credentials may let their scripts read authenticated responses when browser and cookie policies permit credential transmission.
  • After: Only origins in the configured allow-list receive the CORS permissions. Other origins cannot read the response through CORS. Vary: Origin distinguishes origin-dependent responses for caches.

References