← Back to Feed CACHED · 2026-08-15 14:44:37 · CACHE_KEY CVE-2026-18165
CVE-2026-18165 · CWE-352 · Disclosed 2026-08-15

@fastify/oauth2 is an OAuth 2.0 plugin for Fastify.

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

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.

"Login CSRF via cookie planting needs subdomain control; impact is session confusion, not account takeover."
02 · The Attack Path

4 steps from start to impact.

STEP 01

Attacker obtains related-origin cookie access

The attacker must control a host that shares the target application's registrable domain, or find an XSS on a subdomain. This lets them set cookies scoped to the parent domain (e.g., .example.com). Forgotten staging servers, dangling DNS CNAMEs, and subdomain takeover via unclaimed cloud endpoints are the usual vectors.
Conditions required:
  • Attacker controls a subdomain or has XSS on a sibling host under the same registrable domain
Where this breaks in practice:
  • 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
Detection/coverage: Subdomain takeover monitoring (e.g., Nuclei detect-dangling-cname, CanITakeOverXYZ project). WAF subdomain XSS rules.
STEP 02

Plant OAuth state cookies in victim's browser

From the controlled subdomain, the attacker serves JavaScript or a redirect that sets the 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.
Conditions required:
  • Victim visits attacker-controlled subdomain page
  • Target app uses HTTPS but cookies are not __Host- prefixed (the vulnerable default)
Where this breaks in practice:
  • 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
Detection/coverage: Browser DevTools / proxy inspection of Set-Cookie headers. No standard IDS signature exists for cookie planting.
STEP 03

Victim initiates or is redirected through OAuth login

The victim navigates to the legitimate application and triggers an OAuth login flow. The application reads the attacker-planted state cookie and sends it as the state parameter to the identity provider. The victim authenticates normally with the IdP.
Conditions required:
  • Victim clicks login on the legitimate application after cookies are planted
  • Application uses @fastify/oauth2 for login (not just API token grants)
Where this breaks in practice:
  • 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
STEP 04

Callback completes under attacker's session

The IdP redirects back to the application's callback URL with the authorization code and the state parameter. The application checks that 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.
Conditions required:
  • Application relies solely on cookie-state matching (the vulnerable default)
  • Attacker monitors their own account for victim-contributed data
Where this breaks in practice:
  • 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
Detection/coverage: Anomaly detection on login patterns: a single account receiving logins from many different IPs/devices could indicate login CSRF abuse.
03 · Intelligence Metadata

The supporting signals.

In-the-Wild ExploitationNo known exploitation. Not listed on CISA KEV. No reports of active campaigns. Disclosed responsibly on 2026-08-14.
Proof of ConceptNo public PoC repository identified. The advisory by kleju13 describes the attack scenario conceptually. Exploitation requires a bespoke subdomain control setup per target.
EPSS ScoreNot yet scored (CVE published 2026-08-15). Expected to be low given Login CSRF's historically low exploitation rate and the subdomain prerequisite.
KEV StatusNot listed. No KEV entry as of 2026-08-15.
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 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 Version8.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 Date2026-08-14 (GitHub Security Advisory GHSA-p8h8-rj28-m8q9). CVE assigned 2026-08-15.
CreditReported by kleju13. Fix by mcollina. Reviewed by UlisesGascon.
04 · The Call

noisgate verdict.

Final Verdict
= UNCHANGED to MEDIUM (4.2/10)

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.

HIGH Vulnerability mechanics and affected version range
HIGH Vendor severity alignment
MEDIUM Exploitation likelihood (newly disclosed, no EPSS data yet)

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/oauth2 is 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.

05 · Compensating Control

What to do — in priority order.

  1. Enable server-side session-bound state validation — Implement custom generateStateFunction and checkStateFunction that 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.
  2. 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.
  3. 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.
  4. 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.
What doesn't work
  • 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.
06 · Verification

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

noisgate-verify.sh
BASHREAD-ONLYSAFE
#!/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
fi
07 · Bottom Line

If you remember one thing.

TL;DR
This is a legitimate but narrow OAuth protocol-level CSRF in @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

  1. GitHub Advisory GHSA-p8h8-rj28-m8q9
  2. @fastify/oauth2 npm package
  3. CVE-2023-31999 — Prior CSRF in fastify-oauth2 (static state)
  4. Snyk — CVE-2023-35935 CSRF in @fastify/oauth2
  5. RFC 6819 §4.4.1.8 — OAuth Threat Model: CSRF
  6. FIRST EPSS — Exploit Prediction Scoring System
  7. Fastify Security Audit 2023-2024 Report
Peer Review

What defenders are saying.

Submit a review attribution: handle + country only
0 flags selected · stored anonymously
Validation Results

Crowdsourced verification outputs.

Results submitted by users who ran the verification payload against their environment.