Like swapping someone's hotel keycard so they walk into your room instead of theirs
CVE-2026-18165 affects @fastify/oauth2 versions 7.2.0 through 8.2.x. The OAuth 2.0 state parameter — meant to tie the authorization callback to the user who started the flow — is stored in unprefixed cookies (oauth2-redirect-state, oauth2-code-verifier). An attacker who controls a related host (e.g., a subdomain with XSS, an abandoned staging environment, or a subdomain takeover) can plant their own state cookie into the victim's browser. When the victim completes the OAuth flow, the server sees the cookie matches the query string and accepts it — it never proves the *same browser* initiated the flow. The result is a login CSRF: the victim silently gets signed into the attacker's account, and their subsequent actions (uploads, messages, payment details) are captured under the attacker's session.
The vendor rates this MEDIUM at 4.2 and that assessment is honest. The CVSS vector reflects network-reachable but high-complexity, user-interaction-required, no privilege escalation, unchanged scope, and only Low confidentiality/integrity impact with no availability impact. Login CSRF is a real class of vulnerability recognized in OAuth threat models (RFC 6819 §4.4.1.8), but its practical severity is sharply limited: the victim's own credentials and tokens are never exposed, and the attacker only learns what the victim *does* in the planted session, not who they *are*. The vendor score is fair — this is not undersold.
4 steps from start to impact.
Attacker obtains related-origin cookie access
.example.com). Forgotten staging servers, dangling DNS CNAMEs, and subdomain takeover via unclaimed cloud endpoints are the usual vectors.- Attacker controls a subdomain or has XSS on a sibling host under the same registrable domain
- Requires a pre-existing subdomain vulnerability or misconfiguration — this is a prerequisite that most mature orgs have processes to detect
- Subdomain takeover scanners (Subjack, Nuclei templates) catch the most common cases
detect-dangling-cname, CanITakeOverXYZ project). WAF subdomain XSS rules.Plant OAuth state cookies in victim's browser
oauth2-redirect-state and optionally oauth2-code-verifier cookies scoped to the parent domain. These cookies contain state values from the attacker's own in-progress OAuth flow. Because the cookies lack __Host- prefixes, the browser happily accepts them from the subdomain.- Victim visits attacker-controlled subdomain page
- Target app uses HTTPS but cookies are not
__Host-prefixed (the vulnerable default)
- Victim must actually visit the attacker-controlled subdomain — requires social engineering or a watering-hole placement
- SameSite=Lax on the state cookie limits some cross-site delivery vectors, though same-site subdomains bypass this
Victim initiates or is redirected through OAuth login
state parameter to the identity provider. The victim authenticates normally with the IdP.- Victim clicks login on the legitimate application after cookies are planted
- Application uses
@fastify/oauth2for login (not just API token grants)
- If the application uses server-side session binding (
generateStateFunction/checkStateFunction) instead of cookie-only state, the planted cookie is irrelevant - Timing window: cookies must be planted before the victim's next login
Callback completes under attacker's session
state in the query matches state in the cookie — they do, because the attacker planted both. The application exchanges the code and signs the victim into the attacker's account. The victim's subsequent actions (file uploads, form submissions, entered payment info) are now visible to the attacker.- Application relies solely on cookie-state matching (the vulnerable default)
- Attacker monitors their own account for victim-contributed data
- Impact is limited to data the victim enters *after* being logged into the wrong account — the victim's own credentials, tokens, and existing data are never exposed
- Many users notice they are in a foreign account (wrong name, empty history) and log out quickly
The supporting signals.
| In-the-Wild Exploitation | No known exploitation. Not listed on CISA KEV. No reports of active campaigns. Disclosed responsibly on 2026-08-14. |
|---|---|
| Proof of Concept | No public PoC repository identified. The advisory by kleju13 describes the attack scenario conceptually. Exploitation requires a bespoke subdomain control setup per target. |
| EPSS Score | Not yet scored (CVE published 2026-08-15). Expected to be low given Login CSRF's historically low exploitation rate and the subdomain prerequisite. |
| KEV Status | Not listed. No KEV entry as of 2026-08-15. |
| 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 subdomain control) and user interaction. Impact caps at Low C + Low I. |
| Affected Versions | @fastify/oauth2 7.2.0 – 8.2.x. Versions before 7.2.0 had a *different* CSRF issue (CVE-2023-31999, static global state). |
| Fixed Version | 8.3.0 with hostPrefixedCookies: true option. Requires HTTPS. Alternatively, custom generateStateFunction/checkStateFunction with server-side session binding. |
| Scanning / Exposure | ~95,800 weekly npm downloads (npmjs.org, week of 2026-08-03). This is a mid-tier package — significant in the Node.js ecosystem but not ubiquitous. No Shodan/Censys surface since this is a library, not a network service. |
| Disclosure Date | 2026-08-14 (GitHub Security Advisory GHSA-p8h8-rj28-m8q9). CVE assigned 2026-08-15. |
| Credit | Reported by kleju13. Fix by mcollina. Reviewed by UlisesGascon. |
noisgate verdict.
The single most decisive factor is the compounding prerequisite chain: the attacker must already control a related subdomain (implying a separate vulnerability) before this CSRF can even be attempted, and even then the impact ceiling is login-session confusion — not credential theft, not RCE, not privilege escalation. The blast radius is strictly per-user and per-session, with no path to fleet, domain, or supply-chain compromise.
Why this verdict
- Subdomain prerequisite is a full prior vulnerability. The attacker needs XSS or control of a sibling subdomain before this chain even starts. That is itself a separate security finding, and most orgs with subdomain monitoring will close it independently.
- Impact ceiling is login CSRF, not account takeover. The victim ends up in the attacker's session. The victim's own credentials, tokens, and account data are never exposed. The attacker only captures what the victim *does* while unknowingly in the wrong account.
- Role multiplier:
@fastify/oauth2is an application-layer npm library embedded in Node.js web apps. (a) Low-value role: dev/staging apps — login CSRF is inconsequential. (b) Typical role: line-of-business SaaS app — login CSRF leaks user-entered data in one session, blast radius = single user session. (c) High-value role: even if used in a CI/CD dashboard or internal tool, login CSRF cannot escalate to RCE, domain takeover, or supply-chain compromise. The component is not a hypervisor, IdP, DC, backup server, or kernel-mode agent. No high-value-role floor applies. - AC:H + UI:R compounding. Both CVSS modifiers are present. The attacker cannot automate this at scale — each target requires subdomain access on that target's domain plus social engineering to get the victim to visit the subdomain and then log in.
- SameSite=Lax partial mitigation already in place. The v7.2.0 fix added SameSite=Lax to state cookies, which blocks many cross-site cookie-planting vectors. The remaining attack surface is *same-site* subdomains only.
Why not higher?
Login CSRF does not lead to account takeover of the victim's account, remote code execution, or privilege escalation. The attacker gains visibility into one session's user-contributed data at most. There is no path from this vulnerability to fleet compromise, lateral movement, or supply-chain impact. The subdomain control prerequisite dramatically limits the reachable attacker population.
Why not lower?
Login CSRF is a recognized OAuth threat (RFC 6819) with real-world abuse potential for capturing sensitive user-entered data (payment info, PII, file uploads) in applications that handle such data. The ~96k weekly download count means a non-trivial number of applications are potentially affected. Dismissing it entirely would be irresponsible for apps that process sensitive user submissions.
What to do — in priority order.
- Enable server-side session-bound state validation — Implement custom
generateStateFunctionandcheckStateFunctionthat bind the OAuth state to a server-side session (e.g., stored in Redis or a session store) rather than relying on the cookie-only default. This completely neutralizes the cookie-planting vector regardless of version. No mitigation SLA applies for MEDIUM — go straight to the 365-day remediation window. - Upgrade to @fastify/oauth2 >=8.3.0 and set hostPrefixedCookies: true — The fix uses
__Host-prefixed cookies, which browsers refuse to set from subdomains or over HTTP. This is the definitive remediation. Requires HTTPS (which you should already have). Target within the 365-day noisgate remediation SLA. - Audit and remediate dangling subdomains — Run subdomain takeover scans (Nuclei dangling-cname templates, Subjack, or commercial ASM tools) against your domains. Eliminating the subdomain control prerequisite removes the attack surface entirely, independent of the library version.
- Add login anomaly detection — Alert on a single account receiving logins from multiple distinct IPs or geolocations in a short window. This is a trailing indicator of login CSRF abuse and helps detect exploitation if it occurs.
- Standard CSRF tokens on your own forms — this is an OAuth protocol-level CSRF, not a form-submission CSRF. Your app's own anti-CSRF middleware (e.g.,
@fastify/csrf-protection) does not protect the OAuth callback endpoint because the request comes from the IdP redirect, not a form POST. - WAF rules blocking cross-site requests — the attack is same-site (subdomain to parent domain), so WAF cross-origin policies do not trigger. The malicious cookie is set from a sibling host under the same registrable domain.
- Requiring MFA on login — MFA authenticates the user to the IdP, but login CSRF happens *after* successful IdP authentication. The victim authenticates correctly with their own MFA; they just end up in the attacker's application session afterward.
Crowdsourced verification payload.
Run this on any machine with node and npm installed (no special privileges needed). It checks whether the installed version of @fastify/oauth2 in a given project is in the vulnerable range. Usage: bash check_cve_2026_18165.sh /path/to/your/project
#!/usr/bin/env bash
# check_cve_2026_18165.sh — Detect CVE-2026-18165 in @fastify/oauth2
# Usage: bash check_cve_2026_18165.sh /path/to/node/project
# Exit codes: 0 = PATCHED, 1 = VULNERABLE, 2 = UNKNOWN
set -euo pipefail
PROJECT_DIR="${1:-.}"
PKG_NAME="@fastify/oauth2"
if [ ! -d "$PROJECT_DIR/node_modules" ]; then
echo "UNKNOWN — node_modules not found in $PROJECT_DIR. Run 'npm install' first."
exit 2
fi
PKG_JSON="$PROJECT_DIR/node_modules/@fastify/oauth2/package.json"
if [ ! -f "$PKG_JSON" ]; then
echo "UNKNOWN — $PKG_NAME is not installed in this project."
exit 2
fi
VERSION=$(node -e "console.log(require('$PKG_JSON').version)" 2>/dev/null)
if [ -z "$VERSION" ]; then
echo "UNKNOWN — could not parse version from $PKG_JSON."
exit 2
fi
echo "Detected $PKG_NAME version: $VERSION"
# Vulnerable: 7.2.0 <= version < 8.3.0
# Use node for semver comparison
RESULT=$(node -e "
const v = '$VERSION';
const parts = v.split('.').map(Number);
const [major, minor, patch] = parts;
if (major > 8 || (major === 8 && minor >= 3)) {
console.log('PATCHED');
} else if (major === 8 || (major === 7 && minor >= 2)) {
console.log('VULNERABLE');
} else {
console.log('PATCHED');
}
" 2>/dev/null)
if [ "$RESULT" = "VULNERABLE" ]; then
echo "VULNERABLE — $PKG_NAME $VERSION is affected by CVE-2026-18165 (Login CSRF via plantable OAuth state cookies)."
echo "Upgrade to >= 8.3.0 and enable hostPrefixedCookies: true."
exit 1
elif [ "$RESULT" = "PATCHED" ]; then
echo "PATCHED — $PKG_NAME $VERSION is not affected by CVE-2026-18165."
exit 0
else
echo "UNKNOWN — could not determine vulnerability status for version $VERSION."
exit 2
fiIf you remember one thing.
@fastify/oauth2 7.2.0–8.2.x. The vendor's MEDIUM/4.2 rating is accurate — the attack requires subdomain control as a prerequisite, user interaction, and tops out at session confusion (not account takeover or RCE). No noisgate mitigation SLA applies for MEDIUM — go straight to the 365-day remediation window. Concretely: add this to your next quarterly dependency update cycle. Upgrade to @fastify/oauth2 >=8.3.0 and enable hostPrefixedCookies: true (requires HTTPS). If you handle sensitive user-submitted data (payments, PII, file uploads) through a Fastify OAuth login flow, prioritize it sooner — within 90 days — and implement server-side session-bound state validation as an immediate hardening measure. In parallel, run a subdomain takeover scan across your domains to eliminate the prerequisite attack surface. There is no active exploitation and no KEV listing, so this is a planned remediation item, not a fire drill.Sources
What defenders are saying.
Crowdsourced verification outputs.
Results submitted by users who ran the verification payload against their environment.