Commit graph

2 commits

Author SHA1 Message Date
ruv
af2a3f7dea fix(adr-index): harden traversal against symlinks, hostile filenames, adversarial trees (Phase-4 security review)
Addresses all four findings from the Phase-4 security audit on PR #857:

- MED (symlink following): walk() now uses readdirSync(withFileTypes)
  and never follows symlinks — file or directory — skipping each with a
  warning. Belt-and-braces: every kept entry is realpath-asserted to
  resolve inside docs/adr/, so a committed symlink can no longer inline
  outside content into INDEX.md or move the allocation counter.
- MED (fragile traversal): per-entry try/catch with skip+warn for broken
  symlinks / EACCES / unresolvable entries, a visited-realpath set guards
  cycles, and the top-level dispatch is wrapped so --check fails with
  "adr-index: fatal: <msg>" instead of a raw stack trace.
- LOW (markdown injection): slug and rel are now routed through
  mdCell/mdLinkText (escaping |, [, ]) and link targets go through
  encodeURI plus %28/%29 for parens, so a filename containing
  "|", "](", "[..](..)" can no longer break the table or forge links.
- INFO (Windows fail-open): abs.split('/') replaced with path.basename /
  path.relative + sep-normalized display paths, so --check cannot pass
  vacuously on Windows. Also: Math.max(...) -> reduce (no -Infinity on
  an empty set).

Adds `node scripts/adr-index.mjs --self-test`: builds an adversarial
fixture tree in a temp dir (outside-tree file symlink, escaping directory
symlink, broken symlink, symlink loop, hostile "|]()"-laden filename),
runs the script against it as a subprocess, and asserts 12 properties:
symlinks skipped with warnings, no stack trace, hostile row escaped,
link target URI-encoded, counter unaffected.

Regenerating INDEX.md after these changes produces no diff (all current
filenames are benign), and `--check` still passes: 348 ADR files, next
available ADR-317.

Note: CI wiring (a workflow step invoking --check) should land only
after this commit, per the audit — this commit is what makes the gate
safe to expose to PR-controlled trees.

Co-Authored-By: claude-flow <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_012Jib2gQyJpqCoo2xYAbb4X
2026-08-20 08:39:24 -04:00
ruv
6bc3d1d906 docs(adr): numbering hygiene — freeze duplicates, canonical index + collision check (PIR WP0a)
Verified ground truth on origin/main: 336 ADR files (289 plain-counter,
47 namespaced), 27 duplicated plain numbers spanning 61 files
(ADR-272 x5; ADR-264/252/194/144 x3; 22 numbers x2). Corrects issue
#845's ~15+ estimate and its ADR-040 x3 claim — 040/040a/040b is the
intentional sub-ADR convention, not a collision.

- ADR-316: policy — duplicates are frozen historical artifacts, never
  renamed; cite duplicated numbers as "ADR-NNN (slug)"; new numbers
  come from a single canonical counter whose source of truth is the
  generated index. 316 chosen because 305-315 are claimed on
  feat/pir-adrs (PR #847).
- docs/adr/INDEX.md: generated canonical index with next-available
  number header, per-file title/path/date/status and DUPLICATE flags.
- scripts/adr-index.mjs: regenerates the index; --check exits nonzero
  on any duplicate outside the frozen list (CI gate for #845).

Refs #845, #837

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-08-19 21:49:20 -04:00