← Back to Feed CACHED · 2026-10-27 11:40:13 · CACHE_KEY CVE-2026-117499
CVE-2026-117499 · CWE-362 · Disclosed 2026-10-27

@fastify/session session revocation bypass via logout race condition with rolling sessions

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

Like someone photocopying your hotel key card in the split-second the front desk is shredding it

CVE-2026-117499 is a TOCTOU (time-of-check-time-of-use) race condition in the @fastify/session npm package, all versions prior to 11.1.6. When rolling sessions are enabled (the default), a request that restores a session from the store *before* a concurrent Session#destroy() call completes can write its own stale copy *after* the destroy finishes, effectively resurrecting the session ID with its authenticated payload. The attacker must already possess the victim's valid, signed session cookie — no cookie theft or signature forgery is part of this bug — and must keep a request in flight across the exact window when the victim logs out. The fixed version (11.1.6) introduces a per-session generation counter and a conditional store write (set-if-generation-matches) that rejects stale saves.

The vendor (OpenJS Foundation / Fastify maintainers, CNA: GitHub) rated this LOW at CVSS 3.7 with vector AV:N/AC:H/PR:L/UI:R/S:U/C:L/I:L/A:N. That rating is honest and accurate. The attack complexity is genuinely high — winning the race requires sub-request-cycle timing precision against an async store — and the prerequisites (possession of a valid signed cookie *plus* victim-initiated logout during the attacker's in-flight request) stack up to a very narrow exploitation window. The impact caps at reviving one session; there is no privilege escalation, no RCE, and no data exfiltration beyond what the session already authorized. This is a correctness bug with security implications, not a weaponizable exploit.

"Session logout race is real but needs cookie theft first, making it a low-priority library bump."
02 · The Attack Path

4 steps from start to impact.

STEP 01

Obtain victim's valid signed session cookie

The attacker must already hold a copy of the victim's current, cryptographically signed session cookie. This could come from XSS, network sniffing on a non-HTTPS segment, malware on the victim's device, or a shared/public workstation. The vulnerability itself does NOT provide cookie theft — it assumes this prerequisite is already met.
Conditions required:
  • Victim's valid signed session cookie in attacker's possession
  • Target application uses @fastify/session < 11.1.6 with default rolling sessions
Where this breaks in practice:
  • HttpOnly + Secure + SameSite cookie flags block most XSS and MITM cookie theft vectors
  • Modern browsers enforce SameSite=Lax by default, preventing cross-site cookie leakage
  • If the attacker already has the victim's session cookie, they already have session access — the incremental value of surviving a logout is marginal
STEP 02

Initiate a long-running request with the stolen cookie

The attacker sends an HTTP request to the Fastify application using the victim's session cookie. The session middleware restores the session from the store (e.g., Redis, memory) at the start of request processing. The attacker needs this request to remain in-flight — ideally hitting a slow endpoint or one with deferred response — so the session object lives in memory past the destroy window.
Conditions required:
  • Network access to the Fastify application
  • An endpoint that takes long enough to keep the request alive during logout
Where this breaks in practice:
  • Request timeouts (typically 30-60s) limit the race window
  • Load balancers and reverse proxies may terminate idle connections
  • Rate limiting may prevent sustained concurrent requests from a single session
Detection/coverage: Anomalous concurrent requests from the same session ID could be flagged by application-level logging or WAF rules.
STEP 03

Victim initiates logout (destroy)

The victim must log out while the attacker's request from Step 2 is still being processed. The logout handler calls Session#destroy(), which deletes the session from the backing store. However, the attacker's in-flight request already has a restored copy of the session in memory.
Conditions required:
  • Victim must actively log out during the attacker's in-flight request window
  • UI:R — this is not attacker-controlled timing
Where this breaks in practice:
  • The attacker cannot force or predict when the victim will log out
  • The window between session restore and response send is typically milliseconds to low seconds
  • If the victim changes their password or the app rotates session secrets, the cookie becomes invalid regardless
STEP 04

Stale session save overwrites the destroy

When the attacker's in-flight request completes, the rolling session middleware automatically calls store.set() to refresh the session TTL. Because the store entry was already deleted by destroy(), this set() call *recreates* the session ID with its original authenticated data. The attacker can now continue using the cookie as if logout never happened.
Conditions required:
  • Rolling sessions enabled (default in @fastify/session)
  • The session store's set() does not check for generation/version conflicts (all stores before the 11.1.6 conditional write protocol)
Where this breaks in practice:
  • Session stores with built-in optimistic locking or CAS (compare-and-swap) semantics would reject the stale write — though no default store shipped with this protection before the fix
  • If the application uses @fastify/secure-session (signed cookies without server-side store), this bug does not apply
Detection/coverage: No standard scanner or DAST tool detects this race condition. Detection requires application-level audit logging that flags session IDs being written after a destroy event for the same ID.
03 · Compensating Control

1
LOW 3.7→IGNORE 0.0
SEVERITY REDUCED
Upgrade @fastify/session to 11.1.6 — This is the only complete fix. The new version adds per-session generation markers and conditional store writes that reject stale saves. Deploy within your normal dependency update cycle. Per the noisgate remediation SLA for LOW, treat this as backlog hygiene with no hard deadline, but don't let it linger past your next quarterly dependency sweep.
2
LOW 3.7→LOW 2.5
Set short absolute session TTL (maxAge ≤ 15 minutes) — Reduce the window during which a stolen cookie is valid. A shorter maxAge means the race condition, even if won, produces a session that expires quickly. This does not fix the bug but limits the attacker's post-exploit dwell time.
3
LOW 3.7→LOW 1.5
Implement session-secret rotation on logout — If your application rotates the session signing secret or HMAC key when a user logs out (or periodically), previously-signed cookies become invalid regardless of whether the session store entry was recreated. This breaks Step 1 of the attack path.
4
LOW 3.7→LOW 3.0
Enforce HttpOnly + Secure + SameSite=Strict on session cookies — These flags do not fix the race condition but dramatically reduce the probability of Step 1 (cookie theft). SameSite=Strict prevents cross-site request attachment; HttpOnly blocks JavaScript access; Secure prevents cleartext interception.
What doesn't work
  • WAF rules — the exploit uses a normal, validly-signed cookie in a normal HTTP request. There is no malicious payload, injection, or anomalous header for a WAF to match on.
  • Rate limiting — the attacker only needs one well-timed request, not a flood. Rate limits do not narrow the race window.
  • Switching to @fastify/secure-session — while this plugin uses client-side signed cookies without a server store (eliminating the race), it is a different session model with different trade-offs (no server-side revocation at all), not a drop-in migration.
04 · Intelligence Metadata

The supporting signals.

In-the-wild exploitationNone reported. Not listed in CISA KEV. No campaigns, threat actor reporting, or incident references found.
Proof-of-conceptNo public PoC. No repositories on GitHub named after CVE-2026-117499. Not indexed on pocindex.io. No ExploitDB, Nuclei template, or Metasploit module. The race condition is conceptually simple but requires precise request timing to reproduce reliably.
EPSSNot yet scored (CVE published 2026-10-27; EPSS typically lags 24-48 hours). Expected to be very low (<1st percentile) given AC:H, PR:L, UI:R, no PoC, and no exploitation history.
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:L/UI:R/S:U/C:L/I:L/A:N — Network-reachable but requires high attack complexity (race condition timing), low privileges (valid session cookie), and user interaction (victim must log out). Impact limited to low confidentiality and integrity; no availability impact. Scope unchanged.
Affected versions@fastify/session all versions < 11.1.6. This includes the entire 10.x and earlier lines. Only affects server-side session stores with rolling sessions enabled (the default).
Fixed version@fastify/session 11.1.6. Fix adds a per-session generation marker and a conditional store.set() that rejects writes from stale generations. Custom session stores must implement the new conditional write interface.
Exposure footprint~208K weekly npm downloads. Fastify is the #2 Node.js web framework behind Express. However, not all Fastify apps use @fastify/session (many use JWT or @fastify/secure-session instead), and the vulnerable path requires rolling sessions + server-side store + concurrent logout.
Disclosure date2026-10-27 (today). Published via GitHub Security Advisory GHSA-2hw7-jxfh-4rm3.
Reportermcollina (Matteo Collina, Fastify co-creator and Node.js TSC member). Self-reported by the maintainer team — indicates the issue was found during internal code review, not external exploitation.
Related prior CVECVE-2024-35220 — a prior insufficient session expiration bug in @fastify/session (versions < 10.9.0). Different root cause but same component lineage.

Sources.

  1. GitHub Security Advisory GHSA-2hw7-jxfh-4rm3
  2. OffSeq Radar — CVE-2026-117499 Detail
  3. @fastify/session GitHub Repository
  4. @fastify/session npm Package
  5. NVD — CVSS 3.1 Statistics for GitHub Inc. (CNA)
  6. PoC Index — CVE Search
  7. Prior CVE-2024-35220 Advisory (related @fastify/session issue)
05 · The Call

Final Verdict
= UNCHANGED to LOW (3.7/10)

Why this verdict

  • Cookie prerequisite is a full prior compromise: The attacker must already possess the victim's valid, signed session cookie. This means they already have session-level access — the race condition only lets them *keep* it past a logout, which is marginal incremental value over what they already have.
  • AC:H is genuine, not inflated: Winning this race requires the attacker's request to be restored from the store *before* destroy completes and to save *after* destroy completes — a window measured in single-digit milliseconds on typical async stores. This is not a click-and-pwn.
  • UI:R is attacker-uncontrollable: The victim must log out at exactly the right moment. The attacker cannot force or reliably predict this. Combined with AC:H, the probability of successful exploitation in a single attempt is extremely low.
  • Impact ceiling is one session: Even successful exploitation only revives one previously-authenticated session. There is no privilege escalation, no lateral movement, no RCE. C:L/I:L/A:N accurately reflects the damage.
  • Role multiplier: @fastify/session is application-tier session middleware for Node.js web apps. (a) *Low-value role:* dev projects, hobby apps — majority of installs, no meaningful blast radius. (b) *Typical role:* line-of-business Fastify APIs with session auth — impact is one user's session survives logout, blast radius is single-user. (c) *High-value role:* a Fastify app acting as an OAuth/OIDC provider or admin portal — theoretically the chain succeeds, but the outcome is still 'one admin session survives logout,' not domain takeover or fleet compromise. The blast radius is user-session-scoped, never host/domain/fleet-scoped. No high-value-role floor applies because the worst-case outcome does not reach domain takeover, fleet compromise, mass data egress, or supply-chain pivot.
  • No exploitation evidence, no PoC, no KEV: Zero real-world signal. Self-reported by the maintainer during code review. This is a proactive fix, not incident response.

Why not higher?

Upgrading to MEDIUM would require either broader impact (e.g., privilege escalation, data exfiltration) or lower attack complexity. This bug has neither. The attacker already needs what the bug gives them (session access), the race window is genuinely narrow, the victim must cooperate by logging out at the right moment, and the impact is capped at session-level persistence with no escalation path. Even in the most sensitive deployment (admin portal, OAuth provider), the outcome is one session surviving one logout — not systemic compromise.

Why not lower?

The bug is real, not theoretical — rolling sessions are the default, the race is architecturally inherent in async store operations, and the fix required a protocol change (generation markers + conditional writes). There is no workaround. In regulated environments (healthcare, finance), session revocation guarantees are compliance requirements (NIST 800-63B §7.1), so even a low-probability bypass has audit significance. IGNORE would understate the correctness impact.

06 · Verification

Crowdsourced verification payload.

Run this on any host where the target Node.js application is deployed, or in your CI pipeline against package.json / package-lock.json. Requires node (or jq for lockfile parsing). No elevated privileges needed. Example: bash check_cve_2026_117499.sh /path/to/your/app

noisgate-verify.sh
BASHREAD-ONLYSAFE
#!/usr/bin/env bash
# check_cve_2026_117499.sh — Detect CVE-2026-117499 in @fastify/session
# Usage: bash check_cve_2026_117499.sh [/path/to/node/app]
# Exit codes: 0 = PATCHED, 1 = VULNERABLE, 2 = UNKNOWN

set -euo pipefail

APP_DIR="${1:-.}"
PKG_NAME="@fastify/session"
FIXED_VERSION="11.1.6"

# Utility: compare semver (returns 0 if $1 >= $2)
version_gte() {
  printf '%s\n%s' "$1" "$2" | sort -t. -k1,1n -k2,2n -k3,3n -C 2>/dev/null
  # sort -C exits 0 if already sorted (i.e., $2 <= $1)
  # We reverse: check if $2 sorts before or equal to $1
  local sorted
  sorted=$(printf '%s\n%s' "$2" "$1" | sort -t. -k1,1n -k2,2n -k3,3n | head -1)
  [ "$sorted" = "$2" ]
}

echo "[*] Checking for CVE-2026-117499 (@fastify/session < $FIXED_VERSION)"
echo "[*] App directory: $APP_DIR"

# Method 1: Check package-lock.json
if [ -f "$APP_DIR/package-lock.json" ]; then
  INSTALLED=$(jq -r '.packages["node_modules/@fastify/session"].version // .dependencies["@fastify/session"].version // empty' "$APP_DIR/package-lock.json" 2>/dev/null || true)
  if [ -n "$INSTALLED" ]; then
    echo "[*] Found @fastify/session $INSTALLED in package-lock.json"
    if version_gte "$INSTALLED" "$FIXED_VERSION"; then
      echo "PATCHED — @fastify/session $INSTALLED >= $FIXED_VERSION"
      exit 0
    else
      echo "VULNERABLE — @fastify/session $INSTALLED < $FIXED_VERSION"
      exit 1
    fi
  fi
fi

# Method 2: Check node_modules directly
if [ -f "$APP_DIR/node_modules/@fastify/session/package.json" ]; then
  INSTALLED=$(jq -r '.version' "$APP_DIR/node_modules/@fastify/session/package.json" 2>/dev/null || true)
  if [ -n "$INSTALLED" ]; then
    echo "[*] Found @fastify/session $INSTALLED in node_modules"
    if version_gte "$INSTALLED" "$FIXED_VERSION"; then
      echo "PATCHED — @fastify/session $INSTALLED >= $FIXED_VERSION"
      exit 0
    else
      echo "VULNERABLE — @fastify/session $INSTALLED < $FIXED_VERSION"
      exit 1
    fi
  fi
fi

# Method 3: npm ls
if command -v npm &>/dev/null; then
  INSTALLED=$(cd "$APP_DIR" && npm ls @fastify/session --json 2>/dev/null | jq -r '.. | .version? // empty' | head -1 || true)
  if [ -n "$INSTALLED" ]; then
    echo "[*] Found @fastify/session $INSTALLED via npm ls"
    if version_gte "$INSTALLED" "$FIXED_VERSION"; then
      echo "PATCHED — @fastify/session $INSTALLED >= $FIXED_VERSION"
      exit 0
    else
      echo "VULNERABLE — @fastify/session $INSTALLED < $FIXED_VERSION"
      exit 1
    fi
  fi
fi

echo "UNKNOWN — @fastify/session not found in $APP_DIR"
exit 2