← Back to Feed CACHED · 2026-10-19 13:48:30 · CACHE_KEY CVE-2026-110008
CVE-2026-110008 · CWE-352 · Disclosed 2026-08-14

Fastify OAuth2 login CSRF via plantable unprefixed state cookies from sibling subdomains

ASSESSED — NOISGATE
Vendor
—
—
—
Reassessed
—
—
—
Verdict: —
Do you agree?
01 · The Real Story

Someone left the neighbor's spare key under your doormat, and now visitors accidentally walk into the wrong apartment

CVE-2026-18165 (tracked by the user as CVE-2026-110008) affects @fastify/oauth2 versions 7.2.0 through 8.2.1. The plugin validates OAuth state and PKCE code_verifier parameters by comparing callback query values against unprefixed, predictable cookies (oauth2-redirect-state, oauth2-code-verifier) with no server-side binding to the browser session that started the flow. Any party who can *write a cookie* to the target application's registrable domain — practically, someone controlling a sibling subdomain via XSS, subdomain takeover, or dangling DNS — can plant matching state/verifier cookies and silently complete an attacker-owned OAuth flow in the victim's browser. The result is *login CSRF*: the victim is unknowingly signed into the attacker's account. Crucially, the victim's own credentials, tokens, and session are never exposed.

The vendor's MEDIUM 4.2 rating is honest about the CVSS math (AC:H/UI:R/C:L/I:L), but it still slightly overstates the real-world risk. CVSS's AC:H captures 'conditions beyond the attacker's control,' yet it doesn't fully price in that the prerequisite here is *itself a separate vulnerability* — you need cookie-write capability on a sibling subdomain, which means you already need subdomain takeover, dangling DNS, or XSS on a related host. That's chaining, not a single-step exploit. The impact ceiling is also lower than typical C:L/I:L scenarios: the attacker doesn't get the victim's data, they get the victim *using the attacker's data*. In most web applications this yields marginal value. Combined with rock-bottom EPSS (0.1%, 0.8th percentile), no public PoC, no KEV listing, and no observed in-the-wild exploitation, the real operational severity lands at LOW.

"Login CSRF via subdomain cookie planting is a narrow, low-impact chain — downgrade to LOW."
02 · The Attack Path

4 steps from start to impact.

STEP 01

Gain cookie-write on the target domain

The attacker must be able to set cookies scoped to the target application's registrable domain. In practice this means exploiting a *separate* vulnerability — XSS on a sibling subdomain, a subdomain takeover (dangling CNAME/A record), or control of an abandoned staging server sharing the same parent domain. Without this foothold, the chain is dead.
Conditions required:
  • Sibling subdomain under the same registrable domain as the target app
  • XSS, subdomain takeover, or dangling DNS on that subdomain
Where this breaks in practice:
  • Most organizations with mature DNS hygiene won't have dangling subdomains
  • Subdomain takeover requires a separate recon and exploitation phase
  • XSS on a sibling subdomain is a separate finding, not a given
Detection/coverage: Subdomain takeover scanners (e.g., subjack, nuclei takeover templates, CanITakeOverXYZ) detect dangling records. DNS inventory tools (e.g., Censys, SecurityTrails) surface orphaned subdomains.
STEP 02

Plant matching state and PKCE cookies

Using the cookie-write capability, the attacker sets oauth2-redirect-state and oauth2-code-verifier cookies on the victim's browser scoped to the target host. These values correspond to an OAuth flow the attacker has already initiated with the OAuth provider under the attacker's own identity. The cookies are unprefixed, so they are accepted by the @fastify/oauth2 plugin without challenge.
Conditions required:
  • Cookie-write capability from Step 1
  • Knowledge of the cookie names used by @fastify/oauth2 (public, hardcoded)
Where this breaks in practice:
  • Cookie names are documented and static, so this step is trivial *if* Step 1 succeeds
  • Requires attacker to pre-initiate their own OAuth flow to obtain valid state/verifier pair
Detection/coverage: Server-side logging of OAuth state mismatches or unexpected state reuse could flag anomalies, but few apps instrument this.
STEP 03

Lure victim to trigger the OAuth callback

The attacker crafts a page (or uses the compromised subdomain) to redirect the victim's browser to the target application's OAuth callback URL with the attacker's authorization code and the planted state parameter. The victim must visit this page and interact with it (UI:R). The @fastify/oauth2 plugin compares the callback state against the planted cookie, finds a match, and completes the token exchange — signing the victim into the attacker's account.
Conditions required:
  • Victim visits attacker-controlled content and interacts (click/redirect)
  • Target app uses @fastify/oauth2's built-in state validation (not server-side sessions)
Where this breaks in practice:
  • Requires social engineering or watering-hole positioning
  • Victim must not notice they are logged into a different account
  • Modern browsers increasingly restrict cross-site cookie behavior (SameSite defaults)
Detection/coverage: WAF rules monitoring for unusual OAuth callback patterns; anomaly detection on login-from-new-device events.
STEP 04

Harvest victim activity on attacker's account

Once the victim is operating under the attacker's identity, any data the victim enters — form submissions, file uploads, messages — is captured in the attacker's account. The attacker periodically checks their own account to exfiltrate this data. The victim's real account, tokens, and credentials remain untouched throughout.
Conditions required:
  • Victim doesn't notice they're in the wrong account
  • Application stores user-generated content accessible to the account owner
Where this breaks in practice:
  • Many apps display the logged-in user's name/email prominently — victim often notices immediately
  • Impact depends entirely on what the victim does while in the wrong account
  • No lateral movement, no credential theft, no privilege escalation
Detection/coverage: Unusual activity patterns on the attacker's account (geolocation mismatch, concurrent sessions) may trigger fraud detection.
03 · Compensating Control

1
LOW 3.2→IGNORE 0.0
SEVERITY REDUCED
Enable hostPrefixedCookies: true in @fastify/oauth2 config — This is the vendor's recommended fix short of a full version upgrade. Setting hostPrefixedCookies: true causes the plugin to use __Host- prefixed cookies, which browsers enforce must be set with Secure, Path=/, and *no* Domain attribute — making cross-subdomain cookie planting impossible. This completely breaks the attack chain at Step 2. Requires HTTPS. Deploy within the noisgate remediation SLA of 365 days for LOW findings, or sooner if your app handles sensitive user-generated content.
2
LOW 3.2→IGNORE 0.0
SEVERITY REDUCED
Implement server-side state binding via generateStateFunction / checkStateFunction — Instead of relying on client-side cookies for state validation, bind the OAuth state to a server-side session store (Redis, database, etc.). The attacker can plant cookies all day — the server will reject any state that wasn't issued to the victim's actual session. This is the most robust mitigation and eliminates the entire vulnerability class, not just this instance. No version upgrade required; works on all affected versions.
3
LOW 3.2→IGNORE 0.0
SEVERITY REDUCED
Audit and remediate dangling DNS / subdomain takeover exposure — The attack chain is dead without cookie-write capability on a sibling subdomain. Run subdomain enumeration and takeover detection (e.g., subjack, nuclei -t takeovers/, SecurityTrails inventory) across your registrable domains. Remove dangling CNAME/A records and decommission abandoned staging hosts. This breaks Step 1 of the attack path and hardens against a broad class of cookie-tossing attacks beyond this CVE.
4
LOW 3.2→IGNORE 0.0
SEVERITY REDUCED
Upgrade to @fastify/oauth2 >=8.3.0 — The definitive fix. Version 8.3.0 includes the hostPrefixedCookies option and other hardening. After upgrading, enable hostPrefixedCookies: true in your plugin configuration. This is the noisgate remediation action — complete within the 365-day window for LOW findings.
What doesn't work
  • Generic CSRF tokens on your app's forms — This is an *OAuth flow* CSRF, not a form-submission CSRF. Your app's anti-CSRF middleware (e.g., @fastify/csrf-protection) protects form POST endpoints, not the OAuth callback redirect flow. The state parameter IS the CSRF token for OAuth, and it's the one that's broken here.
  • WAF rules blocking CSRF — Standard WAF CSRF detection looks for missing Origin/Referer headers on state-changing POST requests. The OAuth callback is a GET redirect from the identity provider — it looks exactly like a legitimate flow to the WAF.
  • SameSite=Strict cookies on your app — The attack uses cookie *tossing* from a sibling subdomain (same registrable domain), not cross-site cookie injection. SameSite restrictions only apply to cross-site contexts, not same-site subdomain interactions. SameSite=Strict won't help.
04 · Intelligence Metadata

The supporting signals.

In-the-Wild ExploitationNone observed. Not listed in CISA KEV. No threat intelligence reports, no campaign attribution, no mention in GreyNoise or Shadowserver feeds.
Proof-of-Concept AvailabilityNo public PoC. Checked pocindex.io (0 results), GitHub repos named after CVE-2026-18165 (none found), ExploitDB (not listed), nuclei templates (not listed). The GHSA advisory describes the attack conceptually but provides no weaponized code.
EPSS Score0.001 (0.1%) — 0.8th percentile. Bottom 1% of all scored CVEs. FIRST's model sees near-zero probability of exploitation in the next 30 days.
KEV StatusNot listed. No CISA KEV entry as of 2026-10-19.
CVSS VectorCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:L/I:L/A:N — Network-reachable but high complexity (requires sibling subdomain control), user interaction required, no privilege required, unchanged scope, low confidentiality and integrity impact, no availability impact.
Affected Versions@fastify/oauth2 >=7.2.0, <8.3.0 (spans v7.2.0–v7.9.0 and v8.0.0–v8.2.1)
Fixed Version8.3.0 (published ~August 14, 2026). Fix adds opt-in hostPrefixedCookies: true option using __Host- prefixed cookies. Server-side session binding via generateStateFunction/checkStateFunction is the robust alternative.
Package Footprint~84,000 weekly npm downloads, 15 direct dependents. Niche within the Node.js ecosystem — this is a Fastify-specific OAuth2 plugin, not a foundational framework.
Disclosure DateAugust 14, 2026 (GHSA published). Modified September 6, 2026.
Reporter / CreditsReported by kleju13. Fix developed by mcollina (Fastify core maintainer). Reviewed by UlisesGascon.

Sources.

  1. GitHub Security Advisory (GHSA-p8h8-rj28-m8q9)
  2. OSV — CVE-2026-18165
  3. OpenCVE — CVE-2026-18165
  4. FIRST EPSS API — CVE-2026-18165
  5. Snyk — Hijacking OAuth Flows via Cookie Tossing
  6. @fastify/oauth2 — npm
  7. Login CSRF is Low-Risk and High-Risk (alsmola)
  8. PoC Index
05 · The Call

Final Verdict
↓ DOWNGRADED to LOW (3.2/10)

Why this verdict

  • Chained prerequisite kills most attack paths: The entire chain requires the attacker to *already* control a sibling subdomain (via XSS, takeover, or dangling DNS) — this is a separate, non-trivial vulnerability. CVSS captures this as AC:H, but the real-world narrowing is sharper than 'high complexity' implies: it's a dependency on a prior exploit succeeding against the same organization's DNS/subdomain infrastructure.
  • Impact ceiling is login CSRF, not account takeover: The victim is logged into the *attacker's* account. The victim's own credentials, tokens, and session data are never exposed. The attacker can only harvest what the victim voluntarily enters while unknowingly using the wrong account. Most users notice they're in a different account within seconds (name/avatar mismatch). This is categorically less severe than credential theft or session hijacking.
  • Rock-bottom threat intelligence signals: EPSS 0.1% (0.8th percentile), no public PoC, no KEV listing, no observed exploitation, no threat campaign attribution. The exploit economics are poor — the effort to obtain subdomain control far exceeds the marginal value of login CSRF on a single web app.
  • Role multiplier: @fastify/oauth2 is a *relying party* (RP) library — it's used by web applications that *consume* OAuth, not by identity providers that *issue* tokens. Deployment roles: (a) low-value — dev/staging apps, internal dashboards (majority of the 84K weekly installs); (b) typical — line-of-business web apps with OAuth-based login; (c) high-value — theoretically a customer-facing SaaS app. Even in the high-value role, the blast radius of login CSRF is per-session, per-user, per-application — it does not escalate to domain takeover, fleet compromise, identity-scale impact, or supply-chain pivot. This is NOT a high-value-role component by definition (it's not an IdP, DC, hypervisor, PAM, backup, CA, or edge appliance). The floor test does not trigger.
  • Browser mitigations add friction: Modern browsers default cookies to SameSite=Lax, which restricts cross-site cookie delivery on top-level navigations initiated by third-party sites. While cookie *tossing* from a sibling subdomain can bypass this (same-site, not cross-site), it still narrows the attacker's positioning requirements.

Why not higher?

The vulnerability does not meet the HIGH floor because @fastify/oauth2 is not a high-value-role component — it's a web application library, not identity infrastructure. Even in the worst plausible deployment (customer-facing SaaS), successful exploitation yields login CSRF (victim in attacker's account), not credential theft, not session hijacking, not lateral movement. The blast radius is a single user session on a single application. No chain from this bug reaches domain takeover, fleet compromise, or supply-chain impact.

Why not lower?

Despite the poor exploit economics, the bug *is* real and *is* exploitable given the right conditions (organizations with sloppy subdomain hygiene do exist). Login CSRF, while narrow, can lead to data harvesting in sensitive applications (e.g., a victim unknowingly filing a support ticket or entering PII into the attacker's account). The network-reachable, unauthenticated attack vector (AV:N/PR:N) means it can't be dismissed entirely as theoretical. An IGNORE verdict would understate the residual risk for organizations that do have subdomain exposure.

06 · Verification

Crowdsourced verification payload.

Run on any machine with node (>=14) and network access to your npm registry or local node_modules. Invoke: bash check_fastify_oauth2.sh /path/to/your/project — no special privileges required. If the project uses a lockfile, the script inspects it directly; otherwise it falls back to npm ls.

noisgate-verify.sh
BASHREAD-ONLYSAFE
#!/usr/bin/env bash
# check_fastify_oauth2.sh — Detect CVE-2026-18165 in @fastify/oauth2
# Usage: bash check_fastify_oauth2.sh /path/to/project
# Exit codes: 0=PATCHED, 1=VULNERABLE, 2=UNKNOWN

set -euo pipefail

PROJECT_DIR="${1:-.}"
PKG_NAME="@fastify/oauth2"
FIXED_MAJOR=8
FIXED_MINOR=3
FIXED_PATCH=0
VULN_INTRO_MAJOR=7
VULN_INTRO_MINOR=2

version=""

# Try package-lock.json first
if [[ -f "$PROJECT_DIR/package-lock.json" ]]; then
  version=$(python3 -c "
import json, sys
with open('$PROJECT_DIR/package-lock.json') as f:
    lock = json.load(f)
# npm v2+ lockfileVersion>=2 uses 'packages'
pkgs = lock.get('packages', {})
for key, val in pkgs.items():
    if key.endswith('$PKG_NAME') or key == 'node_modules/$PKG_NAME':
        print(val.get('version', '')); sys.exit(0)
# npm v1 uses 'dependencies'
deps = lock.get('dependencies', {})
if '$PKG_NAME' in deps:
    print(deps['$PKG_NAME'].get('version', '')); sys.exit(0)
print('')" 2>/dev/null || true)
fi

# Try yarn.lock via grep
if [[ -z "$version" && -f "$PROJECT_DIR/yarn.lock" ]]; then
  version=$(grep -A1 '"@fastify/oauth2@' "$PROJECT_DIR/yarn.lock" 2>/dev/null | grep 'version' | head -1 | sed 's/.*"\([^"]*\)".*/\1/' || true)
fi

# Fallback: npm ls
if [[ -z "$version" ]]; then
  version=$(cd "$PROJECT_DIR" && npm ls "$PKG_NAME" --json 2>/dev/null | python3 -c "
import json, sys
data = json.load(sys.stdin)
def find_ver(d):
    if 'dependencies' in d:
        for k, v in d['dependencies'].items():
            if k == '$PKG_NAME':
                return v.get('version', '')
            r = find_ver(v)
            if r: return r
    return ''
print(find_ver(data))" 2>/dev/null || true)
fi

if [[ -z "$version" ]]; then
  echo "UNKNOWN — @fastify/oauth2 not found in $PROJECT_DIR"
  exit 2
fi

echo "Found @fastify/oauth2 version: $version"

# Parse semver
IFS='.' read -r major minor patch <<< "$(echo "$version" | sed 's/[^0-9.]//g')"
major=${major:-0}; minor=${minor:-0}; patch=${patch:-0}

# Check if version is in vulnerable range: >=7.2.0 and <8.3.0
vulnerable=false
if (( major == VULN_INTRO_MAJOR && minor >= VULN_INTRO_MINOR )); then
  vulnerable=true
elif (( major == 8 && minor < FIXED_MINOR )); then
  vulnerable=true
elif (( major == 8 && minor == FIXED_MINOR && patch < FIXED_PATCH )); then
  vulnerable=true
fi

if $vulnerable; then
  echo "VULNERABLE — @fastify/oauth2 $version is affected by CVE-2026-18165 (Login CSRF via plantable state cookies)"
  echo "Remediation: upgrade to >=8.3.0 and enable hostPrefixedCookies: true"
  exit 1
else
  echo "PATCHED — @fastify/oauth2 $version is not in the vulnerable range (7.2.0–8.2.x)"
  exit 0
fi