Dynamic CORS origins with credentials allowed

Dynamic CORS origins with credentials allowed

Description

Reflecting an unvalidated request Origin or other input in Access-Control-Allow-Origin, together with Access-Control-Allow-Credentials: true, can let an untrusted origin read responses. Sending credentials also depends on the client's request options and cookie SameSite and third-party policies. If those conditions allow an authenticated response to be read by a malicious origin, sensitive information may be exposed.

CORS governs browser access to responses; it does not replace server authentication, authorization or CSRF defenses. Some cross-origin requests can reach the server without CORS response headers.

Potential impact

  • A malicious origin may read sensitive responses to requests carrying credentials.
  • If server authentication, authorization or CSRF defenses are also insufficient, requests may change data with the user's permissions.
  • Other access-control failures, such as IDOR, may allow additional resources to be read or changed.
  • Incorrect CORS headers may expose responses from internal APIs reachable by the browser.

Remediation

  • List exact allowed origins in a Set or Map, and set Access-Control-Allow-Origin only to a matching origin. If using a regex, fix the scheme, host and port precisely.
  • Do not reflect unvalidated origin, query or header values. Do not use Access-Control-Allow-Origin: "null" for these sensitive responses.
  • Set Access-Control-Allow-Credentials: true only for allowed origins. Otherwise omit it or reject the request with 403.
  • Return Vary: Origin when responses depend on the origin so caches can distinguish them.
  • Validate OPTIONS preflights, allow only needed methods and headers, and reject unsupported combinations.
  • Use SameSite to restrict cross-site cookie transmission, Secure for transport protection and HttpOnly to limit script access. Apply separate CSRF validation to sensitive operations.

Examples

These excerpts compare CORS headers. Authentication, authorization and CSRF handling are omitted.

Before

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

// Reflect the request Origin or query value and always allow credentials
app.use((req, res, next) => {
  const o = req.get("Origin") || req.query.o || "null";
  res.set("Access-Control-Allow-Origin", o);
  res.set("Access-Control-Allow-Credentials", "true");
  next();
});

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

After

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

// Apply an exact allow-list and Vary: Origin
const ALLOWLIST = new Set([
  "https://app.example.com",
  "https://admin.example.com",
]);

app.use((req, res, next) => {
  const origin = req.get("Origin");
  if (origin && ALLOWLIST.has(origin)) {
    res.set("Access-Control-Allow-Origin", origin);
    res.set("Access-Control-Allow-Credentials", "true");
    res.set("Vary", "Origin");
  } else {
    // Omit CORS permissions for unmatched origins, or return 403
    // res.status(403).end(); // Optional, according to policy
  }
  next();
});

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

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

Explanation:

  • Before: The server reflects an unvalidated origin and allows credentialed response access. A malicious origin may read authenticated responses when request options and cookie policy permit credentials to be sent.
  • After: Only an exact allow-list match receives permission to read the response; preflights use the same list. Omitting headers does not block every request or all cookie transmission. Vary: Origin tells caches that the response depends on the origin.

References