Skip to content

fix(server): harden the production edge — framing headers, per-client sign-in limits, DB readiness - #163

Open
Ayush7614 wants to merge 1 commit into
CopilotKit:mainfrom
Ayush7614:fix/server-edge-production-hardening
Open

Ayush7614 wants to merge 1 commit into
CopilotKit:mainfrom
Ayush7614:fix/server-edge-production-hardening

Conversation

@Ayush7614

Copy link
Copy Markdown
Contributor

What changed

Three production gaps in the server edge, all verified against current main and covered by new tests. No existing issue or PR covers any of them (checked open issues/PRs and full-text search).

1. Clickjacking / content-injection headers (apps/server/src/security-headers.ts, new)
The API sent only nosniff/no-referrer/no-store while serving bearer-equivalent signed links (file bytes, browser preview/console) that users open in a browser: any third-party page could frame them, and injected markup in HTML responses ran unsandboxed.

  • Strict CSP (default-src 'none'; base-uri 'none'; form-action 'none') for JSON/OAuth/error responses.
  • The design is constrained by two verified first-party embeds — BrowserConsole.web.tsx iframes /api/browsers/:id/console and PdfReader.web.tsx iframes /api/files/:id/content from the web-app origin — so a blanket DENY/SAMEORIGIN would break the app. Framing is instead restricted with a frame-ancestors allowlist built from the configured clients; the console keeps its exact script/style allowances plus framing protection; byte responses (PDF/PNG) carry framing-only policy so plugin/image rendering is untouched.
  • Permissions-Policy lockdown on every response; HSTS only when PUBLIC_API_URL is https (advertising it on plaintext would be a lie in local setups).
  • Centralized in one middleware (runs first, so 403/429/413s carry headers too); the CopilotKit SSE passthrough returns a raw Response that bypasses context headers, so it applies the policy explicitly.

2. Per-client sign-in budget (apps/server/src/app.ts, rate-limit.ts)
The /api/session limiter was a single process-global 30/minute counter shared by every address (app.ts): one hostile client spamming wrong keys locked every legitimate user out, and the fixed window admitted bursts across the boundary. Replaced with a 10/minute bucket per client key reusing the existing createRateLimitStore/resolveClientKey helpers (same TRUST_PROXY semantics as the global limiter).

3. Truthful readiness (apps/server/src/app.ts, db.ts, render.yaml)
/api/health answers ok:true unconditionally, so deploys receive traffic with Postgres unreachable. New unauthenticated GET /api/ready probes SELECT 1 via Store.ping() and fails closed (503 {database:"down"}); render.yaml now gates traffic on it. /api/health semantics unchanged.

Verification

  • pnpm lint — pass
  • pnpm typecheck — pass
  • pnpm test — 494 pass, 0 fail (483 baseline + 11 new in tests/server-edge.test.ts)
  • pnpm build:server — pass
  • New coverage: header values per route class (JSON, origin-rejected, signed PDF bytes, https HSTS), readiness up/down, per-IP sign-in isolation (attacker 429s while a second IP still gets 401s), policy/origin/key unit cases.

Integration limits

Unit + in-process app tests only (in-memory PGlite, fixture PDF); no provider credentials, live account, or device builds needed.

… sign-in limits, DB readiness

The API sent only nosniff/referrer/no-store while serving bearer-equivalent
signed links (file bytes, browser preview/console) that users open in a
browser: any third-party page could frame them and injected markup in HTML
responses ran unsandboxed. The sign-in limiter was one process-global
30/minute counter, so a single hostile client could lock every legitimate
user out. /api/health always answered ok, so deploys received traffic with
the database unreachable.

- Route-aware security headers (security-headers.ts): strict CSP for
  JSON/HTML, console allowances preserved plus frame-ancestors allowlist
  built from the configured clients (the web app iframes console and PDFs
  cross-origin, so DENY/SAMEORIGIN would break it), framing-only policy on
  byte responses, Permissions-Policy lockdown, HSTS when publicUrl is https.
- Per-client sign-in budget (10/min via the existing rate-limit store/key
  helpers) replacing the global counter.
- GET /api/ready probing SELECT 1 with 503 fail-closed; render.yaml now
  gates traffic on it. /api/health semantics unchanged.
- 11 regression tests in tests/server-edge.test.ts.

This branch has not been deployed

No deployments
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