Like handing out VIP wristbands that the bouncer never actually checks because the house stamp overrides everything
CVE-2026-18500 affects @fastify/jwt versions prior to 10.2.2 (~636K weekly npm downloads). When an application registers a global secret *and* passes a per-request verification key via request.jwtVerify({ verify: { key } }) or reply.jwtSign(payload, { key }), the plugin silently resolves the global secret first and uses it for signature verification, completely ignoring the caller-supplied key. Any JWT signed with the global secret passes verification on routes that were intended to enforce a different, per-request cryptographic boundary. The direct fastify.jwt.verify(token, { key }) API is not affected.
The vendor rates this HIGH 8.1 (CVSS:3.1 AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N). That score accurately reflects the network-reachable, low-privilege, high-CI impact *when the per-request key pattern is in use*. However, the score does not account for the fact that many — arguably most — @fastify/jwt consumers simply use a single global secret and never exercise the per-request key API. For those deployments, this CVE is a no-op. noisgate applies a modest downward adjustment to 7.2 to reflect the narrower-than-universal affected population while acknowledging the severity of the impact where it does apply.
4 steps from start to impact.
Obtain valid low-privilege JWT
- Target application uses
@fastify/jwt< 10.2.2 - Application exposes account registration or the attacker has compromised a low-privilege account
- Applications behind SSO / IdP federation may not issue JWTs from
@fastify/jwtdirectly - Rate limiting on auth endpoints is standard
Identify routes using per-request key verification
- Application uses
request.jwtVerify({ verify: { key } })on at least one route
- Many applications use a single global secret and are not vulnerable
- Per-request key usage is a deliberate, less common architectural choice — common in multi-tenant SaaS but rare in single-tenant monoliths
Present global-secret JWT on key-protected route
@fastify/jwt resolves the global secret *before* the per-request key and uses it for verification. The signature check passes. The route handler executes as if the attacker had presented a token signed with the intended per-request key.- Vulnerable version of
@fastify/jwtin use - Route handler does not perform secondary, out-of-band key verification
- Some applications may perform additional claims validation (e.g.,
iss,aud,tenant_id) that limits cross-boundary access even with a valid signature - Applications using asymmetric per-request keys (RS256/ES256) alongside an HMAC global secret may trigger algorithm-mismatch failures in some configurations
kid or iss claims versus expected values may flag anomalous tokens. No CVE-specific scanner signatures observed as of 2026-08-15.Access unauthorized resources
- Route handler trusts the JWT verification result without additional authorization checks
- Defense-in-depth authorization layers (e.g., OPA, RBAC middleware checking database-backed permissions) may block the actual data access even if the JWT verification succeeds
fastify.jwt.verify(token, { key }) for key-sensitive routes — The direct API is confirmed unaffected by this bug. Refactoring key-sensitive route handlers to use fastify.jwt.verify() instead of request.jwtVerify() eliminates the vulnerability without upgrading. Deploy within 30 days per noisgate mitigation SLA for HIGH.iss, aud, kid, or custom tenant_id claims against expected values in route handlers. This defense-in-depth measure blocks cross-boundary access even if the wrong key verifies the signature. Deploy within 30 days.npm audit and SCA scans to identify affected services — Use npm audit, Snyk, or Socket to enumerate all services importing @fastify/jwt < 10.2.2. Prioritize services that use the per-request key pattern. This is a triage step — execute immediately.@fastify/jwt 10.2.2 — The definitive fix. Version 10.2.2 was released 2026-08-14. Target remediation within 180 days per noisgate remediation SLA for HIGH, but aim for 30 days given the simplicity of the npm update.- WAF / API gateway JWT validation — if the gateway re-verifies tokens using its own key, it may catch mismatches, but most gateways pass through to the application's own verification. The bug is in the application-layer library, not the network path.
- Rotating the global secret — the vulnerability is in key *selection priority*, not key compromise. A new global secret will still override per-request keys.
- Rate limiting — the attacker only needs one valid token and one request to exploit the bypass. Rate limiting does not help.
The supporting signals.
| In-the-wild exploitation | No known exploitation as of 2026-08-15. Not listed on CISA KEV. |
|---|---|
| Proof-of-concept | No public PoC repository identified. The advisory was published by the maintainer (@mcollina) and includes a clear description of the trigger pattern (request.jwtVerify with per-request key), making reproduction straightforward for anyone with a test harness. |
| EPSS | Not yet scored (advisory published 2026-08-14). Expected to be low (<5th percentile) given library-level bug requiring specific usage pattern. |
| KEV status | Not listed. No CISA KEV entry as of 2026-08-15. |
| CVSS vector | CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N — Network-reachable, low privilege required, no user interaction, high confidentiality and integrity impact, no availability impact. Scope unchanged. |
| Affected versions | @fastify/jwt < 10.2.2. All 10.x, 9.x, and earlier releases using per-request key verification are affected. |
| Fixed version | 10.2.2 (released 2026-08-14 via npm) |
| Scanning / exposure | Not applicable for Shodan/GreyNoise — this is a server-side library, not a network-exposed service fingerprint. npm downloads: ~636K/week, indicating broad adoption in the Node.js ecosystem. Exposure is code-level, detectable via SBOM/SCA tooling (Snyk, Socket, npm audit). |
| Disclosure date | 2026-08-14 (GHSA-j4cx-787j-xjqg published alongside patch) |
| Reporter | Matteo Collina (@mcollina), Fastify core maintainer — coordinated self-disclosure. |
Sources.
Why this verdict
- Narrow affected pattern: The bug only fires when an application uses
request.jwtVerify({ verify: { key } })orreply.jwtSign(payload, { key }). Applications using only the global secret (likely the majority of@fastify/jwtconsumers) are completely unaffected, reducing the real-world blast radius. - PR:L prerequisite: The attacker needs a valid JWT from the target application — this requires either a legitimate account or a credential compromise, which adds friction and presupposes an initial foothold.
- Role multiplier:
@fastify/jwtis a general-purpose library, not a canonical high-value-role component. However, the per-request key pattern is *specifically* the pattern used for multi-tenant SaaS isolation and admin/user key separation. When it is used, the blast radius is tenant-isolation bypass (cross-tenant data access, privilege escalation across JWT domains). This is a HIGH-floor outcome for the subset of deployments using this pattern, preventing a downgrade below HIGH. - No exploitation evidence or public PoC: The advisory is <48 hours old with no known weaponization, reducing urgency but not severity.
Why not higher?
A CRITICAL rating would require either active exploitation, a wider affected population, or a higher blast radius. The vulnerability requires a specific usage pattern that most @fastify/jwt consumers do not exercise. The PR:L requirement means unauthenticated mass exploitation is not possible. There is no availability impact, no code execution — this is an authorization boundary bypass, not RCE.
Why not lower?
When the per-request key pattern *is* in use, the impact is a complete cryptographic authorization bypass — the security boundary the developer explicitly constructed is silently ignored. Multi-tenant SaaS platforms using this pattern face cross-tenant data exposure. The 636K weekly download count means even a small percentage of affected deployments represents thousands of services. The network-reachable, low-complexity attack vector keeps this firmly in HIGH territory.
Crowdsourced verification payload.
Run this on a CI/auditor workstation with Node.js and jq installed. It inspects package-lock.json files to detect vulnerable @fastify/jwt versions. Usage: bash check_cve_2026_18500.sh /path/to/project — no special privileges required.
#!/usr/bin/env bash
# CVE-2026-18500 checker for @fastify/jwt
# Checks if the installed version is < 10.2.2
# Usage: bash check_cve_2026_18500.sh /path/to/project
# Exit codes: 0 = PATCHED, 1 = VULNERABLE, 2 = UNKNOWN
set -euo pipefail
PROJECT_DIR="${1:-.}"
PACKAGE_LOCK="$PROJECT_DIR/package-lock.json"
NODE_MODULES="$PROJECT_DIR/node_modules/@fastify/jwt/package.json"
FIXED_MAJOR=10
FIXED_MINOR=2
FIXED_PATCH=2
check_version() {
local ver="$1"
local major minor patch
major=$(echo "$ver" | cut -d. -f1)
minor=$(echo "$ver" | cut -d. -f2)
patch=$(echo "$ver" | cut -d. -f3 | cut -d- -f1)
if [ "$major" -gt "$FIXED_MAJOR" ]; then
echo "PATCHED"
return 0
elif [ "$major" -eq "$FIXED_MAJOR" ] && [ "$minor" -gt "$FIXED_MINOR" ]; then
echo "PATCHED"
return 0
elif [ "$major" -eq "$FIXED_MAJOR" ] && [ "$minor" -eq "$FIXED_MINOR" ] && [ "$patch" -ge "$FIXED_PATCH" ]; then
echo "PATCHED"
return 0
else
echo "VULNERABLE"
return 1
fi
}
# Try node_modules first
if [ -f "$NODE_MODULES" ]; then
VERSION=$(jq -r '.version' "$NODE_MODULES" 2>/dev/null)
if [ -n "$VERSION" ] && [ "$VERSION" != "null" ]; then
echo "[*] Found @fastify/jwt $VERSION in node_modules"
check_version "$VERSION"
exit $?
fi
fi
# Fall back to package-lock.json
if [ -f "$PACKAGE_LOCK" ]; then
VERSION=$(jq -r '.packages["node_modules/@fastify/jwt"].version // .dependencies["@fastify/jwt"].version // empty' "$PACKAGE_LOCK" 2>/dev/null)
if [ -n "$VERSION" ]; then
echo "[*] Found @fastify/jwt $VERSION in package-lock.json"
check_version "$VERSION"
exit $?
fi
fi
echo "UNKNOWN — @fastify/jwt not found in $PROJECT_DIR"
exit 2