Coordinated disclosureCVE-2026-77356 · GHSA-976x-prgx-qv35TypeBox · npm7.8 High

Proof/CVEs & advisories/CVE-2026-77356

CVE.07 — Published record

The schema string became source code.

TypeBox’s 0.x compiler turns schemas into JavaScript and evaluates the result. Any schema string it concatenated into that source stopped being data. The first patch hardened one emitter; a second one was still interpolating directly.

CVECVE-2026-77356
AdvisoryGHSA-976x-prgx-qv35
Package@sinclair/typebox
Ecosystemnpm · Node.js
Affected≥0.24.0 ≤0.34.51
Fixed11 line releases
Severity7.8 High
ReporterCharles Vosburgh
Schema data reached a JavaScript source position. Escaping one character at one emitter is not a code/data boundary.
Weakness
CWE-94
Trigger
schema format
Control
marker unchanged at compile
Outcome
execution on Check()
Public-safety boundary.The proof of concept for this issue is already public in GHSA-976x-prgx-qv35. This page explains the failed invariant, the validation method and the remediation boundary; it does not republish a reusable payload generator.

TypeBox 0.x compiles schema objects into JavaScript source and evaluates that source through the Function constructor. That design makes source emission security-critical: a value that originates in a schema has to be serialized as a JavaScript constant, never concatenated into source text.

Exploitation is not automatic. It requires a downstream application to let a less-trusted value become part of a schema that is then JIT-compiled. Applications that build static developer-authored schemas do not expose that prerequisite.

Every schema-derived string inserted into generated JavaScript must be emitted as a complete data literal by a centralized, context-correct serializer.

The bug was not that TypeBox generates code. Generating code is the point of TypeCompiler. The bug was that the conversion from schema data to source text happened in several independent places, each with its own hand-written quoting.

An early path handled literal strings by escaping single quotes and then wrapping the result in hand-written quotes:

Vulnerable emitterLiteralString
namespace LiteralString {
  export function Escape(content: string) {
    return content.replace(/'/g, "\\'")
  }
}

yield `(${value} === '${LiteralString.Escape(schema.const)}')`

Escaping a single character is insufficient for a JavaScript source context. A backslash combined with a quote changes how the generated literal parses.

That path was hardened first. Follow-on testing then found a second source-generation path that had never been escaped at all — the schema format property:

Second interpolation siteformat path
yield `format('${schema.format}', ${value})`

A crafted format string could terminate the intended literal and inject statements into the function body that TypeBox was about to compile.

The published proof separates two moments that are easy to conflate. Compilation builds the validator. Invocation runs it. A benign process-global marker distinguishes them.

01 · Input

Schema string

A less-trusted value reaches a string-valued schema property.

02 · Emit

Direct interpolation

The value is concatenated into generated JavaScript.

03 · Compile

Marker unchanged

Building the validator alone does not move the marker.

04 · Invoke

Marker changes

Check() runs the injected statement in-process.

Locating the effect inside the generated function — rather than in schema construction or module loading — is what makes the claim precise.

CONTROL marker state before compile: UNCHANGED COMPILE TypeCompiler.Compile(schema) — marker still unchanged INVOKE compiled Check() executes generated source ATTACKER-CONTROLLED statement runs in the Node.js process FIXED RELEASE same trigger, marker: UNCHANGED MARKER=TYPEBOX_CODEGEN_INJECTION

The public trigger was run against representative affected releases and against every listed fixed release.

Negative control

Compilation only

Building the validator leaves the marker untouched, ruling out effects at schema-construction time.

Vulnerable path

Format-name trigger

Reproduced on sampled affected releases including 0.24.51, 0.25.24, 0.26.8, 0.31.29, 0.32.36, 0.33.23, 0.34.50 and 0.34.51.

Fixed control

Eleven releases

The same trigger produced no state change on any of the eleven listed fixed releases.

What the format trigger does not prove. On sampled 0.27.x–0.30.x releases the format PoC stayed negative while public source and fixing diffs showed a separate generated-regexp interpolation path. Those are recorded as source-verified variants, not as runtime results.

The compiler had multiple independent string emitters, each responsible for escaping values before inserting them into generated JavaScript. A distributed invariant is an invariant that gets fixed incompletely — hardening one emitter did nothing for the other interpolation site.

Patch-by-patch escaping at individual emitters invites follow-on variants. One auditable serializer does not.
The SecHive research loop

One emitter was fixed. The search kept going.

The first patch closed the literal-string path. Stopping there would have shipped a partial fix, so the next stage of the loop went looking for every other place a schema value reached generated source.

SK.01

Source mapping

Enumerate every site where a schema value is concatenated into generated JavaScript.

SK.02

Variant hypothesis

Assume the first fix is incomplete until each remaining emitter is accounted for.

SK.03

Release comparison

Run the public trigger against last-affected and fixed artifacts on all eleven maintained lines.

SK.04

Skeptic gate

Separate compile-time from invoke-time with a marker; refuse to claim a runtime result where only source evidence exists.

HUMAN

Promotion and disclosure

Review version ranges, wording, severity and the public-safety boundary before reporting.

The final fix centralizes string emission through a helper that canonicalizes the value with JSON.stringify and returns a complete JavaScript string constant:

Centralized serializerStringConstant
function StringConstant(value: string): string {
  if (!IsString(value)) throw Error('ConstantString: Not a String')
  const canonical = JSON.stringify(value).slice(1, -1)
  const escaped = canonical.replace(/'/g, "\\'")
  return `'${escaped}'`
}

yield `format(${StringConstant(schema.format)}, ${value})`

The same design was applied across the other string-bearing compiler paths — literals, member keys, required keys, format names, known-key arrays and custom kind names. Older maintained branches also received fixes for direct regular-expression source interpolation.

The demonstrated primitive is arbitrary JavaScript execution inside the Node.js process that compiles and invokes the attacker-influenced schema. Confidentiality, integrity and availability impact are bounded by the privileges and reachable resources of that process.

High
Published advisory severity
7.8
CVSS 3.1
CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H

The local vector reflects the real prerequisite. TypeBox schemas are usually developer-authored, and a TypeScript application does not automatically expose schema construction to remote users. A remote exploit depends on a downstream application taking less-trusted data and using it to build a schema that is subsequently compiled.

LineAffectedFixed release
Public advisory range≥ 0.24.0 and ≤ 0.34.51see per-line releases
0.24.xthrough 0.24.510.24.52
0.25.xthrough 0.25.240.25.25
0.26.xthrough 0.26.80.26.9
0.27.xthrough 0.27.110.27.12
0.28.xthrough 0.28.210.28.22
0.29.xthrough 0.29.70.29.8
0.30.xthrough 0.30.50.30.6
0.31.xthrough 0.31.290.31.30
0.32.xthrough 0.32.360.32.37
0.33.xthrough 0.33.230.33.24
0.34.xthrough 0.34.510.34.52

The npm artifacts for the last affected release on each maintained line and for all eleven fixed releases were inspected. No distinct introducing commit was independently established for the entire advisory range; 0.24.0 was inspected as the earliest release in the public interval, which is not the same as proving one commit introduced every affected code path.

  1. Upgrade to the fixed release for the maintained 0.x line, or migrate to TypeBox 1.x where compatible.
  2. Treat every data-to-source conversion as one centralized, auditable boundary.
  3. Never wrap a partially escaped value in another layer of hand-written quoting.
  4. Cover backslashes, quotes, line terminators, literal strings, member keys, format names, custom kind names and regex-related keys in regression tests.
  5. Assert more than the validator’s boolean result — assert that an attacker-controlled marker stays absent before and after both compilation and invocation.
2026-07-11
Initial 0.x patch series began landing.
2026-07-16
Follow-on hardening completed across maintained 0.x lines.
2026-08-21
GHSA-976x-prgx-qv35 and CVE-2026-77356 published with the public PoC.
2026-08-26
Affected/fixed release matrix and public paths re-reviewed.
  1. GHSA-976x-prgx-qv35 — TypeBox security advisory and public PoC
  2. CVE-2026-77356 record
  3. TypeBox repository and release history

Credit: Discovered and reported by Charles Vosburgh. Research was AI-assisted through source mapping, hypothesis generation, release comparison and evidence organization; final validation, disclosure coordination, severity calibration and publication review remained human.