SIGN IN SIGN UP

feat(security): fuzz the parsers that sit on a trust boundary (#19)

* feat(security): fuzz the parsers that sit on a trust boundary

Closes the Scorecard Fuzzing gap (#62), and does it on the code that
actually reads bytes we didn't write.

Four Jazzer.js targets under fuzz/, each asserting a property rather than
merely "doesn't throw":

  fuzz-storage-key   validateStorageKey is the path-traversal guard for
                     every on-disk corpus key. The oracle is acceptance, not
                     rejection: if it returns, the key must not be absolute,
                     Windows-rooted, or contain an empty / "." / ".." segment
                     or a backslash or NUL. Rejecting is always fine.
  fuzz-tar-line      parseTarVerboseLine reads `tar -tv` output for a
                     just-downloaded archive, and the validator keys its
                     containment checks off the type it returns — so a `d`
                     or `l` line coming back as anything else would skip the
                     strict path rules.
  fuzz-zstd-header   zstdContentSize does raw offset arithmetic over an
                     attacker-supplied frame header, and its result feeds
                     `needed = size * 2.05` in the disk preflight. Must never
                     throw, and never yield negative / NaN / fractional.
  fuzz-markdown      extractFrontmatter slices on delimiters over arbitrary
                     document bodies; the body it returns must never be
                     longer than its input, and declining to parse must not
                     alter the body.

Ran locally before wiring any CI: 2.2M executions on storage-key and 3.1M on
tar-line, plus 20k each on the other two, no crashes.

ClusterFuzzLite runs them — 5 min per sanitizer on PRs that touch src/ or
fuzz/ (code-change mode, so only new crashes fail), and an hour weekly in
batch mode to grow the corpus that seeds the PR runs. Both address and
undefined sanitizers. Actions pinned by SHA like everything else.

The build deliberately skips `npm install`: the targets reach 13 project
modules and zero third-party packages (verified), so installing the tree
would add minutes and drag in heavy optional native deps no target imports.

Also: fuzz/ joins the biome lint scope, knip learns the targets are entry
points and that jazzer is a CLI harness rather than an import, and
libFuzzer reproducers are gitignored — a crash-* belongs in a unit test,
not in the repo.

Suite: 2533 pass, 16 skip, 0 fail.

* fix(fuzz): drop the sanitizer matrix — JS builds reject it

The first PR run failed both legs at build time:

    ERROR: JavaScript projects cannot be fuzzed with sanitizers.

address/undefined are C/C++ concepts; the OSS-Fuzz JavaScript builder
refuses any sanitizer but `none`. Jazzer.js reports uncaught exceptions and
our own assertion failures instead, which is exactly what the four targets
assert against — so the matrix was buying nothing even in principle.

One job per workflow now, sanitizer: none, with the reason recorded inline
so it does not get "fixed" back.

* fix(fuzz): run Jazzer.js directly — ClusterFuzzLite can't build JavaScript

CFL's two layers contradict each other for JS, confirmed against the real
action across two pushes:

  build step   ERROR: JavaScript projects cannot be fuzzed with sanitizers.
  CIFuzz cfg   Invalid SANITIZER: none. Must be one of:
               ['address', 'memory', 'undefined', 'coverage'].

`compile` accepts only `none`; the config validator accepts everything
except `none`. Every permitted value fails one side, so no CFL integration
here can build at all — the sanitizer matrix I started with was wrong, and
so was `none`.

Dropped .clusterfuzzlite/ and the two cflite workflows and run Jazzer.js —
the engine CFL would have driven — straight from .github/workflows/fuzz.yml:
60 s per target on PRs touching src/ or fuzz/, 10 min per target weekly,
corpus cached via actions/cache so coverage compounds and PRs seed from the
last scheduled run. Crash reproducers upload as artifacts. Verified with the
exact command the workflow runs: 842,475 executions, 12 corpus entries kept.

The four targets are unchanged — they were never the problem.

Consequence worth stating: Scorecard's Fuzzing check may keep reporting 0.
It detects integrations (OSS-Fuzz membership, a .clusterfuzzlite/Dockerfile)
rather than whether fuzzing happens, and keeping dead CFL config around to
satisfy that detector would be scoring points rather than fuzzing. The
fuzzing is real; the badge may not follow.
G
Gigi committed
fe9367f7d257a4bfe58dc21cb73cf0584e4a6e4a
Parent: d015a0c
Committed by GitHub <noreply@github.com> on 8/5/2026, 9:15:11 PM