Skip to content

fix(api): harden the anonymous public surface (system info, rate limits, form DoS) - #1065

Closed
mmcintosh wants to merge 4 commits into
mainfrom
fix/harden-public-surface
Closed

mmcintosh wants to merge 4 commits into
mainfrom
fix/harden-public-surface

Conversation

@mmcintosh

@mmcintosh mmcintosh commented Aug 20, 2026 •

Copy link
Copy Markdown
Collaborator

Description

Hardens anonymous, unauthenticated endpoints: system info disclosure, missing rate limits on public writes, and a form-submit DoS.

Changes

  • GET /api/system/stats (includes total user count) and GET /api/system/env (reveals configured bindings) now use requireAuth(). /health, /info and /ping stay public.
  • Per-IP rate limits: POST /api/forms/:identifier/submit 20/min, POST /api/events 60/min. As before, the limiter is skipped when CACHE_KV is unbound.
  • Form submit rejects bodies over 512 KB by Content-Length before parsing (413).
  • sanitizeDeep() is bounded to depth 32 and 10,000 nodes; exceeding either returns 413 and nothing is persisted.

Breaking: anonymous callers now get 401 from /api/system/stats and /api/system/env.

Out of scope: stripping error.message from public read handlers; core account lockout.

Testing

Unit Tests

  • Added/updated unit tests: api-system.test.ts (anonymous 401 on /stats and /env); public-forms.test.ts (deep nesting, too many fields and oversized Content-Length return 413; normal submission still 200)
  • All unit tests passing; type-check clean

E2E Tests

  • Added/updated E2E tests: spec 110 (@smoke @api), anonymous 401 on stats/env, /health stays public, oversized submit 413
  • All E2E tests passing (not run locally; CI validates)

Checklist

  • Code follows project conventions
  • Tests added/updated and passing
  • Type checking passes
  • No console errors or warnings
  • Documentation updated (if needed): N/A

Three fixes to the unauthenticated surface:

- Info disclosure: GET /api/system/stats (total user/content/media counts) and
  GET /api/system/env (which bindings/integrations are configured) now require
  authentication. /health, /info, /ping stay public for probes.
- Rate limiting: POST /api/forms/:id/submit (20/min/IP) and POST /api/events
  (60/min/IP) are now per-IP rate-limited, bounding anonymous write amplification.
  (The limiter fail-open-skips when CACHE_KV is unbound, as before.)
- Form-submit DoS: reject bodies over 512KB by Content-Length before parsing,
  and bound the recursive sanitizer (depth 32, 10k nodes) — a hostile deeply
  nested / oversized JSON payload now returns 413 instead of exhausting the stack
  or CPU.

Deferred (noted in the review, not in this PR): stripping error.message from
public read handlers (low-severity, shared error handling) and core account
lockout (entangled with the security-audit plugin).

Tests: api-system anon-401 cases; public-forms depth/node/size 413 cases; E2E
spec 110. Full core suite green.
@mmcintosh
mmcintosh marked this pull request as ready for review October 18, 2026 03:55
@mmcintosh
mmcintosh requested a review from lane711 as a code owner October 18, 2026 03:55
…wn CI infra flake, unrelated to PR changes)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@lane711

lane711 commented Oct 23, 2026

Copy link
Copy Markdown
Collaborator

Thanks for this security hardening PR @mmcintosh — the anonymous surface audit was really valuable. 🙏

Your A4 patch was cherry-picked into #1134 (with your attribution preserved) and expanded with additional ACL fixes and bootstrap hardening. That PR has been merged to main.

Closing this in favour of the merged cherry-pick — it will ship in the next release.

@lane711 lane711 closed this Oct 23, 2026

This branch was previously deployed

1 inactive deployment
internal — fbf9c54e Deployed Oct 22, 2026 by lane711 via authorize #1709
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