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.
4 steps from start to impact.
Gain cookie-write on the target domain
- Sibling subdomain under the same registrable domain as the target app
- XSS, subdomain takeover, or dangling DNS on that subdomain
- 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
subjack, nuclei takeover templates, CanITakeOverXYZ) detect dangling records. DNS inventory tools (e.g., Censys, SecurityTrails) surface orphaned subdomains.Plant matching state and PKCE cookies
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.- Cookie-write capability from Step 1
- Knowledge of the cookie names used by @fastify/oauth2 (public, hardcoded)
- 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
Lure victim to trigger the OAuth callback
@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.- Victim visits attacker-controlled content and interacts (click/redirect)
- Target app uses @fastify/oauth2's built-in state validation (not server-side sessions)
- 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)
Harvest victim activity on attacker's account
- Victim doesn't notice they're in the wrong account
- Application stores user-generated content accessible to the account owner
- 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
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.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.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.@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.- 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/Refererheaders 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.
SameSiterestrictions only apply to cross-site contexts, not same-site subdomain interactions.SameSite=Strictwon't help.
The supporting signals.
| In-the-Wild Exploitation | None observed. Not listed in CISA KEV. No threat intelligence reports, no campaign attribution, no mention in GreyNoise or Shadowserver feeds. |
|---|---|
| Proof-of-Concept Availability | No 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 Score | 0.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 Status | Not listed. No CISA KEV entry as of 2026-10-19. |
| CVSS Vector | CVSS: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 Version | 8.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 Date | August 14, 2026 (GHSA published). Modified September 6, 2026. |
| Reporter / Credits | Reported by kleju13. Fix developed by mcollina (Fastify core maintainer). Reviewed by UlisesGascon. |
Sources.
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/oauth2is 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.
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.
#!/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