CORS = * WITH CREDENTIALS = TRUE

If your CORS policy says any origin can call your API and your cookies say any origin can carry them, you have built a CSRF vending machine. AI generators reproduce this combination because the simplest CORS recipe online is the one that contains the bug.

The scenario referenced below runs on gapbench.vibe-eval.com — a public security benchmark we operate.

The two-line bug

import cors from 'cors'
app.use(cors({ origin: true, credentials: true }))

origin: true in the cors package means “reflect whatever origin the request came from.” credentials: true means “include cookies.” The combination, when paired with cookies that don’t have SameSite=Strict, means any website the user visits can fire authenticated requests at your API and read the responses.

This is not theoretical. Visit attacker-site.example. The page contains JavaScript that does fetch('https://your-api.example/api/me', { credentials: 'include' }). Your server’s response includes Access-Control-Allow-Origin: https://attacker-site.example and Access-Control-Allow-Credentials: true. The browser allows the read. The attacker’s JavaScript now has the response. Same trick with POST and the attacker can perform arbitrary writes on your user’s behalf.

The CORS spec was specifically designed to prevent this. Reflecting the origin and including credentials is the configuration that re-enables exactly what CORS was designed to prevent.

Why this AI codegen pattern survives review

The line cors({ origin: true, credentials: true }) doesn’t look bad. It looks like “we configured CORS.” It looks like more security than cors() with no options. The developer added options, which feels like care. The options are wrong, but the intent looks right.

The AI didn’t write a malicious config. It wrote the config that resolves the CORS errors the developer was getting in the browser console. CORS errors in dev mean cookies don’t propagate, which means auth-protected endpoints break in dev, which means the developer asks the AI to “fix CORS.” The AI’s fix is “be permissive.” The bug ships.

CORS is half of the picture. The other half is cookie attributes. A cookie marked SameSite=Strict won’t be sent on cross-origin requests at all, which kills most cross-origin attacks regardless of CORS configuration. A cookie marked SameSite=Lax (the modern browser default) is sent on top-level navigations but not on background fetches or POSTs. A cookie marked SameSite=None is sent on every cross-origin request — which is almost never what you want unless you have a deliberate cross-origin auth setup.

We see two cookie misconfigurations frequently:

  1. SameSite=None set without thinking, because the dev framework’s example showed it. This pairs with the CORS misconfig to maximize damage.
  2. SameSite not set at all, which used to mean “no protection” but now defaults to Lax in modern browsers. This is the safer default, but the overbroad-domain Domain=.example.com setting can still leak the cookie to subdomains the developer doesn’t control.

Live demo of the missing-CSRF surface: https://gapbench.vibe-eval.com/site/csrf-missing/. The CORS variant: https://gapbench.vibe-eval.com/site/cors-misconfig/. The cookie-scope-leak variant: https://gapbench.vibe-eval.com/site/cookie-scope-leak/.

A specific incident

Anonymized. A SaaS team noticed odd activity in their analytics — a small but consistent stream of requests from attacker-typo.example to their API, all returning 200, all from inside a single user’s browser. Investigation showed the user had visited attacker-typo.example (a phishing copy of an unrelated SaaS), and the page had silently fired requests at every API the user happened to be logged into. Theirs was one of them. CORS reflected the origin, credentials were on, the user’s cookies got sent, the API responded.

The cleanup: the team tightened CORS to an explicit allowlist and added SameSite=Strict to the auth cookie. Both fixes were small. The puzzle was understanding why their CORS was permissive in the first place — none of the team had set it that way deliberately. Git blame led to a Cursor session three months earlier where someone asked “the cookies aren’t propagating to the API in dev, can you fix CORS?” Cursor’s fix: cors({ origin: true, credentials: true }). It made dev work. It also broke the security model.

The takeaway: any time you ask AI to “fix CORS errors,” the AI’s path of least resistance is to make CORS more permissive. Audit that diff specifically. The right fix is almost always to add the dev origin to an allowlist, not to flip to origin: true.

What the right CORS config looks like at scale

Most production apps have a small, fixed set of trusted origins. The configuration should be similarly small and fixed:

const allowedOrigins = [
  'https://app.example.com',           // production frontend
  'https://staging.example.com',       // staging frontend
  ...(process.env.NODE_ENV === 'development'
    ? ['http://localhost:3000', 'http://localhost:5173']
    : []),
]

app.use(cors({
  origin: (origin, cb) => {
    // Same-origin requests have no Origin header — allow
    if (!origin) return cb(null, true)
    if (allowedOrigins.includes(origin)) return cb(null, true)
    cb(new Error(`CORS blocked: ${origin}`))
  },
  credentials: true,
  methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
  allowedHeaders: ['Content-Type', 'Authorization'],
  exposedHeaders: ['X-Request-Id'],
  maxAge: 600,
}))

The pattern that doesn’t work: a regex match against *.example.com. Subdomain takeover (covered in /patterns/s3-and-subdomain-takeover/) gives an attacker a subdomain that matches your regex. Wildcard matches CORS-allow are a known anti-pattern; if you absolutely need them, audit your DNS quarterly.

CSRF in 2026 — what’s still required

The browser default for SameSite is now Lax, which kills most CSRF on its own. But there are gaps:

  1. Cookies that need to cross sites for OAuth or analytics still get set with SameSite=None, which restores the old CSRF surface.
  2. Top-level GETs that mutate state (still a thing in some apps — e.g., /unsubscribe?token=...) are vulnerable to CSRF even with SameSite=Lax because Lax allows top-level navigations.
  3. Subdomain-scoped cookies can be set by any subdomain, which becomes a CSRF vector if you have a less-trusted subdomain.

The right defaults today: HttpOnly + Secure + SameSite=Strict for auth cookies. Reserve SameSite=None for cookies you specifically need cross-site (OAuth dance, embedded widgets), and tighten anything else.

For state-changing operations specifically: don’t accept GETs that mutate. Use POST + a CSRF token (double-submit or sync token) for any non-idempotent action. Modern frameworks make this easy; AI codegen sometimes skips it because the boilerplate works without it in dev.

Cross-stack notes

  • Express + cors: The default of cors() with no options sets Access-Control-Allow-Origin: * and disables credentials — safe for public APIs but breaks cookie-based auth. The “fix it” path leads to origin: true, credentials: true, which is the bug.
  • Next.js (App Router): Per-route handlers set headers manually. AI codegen frequently writes headers().set('access-control-allow-origin', request.headers.get('origin')) — origin reflection, the unsafe pattern.
  • Django + django-cors-headers: CORS_ALLOWED_ORIGINS is the safe pattern. CORS_ALLOW_ALL_ORIGINS = True with CORS_ALLOW_CREDENTIALS = True is the unsafe combination — Django warns in dev, not always loudly enough.
  • Rails + rack-cors: Same shape. The origins '*' with credentials: true combination is what to grep for.
  • Go (gorilla/handlers, rs/cors): AllowedOrigins([]string{"*"}) with AllowCredentials() is the unsafe combo.

How we detect it

This is one of the cheapest detections we run. We send an OPTIONS preflight to your API with Origin: https://attacker.example. We read the response headers. If Access-Control-Allow-Origin echoes our hostile origin and Access-Control-Allow-Credentials: true is set, the bug is real.

We also fetch your homepage and read the Set-Cookie headers. If the auth cookie has SameSite=None or no SameSite attribute and an overbroad Domain value, that’s a complementary finding.

The security-headers-checker does both of these and a few more in one pass.

Fix

For CORS:

import cors from 'cors'
const allowedOrigins = ['https://app.your-site.com', 'https://your-site.com']
app.use(cors({
  origin: (origin, cb) => {
    if (!origin || allowedOrigins.includes(origin)) return cb(null, true)
    cb(new Error('Not allowed by CORS'))
  },
  credentials: true,
}))

If you don’t need cookies on cross-origin requests at all, set credentials: false. The wildcard origin with credentials false is safe — it just means cookies don’t carry, which is fine for public APIs.

For cookies:

  • HttpOnly always.
  • Secure always.
  • SameSite=Strict for auth cookies. Lax if you genuinely need top-level cross-site nav (post-login redirect from an OAuth provider, for instance).
  • Domain set to the most specific value that works for you. Don’t use .your-site.com unless you actively share the cookie across subdomains.

For CSRF on top: if your auth uses cookies (not Authorization headers), add a CSRF token. Either double-submit (token in cookie + token in header, server compares them) or sync-token (server-rendered token in HTML, returned in header). Modern frameworks make this a one-line config; the AI sometimes skips it because the boilerplate happens to work without it in dev.

CWE / OWASP

  • CWE-942 — Permissive Cross-domain Policy
  • CWE-352 — Cross-Site Request Forgery
  • CWE-346 — Origin Validation Error
  • OWASP Top 10 — A05:2021 Security Misconfiguration, A07:2021 Identification and Authentication Failures

Reproduce it yourself

COMMON QUESTIONS

01
What is the CORS-with-credentials misconfiguration?
Cross-Origin Resource Sharing controls which origins (sites) can read responses from your API. The two relevant headers are Access-Control-Allow-Origin and Access-Control-Allow-Credentials. The unsafe combination is Allow-Origin reflecting whatever origin the request came from (or set to '*' with credentials true via custom logic), with Allow-Credentials true. That tells the browser 'every site is allowed to call this API with the user's cookies.' Combined with cookies that don't have SameSite=Strict, this means any malicious site the user visits can fire authenticated requests at your API.
Q&A
02
Doesn't the browser refuse Access-Control-Allow-Origin: * with credentials?
Yes — that exact combination is a spec violation and browsers reject it. The bug shows up in two more subtle shapes. First, the server reflects the Origin header back: the response says Access-Control-Allow-Origin: https://attacker.example, which is technically not '*' but is exactly as permissive. Second, the server sets the wildcard but then a separate middleware adds Access-Control-Allow-Credentials: true and the developer doesn't realize the combination breaks the protection. Both are functionally the wildcard-with-credentials shape.
Q&A
03
How is this related to CSRF?
CSRF — Cross-Site Request Forgery — is the class of attack where a malicious site causes the user's browser to fire a request at your site with the user's cookies attached. Browsers used to allow most cross-origin requests by default, with a few exceptions. SameSite=Lax (default in modern browsers) blocks most CSRF. CORS reflecting the origin and Allow-Credentials true effectively undoes that protection — the malicious site can read responses, which means it can chain reads and writes. CORS misconfig + missing CSRF tokens + cookies without SameSite is the trifecta.
Q&A
04
Why do AI generators reproduce this?
Because 'use cors' middleware with no options is the answer to almost every Stack Overflow question that contains the word 'CORS.' The default in some libraries is permissive. The AI generator gives you the boilerplate, the boilerplate works, the developer doesn't audit the headers because the feature works in dev. Add credentials: true because something about cookies wasn't working, ship.
Q&A
05
Where can I see this on a real URL?
https://gapbench.vibe-eval.com/site/cors-misconfig/ has the wildcard-with-credentials shape (via origin reflection). https://gapbench.vibe-eval.com/site/csrf-missing/ shows the related missing-CSRF-token surface. The clean control surfaces are at /site/ref0/ for general headers.
Q&A
06
What CWE does this map to?
CWE-942 (Permissive Cross-domain Policy with Untrusted Domains), CWE-352 (Cross-Site Request Forgery), CWE-346 (Origin Validation Error). OWASP A05:2021 (Security Misconfiguration), A07:2021 (Identification and Authentication Failures).
Q&A

TEST YOUR CORS AND CSRF POSTURE

We probe the response headers and the cookie attributes and tell you which combinations leave you exposed.

RUN HEADER CHECKER