Your web app hung a 'come on in' sign on the browser's same-origin fence
Tenable WAS plugin 98983 flags a web application whose Access-Control-Allow-Origin response reflects an arbitrary requesting origin (or is set to *) *while also* returning Access-Control-Allow-Credentials: true — or otherwise permits untrusted origins to make authenticated cross-site XHR/fetch calls. The affected component is any HTTP endpoint on the scanned application that returns those headers; there is no single vendor/version to patch. This is a configuration class, not a shipped-code CVE, so 'affected versions' = every deployment that emits the offending header combination, regardless of framework (Nginx add_header misuse, Express cors({origin: true, credentials: true}), Spring @CrossOrigin(origins='*'), ASP.NET AllowAnyOrigin().AllowCredentials(), API-gateway wildcards, etc.).
Tenable rates this Medium (CVSS v3 6.1 / v4 5.1) and that is honest. It maps to CWE-346 (Origin Validation Error) and OWASP A07. The severity is fair *as a class* — the impact ceiling is CSRF-with-response-read against authenticated users, which is meaningful but requires a victim to visit an attacker-controlled page while logged in to the target. It is not RCE, it is not unauthenticated data exfil at the server, and browser same-site cookie defaults (SameSite=Lax default since Chrome 80 / 2020) already blunt the credentialed variant on most modern stacks. Medium is the right bucket; we hold it there.
4 steps from start to impact.
Confirm the reflective/permissive CORS policy
Origin: https://evil.tld request header and observes the response. If the server echoes back Access-Control-Allow-Origin: https://evil.tld (or *) together with Access-Control-Allow-Credentials: true, the misconfiguration is confirmed. Tools of choice: Burp Suite Pro with the *CORS* scanner extension, corsy (s0md3v/Corsy), or manual curl -H 'Origin: https://evil.tld' -I.- Endpoint reachable by attacker (does not need auth)
- Application returns CORS headers on the endpoint
- Modern frameworks default to strict-origin allowlists
- WAFs and API gateways frequently strip or normalize CORS headers
cors-misconfig covers it.Identify a state-changing or data-returning authenticated endpoint
/api/me, /api/transfer, /graphql, /admin/users). Attacker maps these via directory brute-force (ffuf, feroxbuster) or by reading published API docs / OpenAPI specs.- Session-bearing endpoints exist on the vulnerable origin
- Endpoints do not require additional CSRF token / custom header the attacker cannot forge cross-origin
- Well-designed APIs require a custom header like
X-Requested-Withthat triggers a preflight and blocks bypass - Bearer tokens in
Authorizationheader are NOT sent cross-origin automatically — only cookies are
Origin headers from unknown domains combined with 200-response Set-Cookie-authenticated calls.Host attacker-controlled page and lure an authenticated victim
https://evil.tld/pwn.html containing JavaScript that issues fetch('https://target/api/me', {credentials:'include'}).then(r=>r.text()).then(exfil). Victim must be logged in to the target application *and* click / visit the attacker's link in the same browser profile. Delivery is phishing, malvertising, or a watering-hole compromise.- Victim has active session with target
- Victim visits attacker page in same browser context
SameSite=LaxorNoneon session cookie (Strict blocks this entirely)
- Chrome/Edge/Firefox default
SameSite=Laxsince 2020 blocks credentialed cross-site fetch initiated top-level GET only after nav — but blocks POST/XHR entirely - Enterprise victims often use isolated browser profiles or Zscaler/Netskope proxies that strip third-party cookies
- Corporate email gateways (Proofpoint, Mimecast) sandbox suspicious links
fetch to newly-registered domains; browser isolation catches it.Exfiltrate response body to attacker origin
ACAO header lets attacker JS read the cross-origin response body (normally forbidden by SOP). Attacker POSTs the harvested JSON — session identity, PII, API tokens, CSRF tokens for follow-on attacks — to their own collection endpoint. Repeat across all discovered endpoints for full account takeover material.- Response body contains sensitive data or actionable secrets
- Endpoints returning only opaque IDs / no sensitive payload yield little
- Rate limiting and anomaly detection on server side may throttle bulk enumeration
Origin header from single external domain, followed by anomalous data volume.Access-Control-Allow-Credentials: true unless absolutely required — This single header change eliminates the high-impact variant. Without it, cross-origin JS cannot read responses that depend on cookies or Authorization headers. Deploy the config change within the standard MEDIUM window — no mitigation SLA, so target the 365-day remediation cycle, but this is a one-line config fix that should ship in the next release train.Access-Control-Allow-Origin: * when credentials are in play, and never blindly echo the requesting Origin. Maintain a static allowlist of trusted partner/subdomain origins in the reverse proxy (Nginx map $http_origin, Envoy cors_policy.allow_origin_string_match, or the framework's typed CORS middleware). Ship within 90 days.SameSite=Strict (or at minimum Lax) with Secure and HttpOnly — Belt-and-braces defense: even if a rogue CORS policy slips through code review, SameSite=Strict prevents the browser from attaching the session cookie to the attacker's cross-site fetch. Roll this out across all first-party session cookies in the next sprint.X-Requested-With: XMLHttpRequest) on state-changing endpoints — Custom headers force a CORS preflight; if the server rejects preflights from untrusted origins, the exploit chain is broken before the credentialed request lands. Combine with double-submit CSRF tokens.Origin responses — At F5, Cloudflare, Akamai, AWS WAF: inspect outbound response headers and block responses where ACAO echoes an origin not on the allowlist. This is a compensating control while the underlying app is being fixed.- CSRF tokens alone — a permissive CORS policy lets the attacker's JS *read* the CSRF token from a preflight-allowed endpoint and reuse it. CSRF tokens are necessary but not sufficient.
- HTTPS / HSTS — TLS does nothing to prevent a browser from honoring a permissive
ACAOheader; the attack happens over TLS end-to-end. - Content Security Policy (CSP) — CSP restricts what the *victim's* app can load; it does not constrain what an *attacker's* origin can request from the victim's app.
- IP allowlisting the API — the request originates from the victim's browser (a trusted client IP), so IP filtering does not fire.
The supporting signals.
| Finding type | Tenable WAS plugin 98983 — misconfiguration signature, *not* a CVE. Maps to CWE-346 (Origin Validation Error). |
|---|---|
| Vendor severity | Medium — CVSS v2 4.3 / v3 6.1 / v4 5.1 (Tenable-assigned; no NVD entry because this is not a CVE) |
| CVSS v3.1 vector (Tenable) | AV:N/AC:H/PR:N/UI:R/S:C/C:H/I:H/A:N — network vector but UI:R (victim must visit attacker page) and AC:H are the honest constraints |
| In-the-wild status | Class of bug has been abused in bug-bounty payouts against Facebook (2016 Bitquark), Twitter, Uber, Slack, GitHub Enterprise. No CISA KEV entry (KEV is CVE-only). |
| Public tooling | Corsy (s0md3v/Corsy), CORScanner (chenjj/CORScanner), Burp Suite CORS scanner, Nuclei cors-misconfig template |
| EPSS / KEV | N/A — EPSS and KEV are keyed to CVE IDs; this Tenable plugin has none |
| Exposure population | High class-frequency — Detectify's 2023 web-security study found permissive CORS in ~7% of enterprise web assets; PortSwigger research puts the credentialed-variant subset at <1% |
| Blast radius | Per-user, per-endpoint — one victim's session, on one app, per phish. Not lateral, not fleet-scale. |
| Modern browser mitigation | SameSite=Lax default (Chrome 80, Feb 2020) neuters most credentialed cross-site XHR; SameSite=Strict eliminates it |
| Fixed configuration | Explicit domain allowlist; never combine ACAO: * or reflected origin with Access-Control-Allow-Credentials: true |
Sources.
- Tenable WAS Plugin 98983 — Insecure CORS Configuration
- CWE-346: Origin Validation Error
- PortSwigger Web Security Academy — CORS
- OWASP HTML5 / CORS Security Cheat Sheet
- MDN — Access-Control-Allow-Credentials
- Chrome SameSite=Lax by default rollout
- Corsy CORS misconfiguration scanner (s0md3v)
- CORScanner (chenjj)
Why this verdict
- No vendor CVSS baseline exists — this is a Tenable WAS signature (plugin 98983), not a CVE. Assessment starts from Tenable's own Medium (6.1) rating and adjusts against real-world friction.
- Requires victim interaction and active session — victim must be logged in to the target AND visit an attacker page in the same browser profile. Two compounding preconditions drop the effective severity.
- SameSite=Lax cookie default (Chrome 80+, Firefox, Edge) blocks the credentialed cross-site XHR variant entirely for POST and blocks GET-initiated fetch without a top-level navigation. This eliminates the exploit path on most modern applications.
- Role multiplier — customer-facing web app / API: chain succeeds; blast radius = one victim's session per successful phish. Not a fleet event.
- Role multiplier — internal admin console / IdP / SSO portal: chain succeeds; blast radius scales to *whichever admin* got phished but is still bounded by that admin's own session. If the admin's session grants tenant/domain control, that is a role-escalation risk — but the CORS bug is the amplifier, not the primary. Floor still MEDIUM because attacker must still land the phish; the AD/IdP itself is not directly reachable via CORS from the internet.
- Role multiplier — unauthenticated public API: no credentials to steal, chain does not monetize. Effectively LOW in this role.
- No lateral movement, no server-side RCE, no unauthenticated bulk exfil — CORS bugs do not compound into fleet compromise on their own.
Why not higher?
HIGH would require either unauthenticated server-side impact or a chain that succeeds without victim interaction and without an active session cookie. Neither holds: browser SOP still enforces same-origin *without* the victim's cookies, and SameSite=Lax defaults strip those cookies from cross-site requests. The bug is a real amplifier, not a primary compromise vector.
Why not lower?
LOW / IGNORE is wrong because the class of bug has produced material bug-bounty payouts (Facebook, Uber, Slack, GitHub Enterprise) where authenticated tokens, PII, and CSRF tokens were exfiltrated cross-origin. Any app that combines reflected/wildcard ACAO with Allow-Credentials: true and returns session-bearing JSON *is* exploitable against a phished user — that is not backlog hygiene, it is an actionable finding.
Crowdsourced verification payload.
Run this from any workstation or CI runner with network access to the target. Invoke as ./cors-check.sh https://api.example.com/me — no privileges required. It probes the endpoint with a spoofed Origin header and inspects the response.
#!/usr/bin/env bash
# cors-check.sh — detects Tenable WAS 98983 (Insecure CORS Configuration)
# Usage: ./cors-check.sh <url>
# Exits 2 = VULNERABLE, 0 = PATCHED, 1 = UNKNOWN/error
set -u
TARGET="${1:-}"
if [[ -z "$TARGET" ]]; then
echo "UNKNOWN: usage: $0 <url>" >&2
exit 1
fi
EVIL_ORIGIN="https://evil.attacker.tld"
# Fetch response headers with spoofed Origin
HEADERS=$(curl -sS -D - -o /dev/null \
-H "Origin: ${EVIL_ORIGIN}" \
-H "User-Agent: noisgate-cors-check/1.0" \
--max-time 15 \
"$TARGET" 2>/dev/null) || {
echo "UNKNOWN: curl failed against $TARGET"
exit 1
}
ACAO=$(echo "$HEADERS" | grep -i '^access-control-allow-origin:' | tr -d '\r' | awk -F': ' '{print $2}')
ACAC=$(echo "$HEADERS" | grep -i '^access-control-allow-credentials:' | tr -d '\r' | awk -F': ' '{print $2}')
if [[ -z "$ACAO" ]]; then
echo "PATCHED: no Access-Control-Allow-Origin header returned for untrusted origin"
exit 0
fi
# Case 1: reflected origin
if [[ "$ACAO" == "$EVIL_ORIGIN" ]]; then
if [[ "${ACAC,,}" == "true" ]]; then
echo "VULNERABLE: reflected Origin + Allow-Credentials:true — credentialed cross-origin read possible"
exit 2
fi
echo "VULNERABLE: reflected Origin (no credentials, but still unauthenticated data leak risk)"
exit 2
fi
# Case 2: wildcard with credentials (browser will actually reject this combo, but signal is still a misconfig)
if [[ "$ACAO" == "*" && "${ACAC,,}" == "true" ]]; then
echo "VULNERABLE: ACAO:* with Allow-Credentials:true (invalid combo, but indicates broken policy)"
exit 2
fi
# Case 3: explicit allowlist — check if evil origin is NOT reflected
echo "PATCHED: ACAO='$ACAO' does not reflect untrusted origin"
exit 0