# CORS configuration is restrictive

- **Pattern:** `ab-002395` (`security-hardening.secrets-config.cors-restrictive`)
- **Severity:** low
- **Lifecycle:** active
- **Last modified:** 2026-04-18
- **Canonical URL:** https://auditbuffet.com/patterns/ab-002395
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-002395)

## Why it matters

A CORS misconfiguration that combines `Access-Control-Allow-Origin: *` with `Access-Control-Allow-Credentials: true` allows any website to make authenticated cross-origin API calls using the visitor's session cookies — a variant of CSRF. CWE-942 (Permissive Cross-domain Policy) and OWASP A05 (Security Misconfiguration) cover this. NIST 800-53 SC-7 requires boundary protection. Substring origin validation (e.g., `origin.includes('yoursite.com')`) is trivially bypassed with a domain like `evil-yoursite.com`. The wildcard origin tells browsers to skip the same-origin check entirely, making your API accessible to any script on the internet.

## Severity rationale

Low because CORS misconfiguration requires an attacker to exploit it from a browser context, but wildcard-plus-credentials is a direct bypass of the browser's same-origin protection.

## Remediation

Use an explicit origin allowlist with exact string equality — not substring matching — and vary the response on `Origin`:

```ts
const ALLOWED_ORIGINS = [
  'https://yoursite.com',
  'https://www.yoursite.com',
  ...(process.env.NODE_ENV === 'development' ? ['http://localhost:3000'] : []),
]

export function corsHeaders(origin: string | null) {
  const allowed = origin && ALLOWED_ORIGINS.includes(origin)
    ? origin
    : ALLOWED_ORIGINS[0]!
  return {
    'Access-Control-Allow-Origin': allowed,
    'Access-Control-Allow-Credentials': 'true',
    'Vary': 'Origin',  // Required for correct CDN caching
  }
}
```

Never set `Access-Control-Allow-Origin: *` alongside `Access-Control-Allow-Credentials: true` — browsers will ignore credentials with a wildcard origin, breaking auth anyway.

## Detection

- **ID:** `cors-restrictive`
- **Severity:** `low`
- **What to look for:** List all CORS configuration locations (middleware, framework config, API route headers). For each, check CORS middleware or headers configuration. Look for `Access-Control-Allow-Origin: *` (wildcard) combined with `Access-Control-Allow-Credentials: true`. Check how allowed origins are constructed — a regex or string comparison that can be bypassed (e.g., checking if origin contains `yoursite.com`, which would also match `evil-yoursite.com`).
- **Pass criteria:** CORS headers specify an explicit allowlist of trusted origins. If wildcard is used, credentials are not included. Origins are compared with exact string equality, not substring matching — fewer than 1 wildcard (*) origin in production CORS configuration (meaning 0%). Report: "X CORS config locations found, all Y use restrictive origins."
- **Fail criteria:** `Access-Control-Allow-Origin: *` with credentials. Wildcard subdomains without verifying the full origin. Origin validation using substring matching that can be bypassed.
- **Skip (N/A) when:** The API is not accessed from browsers (server-to-server only) and has no browser clients.
- **Cross-reference:** The `csrf-protection` check covers request forgery which CORS alone cannot prevent.
- **Detail on fail:** `"Access-Control-Allow-Origin: * with Access-Control-Allow-Credentials: true — any site can make authenticated cross-origin requests"` or `"Origin validated with includes() check — evil-yoursite.com would bypass the check for yoursite.com"`
- **Remediation:** Use an explicit origin allowlist with exact matching:

  ```ts
  const ALLOWED_ORIGINS = [
    'https://yoursite.com',
    'https://www.yoursite.com',
    process.env.NODE_ENV === 'development' ? 'http://localhost:3000' : '',
  ].filter(Boolean)

  export function corsHeaders(origin: string | null) {
    const allowed = origin && ALLOWED_ORIGINS.includes(origin) ? origin : ALLOWED_ORIGINS[0]!
    return {
      'Access-Control-Allow-Origin': allowed,
      'Access-Control-Allow-Credentials': 'true',
      'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
      'Vary': 'Origin',  // Important: vary on Origin for correct caching
    }
  }
  ```

## External references

- cwe CWE-942
- owasp A05
- nist SC-7

Taxons: access-control

HTML version: https://auditbuffet.com/patterns/ab-002395
