Proof/CVEs & advisories/CVE-2026-77356
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.
formatCheck()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:
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:
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.
Schema string
A less-trusted value reaches a string-valued schema property.
Direct interpolation
The value is concatenated into generated JavaScript.
Marker unchanged
Building the validator alone does not move the marker.
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.
The public trigger was run against representative affected releases and against every listed fixed release.
Compilation only
Building the validator leaves the marker untouched, ruling out effects at schema-construction time.
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.
Eleven releases
The same trigger produced no state change on any of the eleven listed fixed releases.
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.
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.
Source mapping
Enumerate every site where a schema value is concatenated into generated JavaScript.
Variant hypothesis
Assume the first fix is incomplete until each remaining emitter is accounted for.
Release comparison
Run the public trigger against last-affected and fixed artifacts on all eleven maintained lines.
Skeptic gate
Separate compile-time from invoke-time with a marker; refuse to claim a runtime result where only source evidence exists.
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:
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.
CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:HThe 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.
| Line | Affected | Fixed release |
|---|---|---|
| Public advisory range | ≥ 0.24.0 and ≤ 0.34.51 | see per-line releases |
| 0.24.x | through 0.24.51 | 0.24.52 |
| 0.25.x | through 0.25.24 | 0.25.25 |
| 0.26.x | through 0.26.8 | 0.26.9 |
| 0.27.x | through 0.27.11 | 0.27.12 |
| 0.28.x | through 0.28.21 | 0.28.22 |
| 0.29.x | through 0.29.7 | 0.29.8 |
| 0.30.x | through 0.30.5 | 0.30.6 |
| 0.31.x | through 0.31.29 | 0.31.30 |
| 0.32.x | through 0.32.36 | 0.32.37 |
| 0.33.x | through 0.33.23 | 0.33.24 |
| 0.34.x | through 0.34.51 | 0.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.
- Upgrade to the fixed release for the maintained 0.x line, or migrate to TypeBox 1.x where compatible.
- Treat every data-to-source conversion as one centralized, auditable boundary.
- Never wrap a partially escaped value in another layer of hand-written quoting.
- Cover backslashes, quotes, line terminators, literal strings, member keys, format names, custom kind names and regex-related keys in regression tests.
- Assert more than the validator’s boolean result — assert that an attacker-controlled marker stays absent before and after both compilation and invocation.
- GHSA-976x-prgx-qv35 — TypeBox security advisory and public PoC
- CVE-2026-77356 record
- 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.