Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,5 +34,8 @@ jobs:
- name: Test
run: npm test

- name: Fuzz smoke
run: npm run fuzz:smoke

- name: Build
run: npm run build
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ All notable changes will be documented here. The project intends to follow seman

### Added

- Reproducible fast-check properties for arbitrary-byte inspection, parser mutations, subview isolation, limits, cleaners, and fail-closed verification.
- Finite public-API fuzz harness with explicit seed, iteration, input-size, target, and counterexample-path replay controls.
- Deterministic 250-iteration fuzz smoke coverage in CI and documented regression-promotion workflow.

- Deterministic malformed-input corpus for generic bytes, JPEG, WebP, PNG, and shared TIFF corruption families.
- Cross-format invariants for deterministic inspection and cleaning, native-exception containment, fail-closed operations, input immutability, and cheap limit stress.
- Testing and fuzz-readiness guidance with future property and fuzz targets; random fuzzing remains outside normal CI.
Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ GPS rational components remain exact numerator/denominator pairs; decimal coordi

`verifyMetadata` supports `absent`, `present`, or `ignore` expectations. Concepts not implemented for a format produce no check rather than implying an exhaustive search. Verification fails closed if metadata reporting reaches its configured entry limit. Single-file verification observes supported container presence or absence and cannot prove provenance or pixel privacy.

## Generated testing

The deterministic corpus is supplemented by fixed-seed property tests and a finite public-API fuzz harness. CI runs only the bounded smoke profile; extended local runs remain explicitly iteration- and input-size-limited. See the [testing model](docs/testing.md) for replay and regression-promotion commands.

## Security philosophy

Every byte is untrusted. All offsets are interpreted within bounded views, traversal is iterative and limited, and malformed structures fail without unchecked access. PNG image data and compressed metadata are never inflated. Unknown JPEG APP segments, WebP chunks, and PNG ancillary chunks are preserved by default. See the [security model](docs/security-model.md), [architecture](docs/architecture.md), [testing model](docs/testing.md), and [cleaning policy](docs/cleaning-policy.md).
Expand Down
6 changes: 3 additions & 3 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@ bounded binary primitives
→ format fixture tests
→ deterministic malformed corpus
→ cross-format invariants
future property tests
future fuzzing
reproducible property tests
finite public-API fuzz harness
```

Production parsers remain internal. Public operations are the preferred cross-format targets; the shared TIFF parser is directly callable only from test code. See [testing and fuzz readiness](testing.md).
Production parsers remain internal. Public operations are the fuzz targets; JPEG, WebP, PNG, and shared TIFF internals are imported only by property tests. Generated-test code and `fast-check` remain outside `src`, `dist`, and the npm payload. See [testing and fuzz readiness](testing.md).
2 changes: 1 addition & 1 deletion docs/security-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,4 @@ Core production code has zero runtime dependencies and no network, analytics, te

Malformed input is part of the expected threat model. A small deterministic corpus covers representative generic, JPEG, WebP, PNG, and TIFF structural corruption families, including truncation, corrupt lengths and offsets, cycles, and configured work limits. Parser loops must advance or terminate, and unsafe outer container boundaries cause typed fail-closed cleaning and verification errors before output is produced.

The corpus is regression coverage, not proof of parser correctness. No decompression is implemented, so decompression bombs are outside the current attack surface and `maxDecompressedBytes` remains reserved. Reproducible property testing and dedicated fuzzing are planned future layers; random fuzzing is not part of normal CI. See [testing and fuzz readiness](testing.md).
The corpus, fixed-seed property tests, and finite public-API fuzz harness improve regression confidence but are not proof of parser correctness or security. Current protections still come from bounded readers, checked arithmetic, explicit traversal limits, deterministic parser progress, and fail-closed cleaning. No decompression is implemented, so decompression bombs are outside the current attack surface and `maxDecompressedBytes` remains reserved. Normal CI runs only a bounded deterministic fuzz smoke profile; extended local runs remain finite. See [testing and fuzz readiness](testing.md).
129 changes: 90 additions & 39 deletions docs/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,50 +2,101 @@

The test suite uses deterministic TypeScript fixture builders for JPEG markers,
WebP RIFF chunks, PNG chunks and CRCs, and TIFF IFD structures. Fixtures stay
small, readable, and cheap; binary files are used only when they would be
clearer than the builder expression.
small and reviewable; binary files are used only when clearer than builder code.

## Test layers

```text
bounded binary primitives
bounded binary unit tests
→ format fixtures and parser algorithms
→ deterministic malformed corpus
→ cross-format public API invariants
future reproducible property tests
future fuzz targets
→ cross-format invariants
→ reproducible property tests
finite fuzz harness
```

The malformed corpus covers generic byte patterns plus representative JPEG,
WebP, PNG, and shared TIFF truncation, corrupt length, invalid offset, cycle,
and configured-limit families. Corpus assertions focus on stable contracts:
format/status, relevant diagnostic codes, deterministic results, caller-input
immutability, typed fail-closed cleaning and verification, and safe removal of
bounded malformed metadata. They intentionally avoid full-report snapshots and
timing thresholds.

Security-limit tests use tiny inputs with small custom values for input,
segment, chunk, IFD entry/depth, metadata entry, string, and diagnostic limits.
`maxDecompressedBytes` remains unused because the library performs no
decompression.

## Future property and fuzz targets

Likely targets are:

- `inspectMetadata(bytes)` through the public API;
- bounded JPEG, WebP, and PNG parser entry points in test/fuzz builds;
- the bounded TIFF parser as a test-only internal target;
- `cleanMetadata(bytes, policy)` through the public API.

Strong future properties include containment of native bounds exceptions,
deterministic inspection and cleaning, re-inspectable clean output, Privacy
Clean idempotency, input immutability, removal-only output sizing, preservation
of unknown structures, and default ICC preservation. WebP is permitted to patch
the RIFF size and applicable VP8X metadata flags.

No random fuzzing runs in normal CI, and no property/fuzz dependency is
currently installed. A future sprint can add reproducible seeded property tests
or dedicated fuzz harnesses if their coverage benefit justifies the development
dependency and CI cost. The deterministic corpus is regression coverage, not a
proof of parser correctness.
The 67-case malformed corpus covers named generic, JPEG, WebP, PNG, and shared
TIFF corruption families. It remains the permanent regression foundation.
Property tests supplement it with generated variations, while the fuzz harness
repeatedly explores the public API. Generated iterations are not counted as
individual Vitest tests.

## Property tests

Property tests use the single dev-only `fast-check` dependency for bounded
arbitraries, deterministic seed/path replay, and automatic shrinking. They run
as part of `npm test` and can be invoked alone:

```text
npm test -- tests/property
```

Defaults are fixed and finite:

- parser/inspection properties: 64 runs each;
- cleaner/verification properties: 48 runs each;
- infrastructure smoke property: 16 runs;
- generated property input: at most 1,024 bytes;
- default seed: `0x5ec00009`.

`PROPERTY_SEED`, `PROPERTY_RUNS`, and `PROPERTY_PATH` override those settings.
For example, in PowerShell:

```powershell
$env:PROPERTY_SEED="1589641225"
$env:PROPERTY_PATH="0:0:1"
npm test -- tests/property
```

On failure, fast-check reports the seed, counterexample path, and shrunk input.
Use both seed and path to replay the minimized counterexample.

## Fuzz harness

The harness builds the package and exercises only the public
`inspectMetadata`, `cleanMetadata`, and `verifyMetadata` API targets.

```text
npm run fuzz:smoke
npm run fuzz -- --seed 9 --runs 10000 --max-bytes 4096 --target all
```

`fuzz:smoke` is the deterministic CI profile: seed `20260825`, 250 total
iterations, and a 512-byte input maximum. `fuzz` is the finite local profile:
seed `0x5ec00009`, 5,000 total iterations, and a 4,096-byte maximum. Supported
targets are `all`, `inspect`, `clean`, and `verify`. CLI options may also be set
with `FUZZ_SEED`, `FUZZ_RUNS`, `FUZZ_MAX_BYTES`, `FUZZ_TARGET`, and `FUZZ_PATH`.
The maximum permitted generated input is 4,096 bytes.

A failure prints its selected target, configured target, seed, run count,
counterexample path, shrink count, and bounded hexadecimal input. It also prints
an exact replay command. Runs are iteration-bounded, never elapsed-time or
infinite campaigns.

## Regression promotion

A generated failure is handled as follows:

```text
replay seed and path
→ understand the root cause
→ minimize with built-in shrinking
→ fix the production defect
→ promote the smallest meaningful input to a named corpus/regression test
→ rerun property and fuzz coverage
```

A fixed seed alone is not a permanent regression test. The minimal semantic case
must be persisted so later generator changes cannot hide it.

## Security limits and scope

Security tests use tiny custom input, segment, chunk, IFD entry/depth, metadata
entry, string, and diagnostic limits. `maxDecompressedBytes` remains unused
because the library performs no decompression. JPEG scan data, WebP image chunks,
and PNG IDAT remain opaque; generated testing does not add codecs or decoding.

Property and fuzz testing improve regression confidence but do not prove parser
correctness or security. The primary protections remain bounded readers, checked
arithmetic, explicit traversal limits, deterministic parser progress, and
fail-closed cleaning.
41 changes: 41 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
"lint": "eslint .",
"test": "vitest run",
"test:watch": "vitest",
"fuzz:smoke": "npm run build --silent && node scripts/fuzz.mjs --seed 20260825 --runs 250 --max-bytes 512",
"fuzz": "npm run build --silent && node scripts/fuzz.mjs",
"typecheck": "tsc --noEmit"
},
"engines": {
Expand All @@ -35,6 +37,7 @@
"@eslint/js": "^10.0.1",
"@types/node": "^24.13.3",
"eslint": "^10.9.0",
"fast-check": "^4.9.0",
"prettier": "^3.9.6",
"tsup": "^8.5.1",
"typescript": "5.9.3",
Expand Down
Loading
Loading