Skip to content

chore: 2nd-pass audit catches — SSRF guard tests, theme_url scope, defense-in-depth#38

Open
heznpc wants to merge 1 commit into
mainfrom
chore/2nd-pass-audit-2026-05-21
Open

chore: 2nd-pass audit catches — SSRF guard tests, theme_url scope, defense-in-depth#38
heznpc wants to merge 1 commit into
mainfrom
chore/2nd-pass-audit-2026-05-21

Conversation

@heznpc
Copy link
Copy Markdown
Member

@heznpc heznpc commented May 21, 2026

Summary

Adversarial 2nd-pass audit on the 'done' state from PRs #35/#36/#37.
Treats the prior 'all 158 tests pass / declared shipped' state as
verification target, not signal.

Findings

Critical

None. SSRF/XSS/CRLF/secret-leak/eval/exec all clean. cards.test.js has
~3700 effective assertions (8 grep-matches is a wrapper-pattern false
positive).

Major (fixed in this PR — previously declared done; 2nd-pass caught)

M1 — SSRF wire-level guards untested. SECURITY.md (#36) promises
redirect:error + 5s timeout + 2MB cap on ?source=rss&url=. The code
had them; the tests did not. A refactor dropping redirect:error would
have failed 0 tests. Fixed by threading fetchImpl through
fetchCapped and adding 6 wire-level tests modelled on
theme-url.test.js. Also tightened init-spread order so
redirect:error and signal are non-overridable by caller init —
defense in depth.

M2 — README:268 theme_url scope stale. Claimed
"Currently supported by /api/stats, /api/stack (others as follow-up)";
actually every card endpoint routes through resolveCardOptions.
Updated paragraph + dropped now-redundant Planned item.

Minor (inline TODOs)

  • src/common/card.js — unescaped data-cas-target="${titleTarget}".
    Safe today (all callers pass literal "username"), latent if a
    future caller threads user input. TODO marker.
  • src/fetchers/posts.jsFETCH_TIMEOUT_MS is duplicated in
    theme-url.js. TODO marker for lifting to common/utils.js if a
    third caller appears.

Out-of-scope

Verified clean: SVG XSS interpolations, CRLF at URL parser + setHeader,
git-history secret -S scan, no eval/Function/vm/child_process, 0
third-party tracking pixels, GitHub token pool isolation.

Test plan

  • npm run check — clean
  • npm test — 164/164 (was 158/158 + 6 new)
  • CI matrix test (22) / test (24) green

…fense-in-depth

Previously declared done; adversarial 2nd-pass caught the following:

M1 — SECURITY.md (added in #36) promises four guards on the user-controlled
fetch path (redirect:error, 5s timeout, content-length cap, body-length
cap). theme-url.js had integration tests via an injected fetchImpl;
posts.js had none. A refactor that dropped redirect:error from
src/fetchers/posts.js would have failed zero existing tests.
  - Thread fetchImpl through fetchCapped so its wire behavior is testable
    without monkey-patching globalThis.fetch.
  - Add 6 wire-level tests in tests/posts-security.test.js covering each
    guard plus non-ok status surfacing.
  - Tighten fetchCapped: spread caller init BEFORE the security defaults
    so redirect:error and signal are non-overridable. Caller code today
    doesn't trigger this, but the order made the guard pointlessly weak.

M2 — README:268 "theme_url Currently supported by /api/stats, /api/stack
(Other endpoints will adopt as a follow-up)" was stale. Every card
endpoint already routes through resolveCardOptions. Updated the
"Currently supported by" paragraph and dropped the redundant "theme_url
adoption across the catalog" item from the Planned block.

Minor (inline TODOs only, no behavior change):
  - src/common/card.js: data-cas-target="${titleTarget}" — all current
    callers pass the literal "username", so safe today, but unescaped
    attribute interpolation is a latent regression vector if anyone ever
    threads user input through. TODO marker.
  - src/fetchers/posts.js: FETCH_TIMEOUT_MS is duplicated in theme-url.js
    with the same value. TODO marker to lift to common/utils.js if a
    third caller arrives.

Out-of-scope (verified clean, no fix needed):
  - SVG XSS surface: every <text>${x}</text> traced; all escaped or
    numeric/internal-static.
  - CRLF in X-Theme-Error: blocked at both URL parser and setHeader
    layers (empirically verified on Node 22).
  - Secret leakage: git history -S clean for ghp_/BEGIN PRIVATE KEY etc.
  - eval/Function/vm/child_process: 0 occurrences.
  - Telemetry/tracking pixels: 0 third-party calls (README claim
    verified).
  - cards.test.js "8 asserts / 244 LOC" was a misread — the wrapper
    runs ~3700 effective assertions across themes × cards.
@heznpc heznpc enabled auto-merge (squash) May 21, 2026 13:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant