← Back to Feed CACHED · 2026-10-07 20:28:52 · CACHE_KEY CVE-2026-18504
CVE-2026-18504 · CWE-20 · Disclosed 2026-08-18

Fastify Schema Validation Bypass via Root Primitive Coercion Mismatch

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

The bouncer checks your fake ID, says it's legit, then hands the doorman the original fake anyway

CVE-2026-18504 is a schema validation bypass in Fastify < 5.12.1 that occurs only when a route defines a root-level primitive body schema (e.g., a bare {type:"integer",minimum:1,maximum:10}) with default Ajv type coercion enabled. In that narrow scenario, Ajv coerces a JSON string like "10" to the integer 10 for validation purposes and the schema check passes, but request.body retains the original string "10". The handler therefore receives a value whose *type* was never validated, even though the *coerced equivalent* passed. Object and array body schemas — which cover the vast majority of real Fastify routes — are unaffected because Ajv mutates their members in place. Affected versions span all Fastify releases prior to 5.12.1; the fix landed in commit 1ef8a60 by mcollina.

The vendor's MEDIUM 5.4 rating uses AC:L (low attack complexity), which overstates reality. Triggering this bug requires a route that (a) accepts a bare primitive body, (b) relies on Ajv coercion rather than explicit parsing, and (c) makes a downstream security decision — access-control, payment bound, rate limit — that is sensitive to the type of the value. Each of those conditions is independently uncommon; together they describe a niche coding pattern. The PR:L (authenticated) prerequisite already limits the attacker population, and impact is capped at Low confidentiality + Low integrity with no availability effect. No exploitation has been observed in the wild, no PoC exists in public, and EPSS sits at the 0.31 percentile. The vendor severity is defensible on paper but generous in practice.

"Niche type-coercion mismatch in rare Fastify pattern; vendor MEDIUM is generous"
02 · The Attack Path

3 steps from start to impact.

STEP 01

Identify a Fastify route with a root-level primitive schema

The attacker needs to discover a route whose JSON schema defines the body as a bare primitive (integer, number, string, boolean) rather than an object or array. This pattern is uncommon — Fastify's own documentation defaults to object schemas, and most real-world APIs exchange JSON objects. The attacker would probe routes for unusual body validation errors or read leaked OpenAPI specs.
Conditions required:
  • Target runs Fastify < 5.12.1
  • Route uses a root-level primitive body schema
  • Default Ajv type coercion is enabled (it is by default)
Where this breaks in practice:
  • Root-level primitive schemas are a rare anti-pattern; most Fastify routes accept objects
  • Attacker must discover the specific schema shape — blind probing yields generic 400s
STEP 02

Authenticate and send a type-mismatched body

The attacker authenticates (PR:L) and sends a JSON body whose string representation coerces to the correct primitive — e.g., "10" for an integer schema with minimum:1, maximum:10. Ajv coerces "10" → 10, validates it, and returns success, but request.body stays as the string "10". No special tooling is required; a single curl call suffices.
Conditions required:
  • Valid authentication credentials for the target application
  • Knowledge of the expected primitive type and constraints
Where this breaks in practice:
  • Requires an authenticated session — unauthenticated attackers are blocked
  • The string value still represents the same *semantic* number; the difference is type, not magnitude
Detection/coverage: WAF rules that log type mismatches between Content-Type and body shape could flag this, but no mainstream WAF signatures exist for this pattern.
STEP 03

Exploit the type confusion in handler logic

The handler receives a string where it expected an integer. If the handler performs a type-sensitive operation — e.g., a strict equality check (===), a typeof branch, or passes the value to a downstream API that treats strings differently from numbers — the attacker may bypass a business-logic constraint. The actual impact is entirely application-dependent: in most handlers, the string "10" and integer 10 behave identically in JavaScript's loosely-typed runtime.
Conditions required:
  • The route handler makes a security-relevant decision based on the value's JavaScript type
  • That decision leads to unauthorized data disclosure or state change
Where this breaks in practice:
  • JavaScript's loose typing means "10" == 10 is true; only strict checks (===) or typeof guards are affected
  • Most handlers call .toString(), store to a database, or forward as JSON — all of which are type-agnostic
  • The impact is bounded by the handler's own privilege scope, not by the framework
Detection/coverage: Application-level logging of unexpected types in validated bodies could catch this, but no standard detection exists.
03 · Compensating Control

1
LOW 3.8→IGNORE 0.0
SEVERITY REDUCED
Upgrade Fastify to ≥ 5.12.1 — The definitive fix. The commit 1ef8a60 passes parentData and parentDataProperty to Ajv so coerced root primitives are written back to request.body. As a LOW verdict, the noisgate remediation SLA allows up to 365 days, but the fix is a minor semver bump with no breaking changes — deploy it in your next dependency update cycle.
2
LOW 3.8→IGNORE 0.0
SEVERITY REDUCED
Wrap root primitive schemas in an object schema — Change {type:"integer",minimum:1,maximum:10} to {type:"object",properties:{value:{type:"integer",minimum:1,maximum:10}},required:["value"]} and read request.body.value. Object properties are coerced in place, so the mismatch never occurs. This is the vendor-recommended workaround and eliminates the vulnerability without upgrading.
3
LOW 3.8→IGNORE 0.0
SEVERITY REDUCED
Add explicit type checks in route handlers — After schema validation, add a guard like if (typeof request.body !== 'number') return reply.code(400).send({error:'type mismatch'}). This catches the uncoerced value before any security-relevant logic runs. A tactical fix for teams that cannot change schemas or upgrade immediately.
What doesn't work
  • WAF body-schema validation — WAFs inspect HTTP payloads but do not understand Fastify's internal Ajv coercion pipeline. A WAF would see valid JSON and pass it through; the type mismatch happens after the WAF layer.
  • Disabling Ajv coercion globally (ajv: {coerceTypes: false}) — This removes the coercion behavior entirely but will break any route that depends on automatic string-to-number conversion from query parameters, headers, or URL params. This is a high-blast-radius config change that trades one class of bugs for widespread regressions.
  • Rate limiting or IP blocking — This vulnerability is not a volume-based attack. A single well-crafted request is sufficient. Rate limits provide no meaningful protection.
04 · Intelligence Metadata

The supporting signals.

In-the-Wild ExploitationNone observed. Not listed in CISA KEV. No GreyNoise or Shodan tags. No campaign reporting from Recorded Future, Mandiant, or CrowdStrike.
Proof-of-Concept AvailabilityNo public PoC. pocindex.io returns no results. No GitHub repos named after the CVE. No ExploitDB, Nuclei template, or Metasploit module. The GHSA advisory includes a conceptual description but no weaponized exploit code.
EPSS Score0.00309 (0.31%) — bottom decile. Predicts negligible exploitation probability within 30 days.
CISA KEV StatusNot listed. No federal mandate to patch.
CVSS VectorCVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:L/A:N — Network-accessible but requires authentication. Low impact across both confidentiality and integrity. No availability impact. Scope unchanged.
Affected VersionsAll Fastify versions < 5.12.1 (npm). The Fastify 4.x line is also affected if still in use. Object and array body schemas are not affected.
Fixed VersionFastify 5.12.1 (commit 1ef8a60). Current latest is 5.12.5 as of 2026-09-16.
Installed BaseFastify has ~15M weekly npm downloads and ~5,928 dependent packages. However, the vulnerable pattern (root-level primitive schemas) is uncommon — most projects use object schemas.
Disclosure Date2026-08-18 via GitHub Security Advisory GHSA-w2qp-rph6-63g4.
Reporting Researchervelgusgus599 (reporter). Fix developed by mcollina (Matteo Collina, Fastify lead maintainer), reviewed by UlisesGascon.

Sources.

  1. GitHub Security Advisory GHSA-w2qp-rph6-63g4
  2. GitLab Advisory Database — CVE-2026-18504
  3. OSV — GHSA-w2qp-rph6-63g4
  4. Fix Commit — preserve coerced root values
  5. Fastify npm package
  6. Fastify Releases
  7. pocindex.io — PoC Search Engine
05 · The Call

Final Verdict
↓ DOWNGRADED to LOW (3.8/10)

Why this verdict

  • Root-primitive prerequisite is a severe narrowing factor. The vulnerability only fires when a route defines a root-level primitive body schema — not an object, not an array. Fastify's docs, tutorials, and ecosystem plugins all default to object schemas. Conservatively <5% of Fastify routes use bare primitives, making the reachable attack surface a small fraction of the installed base.
  • Application-specific impact caps the blast radius. Even when the type mismatch occurs, the handler receives a string whose *semantic content* matches the schema-validated value. In JavaScript's loosely-typed runtime, "10" == 10 is true. Only handlers that perform strict type checks (===, typeof) AND use that check for a security decision are exploitable. This compounds on the already-narrow primitive-schema prerequisite.
  • PR:L limits attacker population further. The CVSS vector requires Low privileges — the attacker must already hold valid credentials. Unauthenticated exploitation is not possible.
  • No exploitation evidence or tooling exists. Zero PoCs, zero KEV listing, EPSS at 0.31%, no GreyNoise/Shodan scanning activity, no campaign reporting. The bug has been public for 51 days with no weaponization.
  • Role multiplier: Fastify is a general-purpose Node.js web framework. (a) *Low-value role* — dev servers, prototypes: many installs, no security consequence. (b) *Typical role* — API microservices: the main use case; impact is app-specific and bounded by handler logic. (c) *High-value role* — an auth or payment API built on Fastify could theoretically be affected, but these services universally use object body schemas (e.g., {"username":"…","password":"…"}), making the primitive-schema prerequisite a near-zero match. Fastify is not canonically a high-value-role component (it is not an IdP, DC, hypervisor, CA, PAM, or network edge). No plausible high-value-role chain produces domain/fleet/supply-chain impact. No floor applies.

Why not higher?

Upgrading to MEDIUM would require either (a) a realistic path to meaningful impact without the compounding application-specific prerequisites, or (b) evidence of exploitation or PoC availability lowering the attacker skill bar. Neither exists. The type mismatch bug only produces security-relevant consequences in a narrow coding pattern that sits well outside Fastify's mainstream usage. The vendor's own 5.4 MEDIUM is the ceiling, not the floor.

Why not lower?

Dropping to IGNORE would understate the risk for the small population of apps that *do* use root-level primitive schemas with security-relevant type checks. The vulnerability is real, network-accessible, and low-complexity *if* the prerequisite pattern is present. Fastify's large installed base (~15M weekly downloads) means even a 1-2% hit rate could represent thousands of deployable instances. A LOW rating correctly signals 'patch in your normal cadence, don't escalate.'

06 · Verification

Crowdsourced verification payload.

Run on any host or CI runner with node and access to the project's node_modules. Execute from the project root directory: bash check_cve_2026_18504.sh. No elevated privileges required.

noisgate-verify.sh
BASHREAD-ONLYSAFE
#!/usr/bin/env bash
# check_cve_2026_18504.sh
# Checks whether the installed fastify version is vulnerable to CVE-2026-18504
# (schema validation bypass via root primitive coercion mismatch)
# Exit codes: 0 = PATCHED, 1 = VULNERABLE, 2 = UNKNOWN

set -euo pipefail

FIXED_MAJOR=5
FIXED_MINOR=12
FIXED_PATCH=1

# Try to get the installed fastify version
if command -v node >/dev/null 2>&1; then
  VERSION=$(node -e "try{console.log(require('fastify/package.json').version)}catch(e){console.log('NOT_FOUND')}" 2>/dev/null || echo "NOT_FOUND")
else
  echo "UNKNOWN — node not found on PATH"
  exit 2
fi

if [ "$VERSION" = "NOT_FOUND" ]; then
  echo "UNKNOWN — fastify not installed in this project"
  exit 2
fi

# Parse semver (strip any pre-release suffix)
CLEAN=$(echo "$VERSION" | sed 's/-.*//')
IFS='.' read -r MAJOR MINOR PATCH <<< "$CLEAN"

# Validate parsed values
if ! [[ "$MAJOR" =~ ^[0-9]+$ ]] || ! [[ "$MINOR" =~ ^[0-9]+$ ]] || ! [[ "$PATCH" =~ ^[0-9]+$ ]]; then
  echo "UNKNOWN — could not parse fastify version: $VERSION"
  exit 2
fi

# Version comparison
if [ "$MAJOR" -gt "$FIXED_MAJOR" ]; then
  echo "PATCHED — fastify $VERSION (>= $FIXED_MAJOR.$FIXED_MINOR.$FIXED_PATCH)"
  exit 0
elif [ "$MAJOR" -eq "$FIXED_MAJOR" ] && [ "$MINOR" -gt "$FIXED_MINOR" ]; then
  echo "PATCHED — fastify $VERSION (>= $FIXED_MAJOR.$FIXED_MINOR.$FIXED_PATCH)"
  exit 0
elif [ "$MAJOR" -eq "$FIXED_MAJOR" ] && [ "$MINOR" -eq "$FIXED_MINOR" ] && [ "$PATCH" -ge "$FIXED_PATCH" ]; then
  echo "PATCHED — fastify $VERSION (>= $FIXED_MAJOR.$FIXED_MINOR.$FIXED_PATCH)"
  exit 0
else
  echo "VULNERABLE — fastify $VERSION (fixed in $FIXED_MAJOR.$FIXED_MINOR.$FIXED_PATCH)"
  exit 1
fi