Hostile Input
A tagless decoder relies on the schema to interpret every byte. Bounds and length checks are therefore essential when payloads are untrusted.
What is checked
Section titled “What is checked”| Check | Behavior |
|---|---|
| Read past end of input | DecodeError with byte offset |
| Trailing bytes after a complete value | DecodeError |
| Non-canonical (overlong) varint | DecodeError |
| Varint beyond the safe integer range | DecodeError |
| Invalid UTF-8 | DecodeError — fatal, not replacing |
| Boolean byte other than 0 or 1 | DecodeError |
| Enum index past the last member | DecodeError |
| Unknown object property | EncodeError on the way in |
Input that is not a Uint8Array | DecodeError, not a raw TypeError |
__proto__ as a decoded key | handled; the decode target is null-prototype |
The suite covers every truncated prefix of a valid payload, trailing data, invalid booleans and enum indexes, invalid UTF-8, over-limit array lengths, non-canonical varints, mutable schema declarations, open-object rejection, unknown properties, and __proto__. Property-based round-trip and mutation tests are in test/fuzz.test.ts.
Hard limits are 1,000,000 collection elements and 64 MiB for strings or byte arrays. These are backstops; the input-length checks below provide the main allocation defense.
Allocation is bounded by input length, not schema shape
Section titled “Allocation is bounded by input length, not schema shape”A naive decoder can allocate far more memory than the payload size suggests. A seven-byte payload can declare an array with one million elements, and nested arrays can multiply that allocation.
Every schema carries a _minWidth, the fewest bytes one value can occupy. Before allocating an array, the decoder multiplies this width by the declared count and checks that enough input remains.
| Payload | Before | After |
|---|---|---|
| 7 bytes, ordinary nested-array schema | 16.0 MB allocated, 2,287,431:1 | 0.01 MB, rejected at byte 3 |
| Same payload, decodes/s | 809 | 227,596 |
3 bytes, array(literal) | 1,000,000 elements | refused when the schema is built |
Because _minWidth is computed during codec construction, the runtime check adds one multiplication per decoded array.
This is why arrays of zero-width elements are rejected during codec construction. Literals, empty tuples, and empty objects use no bytes, so the decoder could not verify the declared count against the payload length. A tuple may still contain them because its length comes from the schema.
Encode memory
Section titled “Encode memory”encode returns an exact-size copy instead of a view into an oversized buffer. It also releases internal buffers larger than 64 KiB. Encoding a 4.04 MiB payload now retains 4.11 MiB, down from 12.10 MiB.
What shorn does not yet claim
Section titled “What shorn does not yet claim”Listed so you can judge the risk rather than infer safety from silence:
- No coverage-guided fuzzing. Property-based and mutation tests exist; a coverage-guided fuzzer does not.
- No depth limit. A schema nested about 5,900 levels deep can exhaust the JavaScript stack and throw
RangeErrorinstead ofDecodeError. This requires a hostile schema, not merely hostile bytes. Limit depth if schemas come from untrusted input. - No peak-allocation or allocation-rate profiling. Retained memory is measured; transient churn is not.
- No validation-failure throughput or adversarial compression tests.
- No browser-runtime matrix. Bundling is measured; execution is not.
Practical guidance
Section titled “Practical guidance”Use safeDecode at untrusted boundaries, where malformed input should be handled as normal traffic.
const result = safeDecode(Person, bytes);if (!result.success) return new Response("Bad request", { status: 400 });Use fingerprinted for stored and queued payloads. It is not a security feature because the digest is unkeyed and forgeable. It prevents schema mismatches from silently producing incorrect data.
Encrypt when secrecy matters. Compact is not confidential.
Cap payload size at the transport. The 64 MiB limit is a backstop, not a policy.
Do not treat the fingerprint as authentication. Sign or encrypt if you need authenticity.