← Back to Feed CACHED · 2026-10-18 15:39:40 · CACHE_KEY CVE-2026-110024
CVE-2026-110024 · CWE-613 · Disclosed 2026-10-18

@fastify/jwt top-level sign/verify silently drops global temporal defaults when per-call options are passed

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

Like a locksmith who quietly stops adding deadbolts whenever you ask for a different key color

CVE-2026-110024 is a logic bug in @fastify/jwt versions prior to 10.2.5. When a developer calls the top-level fastify.jwt.sign() or fastify.jwt.verify() methods and passes *any* per-call option (e.g., { algorithm: 'RS256' }), the library replaces the entire global defaults object instead of merging. The result: every global default the call doesn't explicitly restate is silently dropped — including expiresIn and maxAge. Tokens minted through this path carry no exp claim and never expire; verification stops enforcing maximum age. The affected range is @fastify/jwt ≥ 9.0.0 < 10.2.5 (all versions where the top-level API exists). The route-level decorators reply.jwtSign() and request.jwtVerify() already merge correctly and are not affected.

The vendor's MEDIUM 6.8 is slightly generous for real-world exposure. The CVSS vector correctly flags AC:H and PR:L — the attacker can't *trigger* this bug remotely; it's the application's own code that silently produces bad tokens. The attacker merely benefits from a token that was minted without expiration, either through normal auth flows or token theft. More importantly, the documented and recommended pattern in the Fastify ecosystem is route-level decorators, which are safe. Only developers who use the top-level API for background services, token-issuance helpers, or CLI tooling hit this path. In a fleet of Fastify apps, a meaningful but minority fraction are exposed. We downgrade to 5.5 to reflect that narrowed blast radius.

"Silent option-merge bug mints never-expiring JWTs, but only on a niche code path most apps don't hit."
02 · The Attack Path

4 steps from start to impact.

STEP 01

Application uses vulnerable code path

The Fastify application registers @fastify/jwt < 10.2.5 with global temporal defaults (expiresIn, maxAge). Somewhere in the codebase — typically a token-issuance service, background job, or helper outside a route handler — the developer calls fastify.jwt.sign(payload, { ...perCallOptions }) with at least one per-call option. This silently replaces the entire defaults object, dropping expiresIn.
Conditions required:
  • Application uses @fastify/jwt < 10.2.5
  • Developer calls top-level fastify.jwt.sign() with per-call options
  • Global defaults include temporal protections (expiresIn, maxAge)
Where this breaks in practice:
  • Route-level decorators (reply.jwtSign) are the documented/recommended pattern and are NOT affected
  • Only top-level API callers with per-call options trigger the bug
  • Developer must have configured global temporal defaults for the drop to matter
Detection/coverage: SCA scanners (Snyk, npm audit, Socket.dev, Dependabot) flag the vulnerable version. Static analysis can grep for fastify.jwt.sign( with a second argument.
STEP 02

Token minted without expiration

The library produces a JWT with no exp claim. The token is returned to the user (or stored for later use) as if it were a normal short-lived token. Neither the application nor the user receives any error or warning. The token is cryptographically valid and accepted by all verifiers that trust the signing key.
Conditions required:
  • Step 1 code path is exercised
Where this breaks in practice:
  • If the application independently checks exp at the application layer (rare but possible), the missing claim may be caught
  • Tokens minted via route decorators in the same application remain correctly bounded
Detection/coverage: JWT inspection tools (jwt.io, jwt-cli) show no exp claim. Log analysis for tokens with abnormally long lifetimes.
STEP 03

Verification fails to enforce maxAge

When the same library verifies incoming tokens via fastify.jwt.verify(token, { ...perCallOptions }), the global maxAge default is similarly dropped. The verifier accepts tokens of any age, including the never-expiring tokens from Step 2. Even tokens that SHOULD have expired based on global config are accepted.
Conditions required:
  • Application uses top-level fastify.jwt.verify() with per-call options
  • Global maxAge was configured but gets dropped
Where this breaks in practice:
  • Route-level request.jwtVerify() still enforces maxAge correctly
  • Applications that don't pass per-call options to verify are unaffected
Detection/coverage: Audit logs showing successful authentications with tokens older than the configured maxAge.
STEP 04

Attacker achieves persistent access

An attacker who obtains a never-expiring token — through normal authentication, session hijacking, token theft from logs/storage, or any other means — retains valid access indefinitely. Password resets, account deactivation, and token rotation policies all fail to revoke access because the token has no expiration boundary. The attacker maintains the privilege level of the original token holder.
Conditions required:
  • Attacker possesses a token minted through the vulnerable path
  • Application relies on JWT expiration as a revocation mechanism (stateless JWT pattern)
Where this breaks in practice:
  • Applications with server-side session stores or token blocklists can revoke tokens independently of exp
  • Short-lived refresh-token architectures limit the blast radius even if access tokens lack exp
  • Attacker still needs to obtain a valid token through some means (PR:L or separate vuln)
Detection/coverage: Anomaly detection on session duration. Identity-aware proxies flagging tokens with no exp claim.
03 · Compensating Control

1
MEDIUM 5.5→IGNORE 0.0
SEVERITY REDUCED
Switch to route-level decorators — Replace all calls to fastify.jwt.sign() / fastify.jwt.verify() with reply.jwtSign() / request.jwtVerify() inside route handlers. These decorators already merge per-call options with global defaults correctly and are completely unaffected by CVE-2026-110024. This is a code change, not a config change, so test before deploying. As a MEDIUM verdict, this falls under the noisgate remediation SLA of 365 days, but given the simplicity of the fix, aim to complete within 30 days.
2
MEDIUM 5.5→IGNORE 0.0
SEVERITY REDUCED
Explicitly restate temporal defaults in every per-call options object — If you must use the top-level API, add expiresIn and maxAge to every per-call options object passed to fastify.jwt.sign() and fastify.jwt.verify(). This is the vendor-recommended workaround. Example: fastify.jwt.sign(payload, { algorithm: 'RS256', expiresIn: '15m' }). This eliminates the silent drop. Apply within the noisgate remediation SLA of 365 days for MEDIUM.
3
MEDIUM 5.5→IGNORE 0.0
SEVERITY REDUCED
Upgrade to @fastify/jwt 10.2.5 — The definitive fix. Run npm install @fastify/[email protected] or update your lockfile. The patch merges per-call options with global defaults instead of replacing them. No breaking changes reported. Deploy within the noisgate remediation SLA of 365 days.
4
MEDIUM 5.5→LOW 2.0
SEVERITY REDUCED
Implement server-side token revocation — If you use stateless JWTs, add a token blocklist or server-side session store (Redis, database) that can invalidate tokens independently of the exp claim. This defends against both this CVE and all other token-lifetime bugs. This is a defense-in-depth measure that reduces the residual severity of any JWT expiration flaw.
5
MEDIUM 5.5→LOW 2.5
SEVERITY REDUCED
Add JWT claim validation middleware — Deploy middleware or an API gateway rule that rejects any incoming JWT lacking an exp claim. This catches tokens minted through the vulnerable code path even before you patch the library. Tools like Kong, Envoy, or custom Express/Fastify middleware can enforce this.
What doesn't work
  • WAF rules — The tokens are cryptographically valid JWTs signed with the correct key. No WAF signature can distinguish a never-expiring token from a normal one at the network layer.
  • Rotating signing keys — Key rotation invalidates old tokens, but only after the rotation window. It doesn't fix the root cause, and any token minted after rotation with the new key will still lack exp if the vulnerable code path persists.
  • Rate limiting — Throttling API requests doesn't address token lifetime. A never-expiring token used at normal rates is indistinguishable from a legitimate session.
04 · Intelligence Metadata

The supporting signals.

In-the-Wild ExploitationNone observed. Not listed in CISA KEV. No reports of active exploitation from any threat intelligence source as of 2026-10-18.
Proof-of-ConceptNo public PoC. No repositories found on GitHub, pocindex.io, ExploitDB, or Nuclei templates for CVE-2026-110024. The bug is trivially reproducible by calling fastify.jwt.sign(payload, { algorithm: 'RS256' }) with a global expiresIn configured, but no weaponized tooling exists.
EPSS ScoreNot yet scored. FIRST EPSS API returns empty data — expected for a same-day disclosure (2026-10-18). Given the library-level scope and no exploitation history, expect a low EPSS once scored (likely < 0.05, bottom quartile).
KEV StatusNot listed. No CISA Known Exploited Vulnerabilities entry.
CVSS VectorCVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:U/C:H/I:H/A:N — Network-reachable but high complexity (requires specific code path), low privileges (valid account needed), no availability impact. C:H and I:H reflect that a non-expiring token grants full user-level read/write access indefinitely.
Affected Versions@fastify/jwt < 10.2.5 (all versions with the top-level sign/verify API). Versions ≥ 10.2.5 merge per-call options with global defaults correctly.
Fixed Version10.2.5 — commit 5475979 on fastify/fastify-jwt. No distro backports apply (npm package, single upstream).
Installed Base~600K–700K weekly npm downloads, 154 direct dependents on npm. Fastify itself exceeds 10M monthly downloads. This is a popular but not ubiquitous package — compare to jsonwebtoken at ~40M/week.
Scanning/ExposureNo Shodan/GreyNoise/Censys exposure data is relevant — this is an application-layer npm library, not a network-facing appliance. Detection is via SCA (Snyk, npm audit, Socket.dev, Dependabot, Renovate).
CreditsReported by awdr74100. Fix by mcollina (Fastify core maintainer). Reviewed by UlisesGascon. Published via OpenJS Foundation security process.

Sources.

  1. GitHub Security Advisory GHSA-f5gh-84pj-v2pg
  2. Strix.ai CVE-2026-110024 Analysis
  3. ThreatInt CVE-2026-110024
  4. fastify/fastify-jwt GitHub Releases
  5. @fastify/jwt npm Package
  6. fastify/fastify-jwt Security Advisories Overview
  7. VulDB Entry 421763
  8. Socket.dev @fastify/jwt Security Analysis
05 · The Call

Final Verdict
= UNCHANGED to MEDIUM (5.5/10)

Why this verdict

  • Route decorators are safe — the recommended pattern is unaffected. The Fastify documentation and ecosystem convention is to use request.jwtVerify() and reply.jwtSign() inside route handlers. These already merge options correctly. Only the top-level fastify.jwt.sign()/fastify.jwt.verify() API — typically used in background services, CLI tooling, or custom token-issuance helpers — triggers the bug. This significantly narrows the exposed population.
  • Developer must use a specific code pattern to trigger. The bug requires (a) global temporal defaults configured, AND (b) per-call options passed to the top-level API. If either condition is absent, the vulnerability doesn't manifest. This isn't an attacker-triggered flaw — it's a silent developer-side misconfiguration the library enables.
  • Impact is session persistence, not direct exploitation. The attacker doesn't gain new access — they retain access they already had (PR:L) or extend the life of a stolen token. There's no RCE, no privilege escalation, no data exfiltration beyond what the token's original scope already permitted. The harm is that token expiration stops working as a revocation boundary.
  • No exploitation evidence compounds the narrowing. Zero PoCs, zero KEV, zero in-the-wild activity, no EPSS score yet. The vulnerability was disclosed today and there's no indication threat actors are aware of or interested in this specific code path.
  • Role multiplier: @fastify/jwt is an application-layer npm library. It is NOT canonically a high-value-role component (not an IdP, hypervisor, CA, PAM, or network edge). (a) In dev sandboxes and prototypes: negligible impact. (b) In typical API servers/microservices: session persistence at user/tenant level — the attacker maintains one user's access, not fleet-wide compromise. (c) In the highest-value scenario — a SaaS platform's auth token issuance — the blast radius is persistent user-level access, which is serious for compliance but does NOT reach domain takeover, fleet compromise, or supply-chain pivot. The floor remains MEDIUM.

Why not higher?

The vulnerability cannot reach HIGH because the attacker position is PR:L (requires existing valid account or token theft via a separate flaw), the affected code path is a minority pattern in the ecosystem (route decorators are safe), and the blast radius caps at persistent user-level session access — it does not escalate privileges, enable RCE, or compromise infrastructure. @fastify/jwt is an application library, not identity infrastructure; even in worst-case SaaS deployments the outcome is tenant-scoped, not fleet-scoped.

Why not lower?

The vulnerability should not be LOW because when the affected code path IS hit, the result is genuinely dangerous: tokens with no expiration ever. This breaks a fundamental security invariant. Any token theft incident becomes permanent access rather than time-bounded. For organizations subject to PCI DSS, SOC 2, or HIPAA session-timeout requirements, this is a compliance violation. The ~600K weekly downloads mean a meaningful absolute number of affected applications exist, even if the fraction using the vulnerable pattern is modest.

06 · Verification

Crowdsourced verification payload.

Run this script on any host where the Fastify application is deployed, or in your CI pipeline. Execute from the application's root directory (where package.json lives). Requires node (any version) and read access to node_modules. Example: bash check_cve_2026_110024.sh /opt/myapp

noisgate-verify.sh
BASHREAD-ONLYSAFE
#!/usr/bin/env bash
# check_cve_2026_110024.sh — Detect @fastify/jwt < 10.2.5 (CVE-2026-110024)
# Usage: bash check_cve_2026_110024.sh [/path/to/app/root]
# Exit codes: 0 = PATCHED, 1 = VULNERABLE, 2 = UNKNOWN

set -euo pipefail

APP_ROOT="${1:-.}"
PKG_JSON="$APP_ROOT/node_modules/@fastify/jwt/package.json"

if [ ! -f "$PKG_JSON" ]; then
  echo "UNKNOWN — @fastify/jwt not found in $APP_ROOT/node_modules"
  exit 2
fi

# Extract version from package.json
VERSION=$(node -e "try { console.log(require('$PKG_JSON').version) } catch(e) { console.log('error') }" 2>/dev/null)

if [ "$VERSION" = "error" ] || [ -z "$VERSION" ]; then
  echo "UNKNOWN — could not parse version from $PKG_JSON"
  exit 2
fi

echo "Detected @fastify/jwt version: $VERSION"

# Compare versions: vulnerable if < 10.2.5
REQUIRED="10.2.5"
SORTED_LOWEST=$(printf '%s\n%s' "$VERSION" "$REQUIRED" | sort -V | head -n1)

if [ "$VERSION" = "$REQUIRED" ] || [ "$SORTED_LOWEST" = "$REQUIRED" ]; then
  echo "PATCHED — @fastify/jwt $VERSION >= $REQUIRED (CVE-2026-110024 fixed)"
  exit 0
else
  echo "VULNERABLE — @fastify/jwt $VERSION < $REQUIRED (CVE-2026-110024 applies)"
  # Additional check: scan for top-level API usage
  echo ""
  echo "Scanning for potentially affected code patterns..."
  HITS=$(grep -rn 'fastify\.jwt\.sign\|fastify\.jwt\.verify\|app\.jwt\.sign\|app\.jwt\.verify\|server\.jwt\.sign\|server\.jwt\.verify' "$APP_ROOT/src" "$APP_ROOT/lib" "$APP_ROOT/routes" 2>/dev/null || true)
  if [ -n "$HITS" ]; then
    echo "Found top-level API usage (review these for per-call options):"
    echo "$HITS"
  else
    echo "No top-level fastify.jwt.sign/verify calls found in src/, lib/, routes/ — you may only use route decorators (which are safe)."
  fi
  exit 1
fi