Skip to content

fix(security): gate symlinked directories under docs/architectures/ - #1242

Merged
mrbobbytables merged 2 commits into
mainfrom
sec/fix-symlink-dir-gate
Oct 9, 2026
Merged

mrbobbytables merged 2 commits into
mainfrom
sec/fix-symlink-dir-gate

Conversation

@hivecommons-hive

Copy link
Copy Markdown
Contributor

Security Fix

Closes #1241

listArchitecturePages() in scripts/lib/architecture-pages.mjs is the single
source of truth for which files under docs/architectures/ are published
pages. scripts/validate-architectures.mjs drives its findActiveContent()
scan from that list, so a published page the walk never returns is never
scanned for live MDX expressions, event handlers or script-capable URL schemes.

A symlinked directory fell through every arm of the walk:

  • entry.isDirectory() is false for a symlink, so the walk never recursed.
  • A link named without a page extension (linked) was continued by the
    PAGE_EXTENSION test before the isFile()/irregular split was reached.

Docusaurus still publishes through it: @docusaurus/plugin-content-docs
(lib/docs.js:33) globs docs with Globby(options.include, { cwd, ignore })
and passes no followSymbolicLinks, which fast-glob defaults to true
(verified: new Settings({}).followSymbolicLinks === true). So every page
beneath such a link shipped at /architectures/<link>/... with the
active-content gate reporting zero findings rather than failing closed. The
importer's pruning loop (scripts/import-architectures.mjs:83) reads the same
.pages list, so the page was also never pruned or regenerated.

A symlinked page was already handled — this closes the same hole one level up.

What changed

  • scripts/lib/architecture-pages.mjs — listArchitecturePages() gains an
    explicit symlink arm after the isDirectory() arm. A link is never followed,
    and is reported as irregular when it carries a page extension or
    resolves to a directory (new resolvesToDirectory() helper, a guarded
    statSync() that returns false for a broken link).
  • scripts/validate-architectures.mjs — the irregular-page message now names a
    symlinked directory as well as a symlinked page.
  • tests/architecture-pages.test.mjs — 5 regression tests.

A symlink to a file Docusaurus does not route (notes.txt, a dangling
non-page link) publishes nothing and is still skipped, so no existing checkout
starts failing.

Verification

  • node --test tests/architecture-pages.test.mjs — 21/21 pass.
  • node --test tests/validate-architectures.test.mjs — 29/29 pass.
  • npm run validate:architectures — Validated 8 architecture records.
  • npx prettier --check on all three files — clean.
  • npm run test:unit before vs. after, in the same installed checkout:
    54 failures both runs (pre-existing in this sandbox), pass count
    1633 → 1638 for the 5 added tests. No regressions.

Files/functions claimed by this PR: scripts/lib/architecture-pages.mjs
(listArchitecturePages, resolvesToDirectory),
scripts/validate-architectures.mjs (irregular-page message),
tests/architecture-pages.test.mjs. Checked disjoint from every open
hold-gated PR: #1206/#1213 (svg-active-content.mjs), #1219
(mdx-active-content.mjs), #1229 (architecture-content.mjs), #1235
(docusaurus.config.js), and the test-lane PRs (e2e/coverage harness only).


Hold-gated: human review required.

— hive: agent=sec-check backend=copilot model=claude-opus-5 copilot=1.0.88

listArchitecturePages() is the single source of truth for which files under
docs/architectures/ are published pages, and validate-architectures.mjs drives
its findActiveContent() scan from that list. A symlinked directory fell through
every arm of the walk: isDirectory() is false for the link, so it never
recursed, and a link named without a page extension was skipped before the
isFile()/irregular split was reached.

Docusaurus still publishes through it. plugin-content-docs globs docs with
Globby(include, { cwd, ignore }) and passes no followSymbolicLinks, which
fast-glob defaults to true, so every page beneath the link shipped at
/architectures/<link>/... without ever reaching the active-content scan.

Fail closed on symlinks instead: a link is never followed, and is reported as
irregular when it carries a page extension or resolves to a directory. A link
to a file Docusaurus does not route still publishes nothing and is still
skipped, so no existing checkout starts failing.

Closes #1241

Signed-off-by: sec-check <sec-check@hive.kubestellar.io>
@hivecommons-hive hivecommons-hive Bot added the hold label Oct 9, 2026
@hivecommons-hive

Copy link
Copy Markdown
Contributor Author

Important

Held for human review by the hive's ACMM level gate.

This PR was opened by the "sec-check" agent while Hive policy required a human checkpoint for that agent. Non-outreach agents are held at ACMM L3–L5; the outreach agent is always held because it publishes project-facing communication.

Hive will keep the hold label until a human removes it. Operators can make a deliberate one-off release during an ACMM level change with release_level_holds=true, but level changes never release this hold automatically.

@hivecommons-hive

Copy link
Copy Markdown
Contributor Author

CI triage (ci-maintainer): the Validate repository run on head 98ab94f (run 37941231191) failed the unit-coverage gate — Source line coverage 99.99% is below the required 100%. Cause is diff-local: the symlink gate this PR adds to scripts/lib/architecture-pages.mjs leaves line 132 uncovered (file at 99.30% lines / 97.22% regions, uncovered region at line 131). Baseline is green (merge_group run 37956088312 passed at 16:01Z), so this is not a shared incident.

Fix: add a unit test that drives the new symlinked-directory branch in listArchitecturePages() (or restructure so existing tests reach it). Note main has since moved to facf9be (parser-based SVG/MDX scanning merge), so the branch also needs a base update. This lane cannot push to sec/ branches — leaving both to the owning lane or human reviewer.


🐝 Hive Agent: ci-maintainer | Instance: hosted-available-lke648397-260827-5n31 | SHA: unknown

— hive: agent=ci-maintainer backend=copilot model=kimi-k3 copilot=1.0.88

The new symlink arm intercepts symlinked pages before the isFile()/else
split, so the existing symlinked-page test no longer reaches the else
arm and the 100% source-line coverage gate failed. A FIFO named like a
page reaches it directly.

Signed-off-by: sec-check <sec-check@hive.kubestellar.io>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[sec-check] Symlinked directory under docs/architectures/ bypasses the active-content gate in listArchitecturePages()

1 participant