Skip to content

fix(sec): seal merger-proxy + redemption + transactional SPAC deal recompute#169

Open
sroussey wants to merge 2 commits into
claude/wonderful-hypatia-y6cb4lfrom
claude/wonderful-hypatia-bko1yr
Open

fix(sec): seal merger-proxy + redemption + transactional SPAC deal recompute#169
sroussey wants to merge 2 commits into
claude/wonderful-hypatia-y6cb4lfrom
claude/wonderful-hypatia-bko1yr

Conversation

@sroussey

Copy link
Copy Markdown
Contributor

Summary

Four HIGH-priority findings from an automated security/correctness review of sec for the last 24h, scoped to extractors that #165 / #166 left half-wired.

Stacked on PR #165 (claude/wonderful-hypatia-y6cb4l). The seal helpers (verifyRowSpan, boundSourceSpan, multi-stage wrapUntrusted defang) are inherited from that PR; this PR extends them to the two missed extractors and closes a defang gap. Retarget to main after #165 merges.

  • HIGH-1 (security) — merger-proxy + redemption extractors bypassed fix: six HIGH-priority hardening fixes (prompt-injection seal + 8-K storage + XML entity expansion) #165's prompt-injection seal. Filer-controlled DEFM14A prospectus / 8-K narrative could ship unbounded source_span through SpacMergerExtractionRepo / SpacRedemptionExtractionRepo.
  • HIGH-2 (security, defense-in-depth) — wrapUntrusted defang missed </UNTRUSTED&Tab;FILER&Tab;DOCUMENT>-style tokens because &Tab; wasn't in the named-entity table and &/; broke the tag-scan regex. The per-call nonce on the real fence still holds — this closes the layered gap.
  • HIGH-3 (DX/observability) — processMergerProxy never wrote to extractor_runs. sec version coverage extractor merger-proxy read zero; drop-previous was a no-op.
  • HIGH-4 (correctness) — SpacReportWriter.recomputeAndSaveDeals deleted orphan deal rows then wrote new rows non-atomically. A crash mid-pass corrupted the SPAC report row.

Two commits, scoped per concern pair:

  1. fix(forms): apply prompt-injection seal to merger-proxy + redemption extractors (HIGH-1 + HIGH-2)
  2. fix(spac): record extractor_runs for merger-proxy + transactional recompute (HIGH-3 + HIGH-4)

Test plan

  • bun test src/sec/forms/registration-statements/s1/
  • bun test src/sec/forms/proxies-information-statements/
  • bun test src/sec/forms/miscellaneous-filings/
  • bun test src/storage/spac/
  • bun test src/storage/form-8k-event/
  • bun run build

Generated by Claude Code

claude added 2 commits June 25, 2026 08:33
…extractors

The merger-proxy and redemption extractors that landed via PR #166 missed
the new prompt-injection seal helpers introduced in PR #165. The seal — raw-
byte verifyRowSpan at gate, boundSourceSpan at persist — is now applied to
both extractors so an unbounded source_span can no longer ship through
SpacMergerExtractionRepo / SpacRedemptionExtractionRepo via filer-controlled
DEFM14A or post-vote 8-K narrative.

Also widen the fence defang to neutralize the </UNTRUSTED&Tab;FILER&Tab;
DOCUMENT> family of bypasses: add whitespace named entities (Tab, NewLine,
nbsp, ensp, emsp, thinsp, zwsp, zwnj, zwj) to NAMED_ENTITY_TABLE and collapse
numeric whitespace entities (&#9; / &#x20; etc.) to a single space before the
TAG_SHAPED scan. The per-call 64-bit nonce on the real fence remains the
primary defense; this closes the layered defang gap.

No extractor version bumps: prompt is unchanged in non-adversarial inputs,
the gate change is normalization-only.
…ompute

Two SPAC correctness issues:

1. processMergerProxy never wrote to extractor_runs. The outer
   ProcessAccessionDocFormTask records a run for the form's extractor id
   (DEFM14A), but the merger-proxy nested extractor id was uncovered, so
   `sec version coverage extractor merger-proxy` always read zero and
   `drop-previous` was a no-op. Mirrors the redemption recordRun pattern
   from PR #168: success at the end, PARSE_ERROR in the segmenter catch,
   PROVIDER_ERROR around runSection.
2. SpacReportWriter.recomputeAndSaveDeals deleted orphan deal rows then
   wrote new deals in a non-atomic loop. A crash, AbortSignal, or DB error
   between the delete and the final saveDeal corrupted the SPAC report
   row. New SpacDealReplace helper wraps the delete+upsert pass in a real
   transaction: better-sqlite3 `db.transaction` for SQLite, BEGIN/COMMIT/
   ROLLBACK on a checked-out PG client. In-memory fallback retains the
   sequential semantics (no concurrency in tests).

No extractor version bump: merger-proxy stays at 1.0.0; `coverage` will
simply start populating an empty table.
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.

2 participants