THE 'ALMOST SECURE' AUTH GAPS
Four bugs that don't show up in a checklist because the code looks correct. Cookie scope set wider than necessary. TLS configured but with downgrade paths still open. OAuth tokens leaking through Referer headers. PKCE technically supported but optional.
The scenario referenced below runs on gapbench.vibe-eval.com — a public security benchmark we operate.
Bugs that pass review
Every one of these four bugs ships in code that looks fine. The review checklist says “cookies have HttpOnly and Secure” — yes. “TLS is enabled” — yes. “OAuth uses Authorization Code flow” — yes. “PKCE is supported” — yes. All four boxes checked. All four wrong in the same subtle way.
The review didn’t ask the second-order question. How wide is the cookie scope? Is TLS the only option, or just one option? Where does the OAuth token end up rendered? Is PKCE required, or just available?
Overbroad cookie domain
res.cookie('session', token, {
httpOnly: true,
secure: true,
sameSite: 'lax',
domain: '.example.com', // <-- here
})
Domain=.example.com ships the cookie on every subdomain. If you have marketing.example.com running a third-party site builder, partner.example.com running a SaaS your team integrated with, old.example.com running a Vercel project nobody owns anymore, team.example.com from before your domain consolidation — every one of those sees the session cookie.
If any of those subdomains is compromised or under attacker control (subdomain takeover, marketing-vendor breach, anything), the session token leaks.
The fix: don’t set domain unless you actively need to share the cookie across subdomains. The default scope is the host that issued the cookie — exactly the principal that should see it. If you must set domain, set it to the most specific value that works.
Live: https://gapbench.vibe-eval.com/site/cookie-scope-leak/.
TLS downgrade (no HSTS)
You configured TLS. Your site responds correctly to https://example.com. Your reverse proxy redirects HTTP to HTTPS. Audit passes.
Then a user on a rogue WiFi types example.com without https://. The first request goes plain HTTP. The attacker sitting between user and server intercepts it, never sends the redirect, serves their own version of your site. The user is now on attacker.example, but their address bar (which they probably don’t look at) shows what looks like example.com.
The mitigation is HSTS — Strict-Transport-Security: max-age=31536000; includeSubDomains; preload. Once a user has visited your site once over HTTPS with HSTS set, the browser refuses to ever connect over HTTP. The first-visit case is solved by submitting your domain to the HSTS preload list, which ships with browsers.
The fix is one header. AI-generated apps frequently ship without it because the AI’s “set up TLS” advice doesn’t always include it.
Live: https://gapbench.vibe-eval.com/site/tls-downgrade/.
OAuth token leak via Referer
OAuth callback pages occasionally end up with the access token in the URL — implicit flow, or hybrid flows, or just sloppy server-side rendering that puts the token in a hidden field on the page. If that page contains third-party scripts (analytics, tag managers, anything outbound) and any of them make a request before the page strips the token, the token can leak via the Referer header.
The same happens if the user clicks a link on the callback page before the token gets handled — the next page’s outbound requests include a Referer with the token.
Fix: don’t put tokens in URLs. Use the Authorization Code with PKCE flow, return tokens via POST. If you can’t avoid them in URLs (legacy implicit flow), set a strict Referrer-Policy: no-referrer on the callback page, and load no third-party scripts there.
Live: https://gapbench.vibe-eval.com/site/oauth-token-leak-referer/.
PKCE downgrade
The PKCE extension to OAuth requires the client to send a code_challenge when initiating the flow and a matching code_verifier when exchanging the code. The pair makes the authorization code useless to anyone who didn’t generate the verifier.
If the OAuth server supports PKCE but doesn’t require it, a flow can complete without the verifier. An attacker who intercepts the authorization code (via stolen redirect_uri, leaked Referer, etc.) can exchange it without supplying a verifier — because the server doesn’t insist.
Fix: configure your OAuth server to require PKCE for all public client flows. Reject any token exchange where the original authorization request didn’t include code_challenge.
Live: https://gapbench.vibe-eval.com/site/pkce-downgrade/. Companion: https://gapbench.vibe-eval.com/site/ref-oauth/ for the clean control.
A specific incident — cookie scope plus subdomain takeover
Anonymized. A B2B product had three subdomains — app.example.com for the main UI, api.example.com for the backend, and marketing.example.com for the WordPress marketing site. The auth cookie was set with Domain=.example.com so it propagated across all three. Reasonable when the team set it up; brittle later.
The marketing subdomain was hosted on a third-party WordPress provider. The team migrated off WordPress to a different provider but didn’t update the DNS for ~3 weeks. During that window, the WordPress provider’s account had lapsed. Anyone who registered the same site name on that provider could serve content under marketing.example.com.
An attacker did exactly that. They served a small JavaScript on the takeover-eligible subdomain that did fetch('https://api.example.com/api/me', { credentials: 'include' }), got the response (because the cookie was scoped to .example.com), and exfiltrated session data.
The cleanup: shorter cookie scope (host-only, no Domain attribute), DNS hygiene (audit quarterly), the WordPress site moved to a separate apex domain.
The lesson: cookie domain scope is a load-bearing security control. The wider you set it, the more subdomain failures become full-cookie failures. Set the narrowest scope that works. If you must share across subdomains, audit those subdomains continuously — every one of them is now in your auth surface.
TLS — the things people miss
TLS being “configured” is a low bar. TLS being correctly configured is a longer list:
- HSTS with includeSubDomains and preload. The header tells browsers to refuse HTTP for the duration. Preload list inclusion makes it apply on first visit too.
- No HTTP at all on production hosts. Don’t redirect HTTP→HTTPS; refuse HTTP entirely. The redirect is a downgrade window.
- Modern cipher suites. TLS 1.2 minimum, 1.3 preferred. Ban RC4, 3DES, CBC-mode AES.
- Certificate transparency monitoring. Watch CT logs for certs issued for your domain by CAs you don’t use. Alerting on this catches a class of takeover attacks early.
- OCSP stapling enabled. Reduces revocation-checking latency and information leakage.
- No mixed content. All sub-resources on HTTPS. Mixed content has been silently blocked by browsers for years, but warnings still leak.
- Forward secrecy enabled. All modern cipher suites support it; verify your config doesn’t disable it.
Most of these are enabled by default on managed platforms (Vercel, Netlify, Cloudflare). Self-hosted setups often miss several. Tools like SSL Labs (ssllabs.com/ssltest) grade the configuration in detail.
OAuth — the parameters that matter
A correct OAuth Authorization Code flow includes:
stateparameter, generated server-side, validated on callback. Prevents CSRF on the OAuth dance.code_challenge(PKCE) for public clients, S256 method. Prevents code-interception attacks.redirect_uriexact-match in the OAuth provider’s allowlist. No wildcards, no subdomain patterns.nonceparameter for OIDC, validated against the ID token. Prevents replay.- Token exchange via POST, not GET. Tokens never appear in URLs or referrer headers.
- Strict Referrer-Policy on OAuth callback pages.
Referrer-Policy: no-referreris the safest.
Each of these mitigates a specific known attack. AI-generated OAuth code typically gets 1, 3, 5 right and skips 2, 4, 6. The skipped ones are the ones we keep finding.
Wrong fix vs right fix — cookies
// WRONG: convenience, broad scope
res.cookie('session', token, {
httpOnly: true,
secure: true,
sameSite: 'lax',
domain: '.example.com', // every subdomain sees this
})
// WRONG: SameSite=None for "OAuth compatibility" without thinking
res.cookie('session', token, {
httpOnly: true,
secure: true,
sameSite: 'none', // CSRF surface restored
domain: '.example.com',
})
// RIGHT: tightest scope that works
res.cookie('session', token, {
httpOnly: true,
secure: true,
sameSite: 'strict', // or 'lax' if you need top-level cross-site nav
// No Domain attribute — host-only cookie
// Only the issuing host (e.g., app.example.com) sees this
path: '/',
})
Cross-stack notes
- Express:
res.cookiecontrols everything. DefaultsameSiteis undefined (browser interprets as Lax), defaultsecureis false. AI codegen often skipssecurein dev and forgets to flip it. - Next.js: Per-route cookie setting via
cookies(). Same shape as Express. - Django:
SESSION_COOKIE_SAMESITE,SESSION_COOKIE_SECURE,SESSION_COOKIE_DOMAIN. The defaults are reasonable; the issue is when AI codegen overrides them. - Rails:
config.session_store. Defaults to host-only and Lax. AI-generated apps that copy old tutorials sometimes setdomain: :allwhich breaks this. - Spring:
ServerHttpSecurityandCookieSerializer. Configuration has many knobs; AI codegen frequently sets some and not others.
How we detect
Cookie scope: we read Set-Cookie headers from your auth flow, parse the Domain attribute, flag anything broader than the issuing host.
TLS downgrade: we check whether your domain has HSTS, the max-age, whether includeSubDomains is set, and whether you’re on the HSTS preload list. We also probe whether HTTP returns content vs. an immediate redirect.
OAuth Referer leak: we walk the OAuth flow, observe whether tokens land in URLs, check for third-party script tags on callback pages, and verify Referrer-Policy.
PKCE: we initiate an OAuth flow without code_challenge, observe whether the server accepts it.
All runtime. Static scanners can flag the cookie config in source but can’t confirm the live HSTS posture or the OAuth-server-side PKCE setting.
CWE / OWASP
- CWE-565 — Reliance on Cookies without Validation and Integrity Checking
- CWE-319 — Cleartext Transmission of Sensitive Information
- CWE-200 — Information Exposure (Referer)
- CWE-345 — Insufficient Verification of Data Authenticity (PKCE)
- OWASP Top 10 — A02:2021 Cryptographic Failures, A07:2021 Identification and Authentication Failures
Reproduce it yourself
- Cookie scope leak: https://gapbench.vibe-eval.com/site/cookie-scope-leak/
- TLS downgrade: https://gapbench.vibe-eval.com/site/tls-downgrade/
- OAuth Referer leak: https://gapbench.vibe-eval.com/site/oauth-token-leak-referer/
- PKCE downgrade: https://gapbench.vibe-eval.com/site/pkce-downgrade/
- Clean OAuth control: https://gapbench.vibe-eval.com/site/ref-oauth/
Related reading
- Pattern: SSRF, open redirects, and OAuth redirect_uri
- Pattern: CORS = * with credentials = true
- Pattern: Magic links, OTP, and password resets
COMMON QUESTIONS
TEST YOUR AUTH POSTURE
We probe cookie scope, TLS configuration, OAuth token handling, and PKCE enforcement.