The bouncer checks your fake ID, says it's legit, then hands your real one to the bartender anyway
CVE-2026-18504 affects Fastify versions prior to 5.12.1. When a route's body schema defines a root-level primitive type (e.g., { type: 'integer' }) and Ajv's default type coercion is enabled, Fastify validates the *coerced* value but exposes the *original, uncoerced* value to the route handler. A JSON body of "10" coerces to integer 10, passes the schema check, but request.body remains the string "10". Object and array body schemas are not affected because their members are coerced in-place. Only root-level primitive body schemas trigger the mismatch.
The vendor's MEDIUM / 5.4 rating is directionally correct and arguably generous. The attack requires authentication (PR:L), the affected surface is narrow (root primitive schemas are uncommon in production REST APIs — the vast majority accept JSON objects), and the impact is limited to low confidentiality and low integrity (C:L/I:L/A:N). There is no path to code execution, privilege escalation, or denial of service. The vulnerability is real but highly situational — exploitation requires the application to make a downstream security decision (e.g., an authorization boundary, a rate-limit check, a financial calculation) that depends on the *type* of the validated root primitive, which is an uncommon pattern.
4 steps from start to impact.
Identify a root-primitive endpoint
POST /transfer expecting { type: 'integer' } for an amount field. This is discoverable through OpenAPI/Swagger docs that Fastify commonly auto-generates, or via trial and error against endpoints that reject object payloads.- Valid authenticated session or API key
- Target application uses Fastify < 5.12.1
- At least one route defines a root-level primitive body schema
- Most production APIs use object body schemas (
{ type: 'object', properties: {...} }), not bare primitives - Developers following REST conventions will wrap primitives in objects
Send a string where an integer is expected
"10" instead of 10. Fastify's Ajv validator coerces "10" to 10, validates it against the integer schema, and passes. However, request.body is set to the original string "10" rather than the coerced integer.- The value must be coercible to the target type under Ajv rules
- If the handler performs its own type check or cast, the mismatch is neutralized
- Many frameworks and ORMs downstream will reject or re-cast the wrong type
Exploit type-dependent business logic
request.body as if it were a validated integer, but it is a string. This can bypass security checks that rely on numeric comparisons — e.g., if (request.body > 1000) throw 'limit exceeded' succeeds because JavaScript string comparison "10" > 1000 is false (string-to-number coercion in JS comparison operators makes this unreliable as a bypass in many cases, but edge cases exist with === strict equality checks, typeof guards, or downstream systems that handle strings differently from integers).- Application must make a security-relevant decision based on the type or typed comparison of
request.body - No downstream re-validation or type casting
- JavaScript's loose comparison operators often re-coerce the string back to a number, neutralizing the mismatch
- Typed languages in downstream microservices (Go, Java, Rust) will reject the wrong type at deserialization
- Most security-critical operations use parameterized queries or typed ORMs that enforce types independently
typeof request.body would reveal the mismatch. No CVE-specific scanner signatures exist as of 2026-08-19.Achieve limited C/I impact
- Successful bypass of a type-dependent security check in step 3
- Blast radius is limited to the specific business function behind the affected endpoint
- No lateral movement, no RCE, no persistence
The supporting signals.
| In-the-wild exploitation | None observed. No reports of active exploitation. Not listed in CISA KEV. |
|---|---|
| Proof of concept | Not publicly available. The GHSA advisory does not include a PoC. The concept is trivially reproducible by sending a JSON string to a root-primitive endpoint, but weaponization requires a target-specific business-logic bypass. |
| EPSS score | Not yet scored — CVE is RESERVED in NVD as of 2026-08-19; EPSS data not available. Expected to be low (<5th percentile) given the authentication requirement and narrow attack surface. |
| KEV status | Not listed. No CISA KEV entry. |
| CVSS vector | CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:L/A:N — Network-accessible but requires authentication. Low complexity, no user interaction. Limited confidentiality and integrity impact, no availability impact, unchanged scope. |
| Affected versions | fastify < 5.12.1 (npm). Only versions with Ajv type coercion enabled and root-level primitive body schemas are exploitable. |
| Fixed version | fastify 5.12.1 (released 2026-08-18) |
| Exposure data | Fastify has ~9M weekly downloads on npm. However, the subset of applications using root-level primitive body schemas is estimated to be very small — likely <5% of deployments. |
| Disclosure date | 2026-08-18 (coordinated disclosure via GitHub Security Advisory) |
| Reporter / credits | Reported by velgusgus599, fix by mcollina, reviewed by UlisesGascon |
noisgate verdict.
The single most decisive factor is the extremely narrow attack surface: only root-level primitive body schemas (not objects or arrays) are affected, which represents a small fraction of real-world Fastify route definitions. Combined with the authentication requirement and the absence of any code-execution or privilege-escalation path, the blast radius even on high-value deployments remains limited to business-logic-level type confusion on individual endpoints.
Why this verdict
- Narrow schema surface: Only root-level primitive body schemas are affected. The overwhelming majority of Fastify APIs use object schemas (
{ type: 'object' }), which coerce members in-place and are completely unaffected. This drastically limits the reachable population. - Authentication required (PR:L): The attacker must hold valid credentials or an API key. This is not an unauthenticated drive-by — it requires a prior relationship with the application.
- No escalation path: Impact caps at C:L/I:L/A:N with unchanged scope. There is no route to RCE, SSRF, privilege escalation, or system compromise. The worst case is a business-logic bypass on a single endpoint.
- Role multiplier: Fastify is a general-purpose Node.js web framework. In a *typical role* (API backend, microservice), exploitation yields a minor business-logic bypass. In a *high-value role* (API gateway, auth service, CI/CD frontend), the type confusion could theoretically affect an authorization decision — but the chain still requires a root-primitive schema AND a type-dependent security check, which is architecturally uncommon in identity or gateway services. The blast radius even in high-value roles does not reach domain/fleet/supply-chain scale. No floor override triggered.
- JavaScript coercion semantics partially self-heal: Many downstream comparison operators in JavaScript re-coerce strings to numbers, reducing the practical impact of the type mismatch in common code patterns.
Why not higher?
There is no path to remote code execution, privilege escalation, or denial of service. The scope is unchanged (S:U), and impact is capped at low confidentiality and low integrity. The authentication requirement removes the unauthenticated-mass-exploitation scenario. Even in high-value Fastify deployments (API gateways, auth services), the specific preconditions — root primitive schema + type-dependent security decision — are architecturally uncommon enough that the blast radius does not reach fleet or domain scale.
Why not lower?
The vulnerability is real, trivially triggerable once preconditions are met, and affects a framework with ~9M weekly npm downloads. Type coercion is enabled by default in Fastify, so the misconfiguration is opt-out rather than opt-in. While the affected surface is narrow, a developer who does use a root primitive schema for a security-critical endpoint (e.g., a financial amount or a permission level) would have a false sense of safety from validation that is silently broken.
What to do — in priority order.
- Wrap root primitives in object schemas — Change
schema: { body: { type: 'integer' } }toschema: { body: { type: 'object', properties: { value: { type: 'integer' } } } }. Object schemas coerce members in-place and are not affected by this bug. This is a code change, not an infrastructure control — prioritize it within your 365-day noisgate remediation SLA for MEDIUM findings. - Add explicit type checks in security-critical handlers — Insert
if (typeof request.body !== 'number') return reply.code(400).send(...)at the top of any handler that depends on a root primitive type for a security decision. This neutralizes the mismatch regardless of Fastify version. - Disable Ajv type coercion for body schemas — Configure the Ajv validator compiler with
coerceTypes: falsefor body validation. This eliminates the coercion mismatch entirely but may break existing routes that rely on coercion. Test thoroughly before deploying. - Upgrade to fastify ≥ 5.12.1 — The definitive fix. The patched version ensures
request.bodyreceives the coerced value, matching what was validated. Schedule within your standard dependency update cycle.
- WAF rules — This is not a payload-signature attack. The malicious input is a valid JSON string (
"10") that differs from the expected type. No WAF can distinguish this from legitimate usage without application-level schema awareness. - Rate limiting — The attack does not involve volume; a single request can trigger the bypass.
- Content-Type header validation — Unlike CVE-2026-33806 and CVE-2026-25223, this vulnerability does not involve Content-Type manipulation. Hardening Content-Type parsing does not help.
Crowdsourced verification payload.
Run on the target host or any machine with node and npm available. Execute: bash check_cve_2026_18504.sh /path/to/your/project. No special privileges required — it reads package.json and node_modules.
#!/usr/bin/env bash
# check_cve_2026_18504.sh — Detect fastify < 5.12.1 (CVE-2026-18504)
# Usage: bash check_cve_2026_18504.sh [/path/to/project]
# Exit codes: 0=PATCHED, 1=VULNERABLE, 2=UNKNOWN
set -euo pipefail
PROJECT_DIR="${1:-.}"
if ! command -v node &>/dev/null; then
echo "UNKNOWN — node not found in PATH"
exit 2
fi
# Try npm list first (handles hoisted deps)
if command -v npm &>/dev/null; then
FASTIFY_VER=$(cd "$PROJECT_DIR" && npm ls fastify --json 2>/dev/null | node -e "
const j=require('fs').readFileSync('/dev/stdin','utf8');
try { const d=JSON.parse(j); const v=d.dependencies?.fastify?.version; if(v) console.log(v); else process.exit(1); } catch(e){ process.exit(1); }
" 2>/dev/null) || true
fi
# Fallback: read from node_modules
if [ -z "${FASTIFY_VER:-}" ]; then
PKG="$PROJECT_DIR/node_modules/fastify/package.json"
if [ -f "$PKG" ]; then
FASTIFY_VER=$(node -e "console.log(require('$PKG').version)" 2>/dev/null) || true
fi
fi
if [ -z "${FASTIFY_VER:-}" ]; then
echo "UNKNOWN — fastify not found in $PROJECT_DIR"
exit 2
fi
echo "Detected fastify version: $FASTIFY_VER"
# Compare versions: vulnerable if < 5.12.1
RESULT=$(node -e "
const semver = (v) => v.split('.').map(Number);
const [ma,mi,pa] = semver('$FASTIFY_VER');
const [fa,fi,fp] = semver('5.12.1');
if (ma < 5) { console.log('PATCHED'); process.exit(0); } // v4.x not affected by this specific advisory
if (ma > fa || (ma===fa && mi > fi) || (ma===fa && mi===fi && pa >= fp)) {
console.log('PATCHED');
} else {
console.log('VULNERABLE');
}
")
echo "$RESULT"
if [ "$RESULT" = "VULNERABLE" ]; then exit 1; fi
if [ "$RESULT" = "PATCHED" ]; then exit 0; fi
exit 2If you remember one thing.
[email protected] to your next scheduled dependency update cycle. Before that update lands, audit your codebase for any route using a root-level primitive body schema (e.g., { type: 'integer' } or { type: 'string' } at the body root) — if you find none, you are not exploitable regardless of version. If you do find affected routes, either wrap the primitive in an object schema or add an explicit typeof guard in the handler as an interim fix. There is no active exploitation and no PoC in the wild, so this does not warrant emergency patching.Sources
What defenders are saying.
Crowdsourced verification outputs.
Results submitted by users who ran the verification payload against their environment.