Parsers and codecs

The parsers and codecs that turn untrusted bytes into structure, decompression (deflate, Brotli), tar extraction, text and base encodings, IDNA hostnames, JSON, and the Parsimony JavaScript parser, are all Cruft's own memory-safe Rust. This page states which size limits and traversal defenses each one enforces and which are only present.

A runtime turns untrusted bytes into structure constantly: decompressing a response body, unpacking a package tarball, decoding text in some character set, parsing JSON, parsing JavaScript itself. Node sources those parsers from several places, V8 for JSON, C libraries for zlib and Brotli, ICU for hostname canonicalization, an npm package for tar. Cruft's are one story: every parser and codec in this tier is Cruft's own Rust, memory-safe by construction and sharing one bounds-checking and bomb-cap posture. The tradeoff is maturity. This tier is stronger on correctness and memory safety than on adversarial input, and the sections below are exact about which limits the code enforces and which are only present.

Each of these is a place where hostile input meets Cruft's parsing code, and each is a classic vulnerability surface: decompression bombs, path traversal, buffer overruns, denial of service.

Alpha (0.0.10). These crates are much better at correctness than at adversarial input. Several resource limits and bounds exist in the code with no test or fuzzer behind them yet. Do not make any crate here your only defense on an adversarial-input path.
Compression and archives
deflate (DEFLATE/gzip/zlib)brotli (Brotli)tarcompression (Compression/DecompressionStream)
Text and encoding
textencoder (TextEncoder/TextDecoder)basen (base64/32/16)percent-encodingidnapunycode
Structure
json-manifest (RFC 8259 JSON)Parsimony (JavaScript and TypeScript)

Compression and archives

Four crates decompress or unpack untrusted bytes: DEFLATE/gzip/zlib, Brotli, tar, and the CompressionStream/DecompressionStream surface over them. The dangerous input to a decompressor is the small one, a few kilobytes of valid stream whose output is gigabytes, so this family is judged as much on what it refuses as on what it decodes. None of the four carries unsafe, and none panics on malformed input.

The DEFLATE decoder validates back-references (a copy distance pointing before the start of output is rejected, not read out of bounds), bounds-checks the Huffman and length/distance decoders, and never pre-allocates from an attacker-declared length. Its output is checked against a 256 MiB ceiling on every growth, so a small gzip input cannot expand without bound. The deflate and gzip page covers the encoder and the full defense set.

Brotli carries the same ceiling. Decoded output is checked against a 256 MiB MAX_OUTPUT accumulated across all meta-blocks, not per block, returning an OutputTooLarge error rather than expanding without bound. Decode routes through decode_with_limit(data, MAX_OUTPUT), so the Content-Encoding: br handler and the DecompressionStream Brotli route both inherit the cap.

The tar path on the npm installer is traversal-safe: the extractor rejects absolute paths, .. components, and symlink and hardlink entries before writing anything, with tests for each. The zip-slip and tar-slip class, and symlink escape, are blocked on the install path.

Two structural weaknesses remain, and both are in Limitations below: the decompression caps are coarse and mostly untested, and the tar traversal defense lives in the installer rather than in the tar crate, so a second consumer that joins entry paths inherits the raw, unsanitized names.

Text and encoding

Five small crates decode text and encodings: TextEncoder/TextDecoder, base64/base32/base16, percent-encoding, idna (the host-name canonicalizer), and punycode. Two of them, idna and punycode, are security-critical because the URL parser uses them to canonicalize hostnames, so a bug there is a host-confusion or SSRF issue. None of the five carries unsafe, and none panics on malformed input reachable from JavaScript.

  • TextEncoder/TextDecoder produces U+FFFD, or a fatal error in fatal mode, on invalid UTF-8 rather than crashing.
  • The base codecs are strict: bad length, bad characters, bad padding, and non-zero trailing bits are all rejected.
  • percent-encoding has a strict mode that errors on malformed escapes and a lenient pass-through mode.
  • punycode's classic RFC 3492 decode integer-overflow is guarded with checked arithmetic at every accumulation step, so the crafted-overflow bug that has bitten many punycode implementations returns a clean error here.
  • idna is real UTS-46, backed by a complete Unicode 17.0.0 mapping table that rejects disallowed code points.

idna's mapping is strong; its validity layer is a subset, and that is where the risk sits. The bidirectional-text rule defaults any RTL script it does not recognize to neutral and then skips the check; the zero-width joiner and non-joiner are accepted with no contextual rule; and there is no hyphen-position check, no leading-combining-mark check, and no label-length limit, while normalization only composes Latin sequences. A full IDNA2008 validator enforces all of these. The consequence is concrete: some mixed-script and joiner-abuse hostnames that a complete validator rejects are accepted and canonicalized here. The host-confusion gap is not in how names are mapped, it is in which names are allowed through. Until the validity layer is completed and checked against a confusable corpus, treat idna as canonicalizing correctly but filtering incompletely.

JSON and JavaScript

Two crates turn text into structure: a strict RFC 8259 JSON parser used on the package-install path, and Parsimony, the JavaScript and TypeScript parser that parses all user and npm code, the single largest untrusted-input surface in the runtime. Neither carries unsafe, and both cap recursion depth.

The JSON parser bounds-checks every byte access, returns an error on every malformed input, handles UTF-8 and single \u escapes without panicking, and is strict: no JSON5, comments, or trailing commas. Parsimony is well tested for grammar and early errors, reads its token stream through an EOF sentinel so reading past the end cannot panic, and delegates identifier classification to the Unicode identifier tables.

Both cap nesting depth, which is the load-bearing security property here: a recursive-descent parser overflows the native stack on deeply nested input, an uncatchable abort. Parsimony no longer overflows on ((((...)))), [[[[...]]]], or nested blocks; feeding it millions of levels of each returns a catchable SyntaxError and the process stays up. The JSON manifest parser carries a MAX_DEPTH of 256, and enter_nested() returns an error past the bound rather than recursing one native frame per level, a guard that is backed by tests. A hostile package.json or lockfile can no longer crash the install by depth alone. At the runtime layer, JSON.parse of a 200k-deep array likewise raises a catchable SyntaxError.

One correctness bug remains in the JSON string scanner: surrogate-pair \u escapes decode to an empty string. A single \uXXXX escape works, but a paired escape, the two \uXXXX halves of an astral code point that should decode to a length-2 string such as U+1F600, silently yields "". This is a correctness divergence, not a crash or a memory-safety issue: valid JSON containing astral-plane characters written as surrogate pairs is mis-decoded rather than rejected.

Limitations

  • The decompression caps are coarse and mostly untested. The DEFLATE and Brotli ceilings run on every decode, and Brotli's OutputTooLarge path is exercised in-crate, but no test drives the DEFLATE cap or its back-reference check. The DEFLATE side has no ratio or caller-configurable limit and a stream adapter that buffers all input whole before decoding, so 256 MiB per call is a lot of memory to concede in a per-request server.
  • The tar traversal defense lives in the installer, not the crate. The tar crate returns entry paths verbatim, .., absolute, and link entries included; only the installer sanitizes them. A second consumer already joins unsanitized paths, so the next consumer inherits a footgun. The defense belongs in the tar crate.
  • No bomb, bad-back-reference, or fuzz tests on compression. The correctness vectors are good; the bomb caps and the back-reference guard are covered by reading, not by a test or a fuzzer.
  • idna filters incompletely. The validity layer is a subset of IDNA2008, so some mixed-script and joiner-abuse hostnames pass canonicalization. The mapping is correct; the filtering is not yet complete.
  • Small codec residue. A few latent, currently-unreachable expect() calls in the codecs; punycode's overflow guard is correct but has no test; a minor streamed-BOM bug in the text decoder; and no fuzzing across the encoding crates.
  • The parsers have no input-size limit, and Parsimony's regex-literal scanning contains one str::from_utf8().unwrap() that is not proven safe against adversarial multibyte Unicode. There is no fuzz target for either parser, which is the single highest-value fuzz surface in the runtime.