Skip to content

fix(edge): bound import-source and share requests - #116

Merged
Programcode merged 2 commits into
mainfrom
fix/edge-import-share-limits
Oct 7, 2026
Merged

Programcode merged 2 commits into
mainfrom
fix/edge-import-share-limits

Conversation

@Programcode

@Programcode Programcode commented Oct 7, 2026 •

Copy link
Copy Markdown
Owner

Closes the three findings still open from the 2026-08-19 security review: [4], [7] and [10]. The other eight are already fixed on main.

What changes

  • import-source, slow senders ([4]). The 10-second timeout used to stop when the headers arrived. It now covers DNS, every redirect hop and the whole body read. Each body read races the deadline, so a source that sends slowly, or not at all, gets cut off at 10 s with a 504. The 1 MB cap is unchanged.
  • import-source, address ranges ([7]). Resolved A and AAAA records are now checked against the full IANA IPv4 and IPv6 special-purpose registries. That adds the IETF protocol, benchmarking, documentation, multicast and reserved ranges, NAT64 (64:ff9b::/96 and 64:ff9b:1::/48), 6to4, Teredo and the other IPv6 special blocks. IPv4-mapped IPv6 addresses are now checked in hex form too.
  • share, limits for signed-in users ([10]). Requests over 512 KB get a 413 before anything touches the database. expiresInDays must be a whole number from 1 to 90 (400 otherwise). Each user can create 50 shares a day, enforced through the existing atomic consume_daily_quota RPC under a new share key (429 with the same code/limit/current fields as import-source). Malformed JSON now gets a 400 instead of a 500.

No migration: consume_daily_quota already accepts any quota key.

Defaults to check

  • Shares now always expire. A share without expiresInDays used to be permanent. It now expires after 30 days, because an upper bound means nothing if leaving the field out still gives a permanent link. Nothing in src/ calls the share function today, so no client changes. Existing shares keep their current expiry.
  • 512 KB payload cap, 90-day maximum and 50 shares a day are my picks.

Needs Adam's sign-off

The runtime cannot pin a fetch to the address it checked, so a DNS answer that changes between the check and the connection is not covered. This PR records that as an accepted exception in docs/standards/security.md under "Recorded exceptions" and marks it as needing Adam's decision. Closing it fully needs an egress proxy outside the Edge runtime.

Verification

Local only. Every GitHub Actions job was refused for billing ("recent account payments have failed"), so there is no CI run on this head.

  • New tests written first. Against the old code, 12 failed for the expected reasons: a body still being read after 11 s, reserved addresses accepted, no share limits. They pass on this branch. With the deadline signal removed from the body read, the slow-body tests fail again.
  • Local checks at 72af855. npm run test:edge 333/333, npm test 272/272, lint, typecheck and docs:check all pass. npm run verify passed at 717f41d, including build and the Chromium smoke run (29 passed).
  • Real Deno runtime, real DNS and network. The actual import-source and share handlers ran under deno run, with only Supabase auth and REST stubbed:
    • A server dripping one paragraph a second, reached through its public IP via nip.io, got a 504 after 10.0 s. The server logged the disconnect at 9.8 s.
    • Names resolving to 198.18.0.1, 224.0.0.1 and 192.0.0.1 got a 400 before any connection.
    • A real Wikipedia import still worked (8,205 words).
    • Share: a normal share got a 30-day expiry, 600 KB got a 413, expiresInDays 91 got a 400 and 90 was accepted, and the 51st share of the day got a 429.
  • Not run locally: verify-debatelab recipes and the DB gate. The shared Supabase stack was busy with another PR's two-hour agentic run. No SQL changed, and the share quota key reuses consume_daily_quota, which daily_usage_quota_test.sql already covers. Staging gets the full-stack check after merge, before production.
  • Codex review. Round 1 found five issues; the fixes and dispositions are in the PR comment. Round 2 on 72af855 found no bugs.

🤖 Generated with Claude Code

import-source now keeps one 10-second deadline from DNS through the last
body byte, so a slow sender is cut off instead of holding the function
open. Resolved addresses are checked against the full IANA special-purpose
lists, including NAT64, benchmarking, multicast and reserved space.

share now caps the request at 512 KB, accepts expiries of 1 to 90 days
(30 when omitted), and reserves a "share" daily quota of 50 per user
through consume_daily_quota.

The DNS pinning gap that the runtime cannot close is recorded as an
exception in docs/standards/security.md, pending Adam's decision.

Closes findings [4], [7] and [10] of the 2026-08-19 security review.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@vercel

vercel Bot commented Oct 7, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
debate-navigator Ready Ready Preview Oct 7, 2026 3:21pm UTC
debatelab-staging Ready Ready Preview Oct 7, 2026 3:21pm UTC

Copilot AI balanced review requested due to automatic review settings October 7, 2026 15:04

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@coderabbitai

coderabbitai Bot commented Oct 7, 2026 •

Copy link
Copy Markdown

Warning

Review limit reached

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

Next included review available in 36 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Learn how review limits work.

Review configuration:

⚙️ Run configuration
  • Configuration used: Repository YAML (base), Organization UI (inherited)
  • Review profile: CHILL
  • Plan: Free
  • Run ID: 60b1aabf-0c3a-4403-9d4a-c931f8a0d14e
📥 Commits

Reviewing files that changed from the base of the PR and between f9f3efc and 72af855.

📒 Files selected for processing (14)
  • docs/standards/security.md
  • src/test/security-hardening.test.ts
  • supabase/README.md
  • supabase/functions/_shared/bounded-body.test.ts
  • supabase/functions/_shared/bounded-body.ts
  • supabase/functions/_shared/url-validation.ts
  • supabase/functions/import-source/deadline_test.ts
  • supabase/functions/import-source/index.ts
  • supabase/functions/import-source/resource-limits.ts
  • supabase/functions/import-source/url-validation.test.ts
  • supabase/functions/import-source/url-validation.ts
  • supabase/functions/share/auth_test.ts
  • supabase/functions/share/index.ts
  • supabase/functions/share/limits_test.ts
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Note

🎁 Summarized by CodeRabbit Free

Your organization is on the Free plan. CodeRabbit will generate a high-level summary and a walkthrough for each pull request. For a comprehensive line-by-line review, please upgrade your subscription to CodeRabbit Essentials by visiting https://app.coderabbit.ai/settings/billing.

Comment @coderabbitai help to get the list of available commands.

import-source now reads its own request body with a 16 KB cap under the
same deadline, so a stalled upload gets a 408 instead of holding the
function open and then reporting a misleading source timeout. Adds
100:0:0:1::/64 to the blocked IPv6 ranges.

Replaces the importer source-string assertions with behavior tests for
the byte cap at its exact boundary and the one-day share expiry.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@Programcode

Copy link
Copy Markdown
Owner Author

Codex review round 1 (at 717f41d): five findings.

  1. import-source request body counted against the deadline but unbounded (medium). Fixed in 72af855. The request body is now read with a 16 KB cap under the same deadline. A stalled upload gets a 408 and its stream is cancelled, and an oversized one gets a 413. Test: "import-source stops waiting for a request body that never finishes" (fails on 717f41d).
  2. Missing IPv6 dummy prefix 100:0:0:1::/64 (medium). Fixed in 72af855 and added to the blocked-range test (fails on 717f41d).
  3. Blanket ranges also block a few globally reachable addresses (low). Valid, accepted as is. The registry marks some anycast and ORCHID addresses inside blocked blocks as global, such as 192.0.0.9 and 2001:3::/32. None of them serve web pages, so blocking the whole block costs nothing and keeps the list simple. A comment in _shared/url-validation.ts now says so.
  4. Importer guard in security-hardening.test.ts matches source strings (low). Fixed in 72af855. The string assertions are gone. Behavior is covered by import-source/deadline_test.ts, import-source/url-validation.test.ts, and the new _shared/bounded-body.test.ts.
  5. Missing boundary tests (low). Fixed in 72af855. Added a body of exactly the cap (accepted), one byte over (rejected), and a too-small Content-Length (rejected), plus share expiries of 1 and 90 days (accepted). Both boundaries already behaved correctly, so these tests guard them rather than fail first.

Local at 72af855: npm run test:edge 333/333, npm test 272/272, lint and typecheck pass. Codex round 2 is starting on this head.

@Programcode

Copy link
Copy Markdown
Owner Author

Deployed to production from 6ec1a65, which also contains the follow-up #118. Staging verification found that the hosted runtime holds a response until the request body has been read, so oversized requests waited about 150 s for their 413. #118 fixes that.

  • import-source v13 → v14, then share v17 → v18 (verify_jwt still on). No migrations. The previous production source is saved on the VPS for rollback.
  • Production checks, credential-free:
    • CORS preflight returns 200 on both functions.
    • Unauthenticated requests get a 401.
    • A rejected token with a 600 KB body gets a 401 in about 0.4 s.
    • https://debate.pi-stack.org returns 200.
  • Staging at 6ec1a65, signed in with the seeded Pro account:
    • The reserved ranges 198.18.0.1 and 224.0.0.1 (via nip.io) get a 400.
    • A real Wikipedia import returns 200 (8,205 words).
    • A share gets a 30-day expiry and reads back anonymously.
    • 600 KB and 2 MB shares get a 413 in under a second.
    • expiresInDays: 91 gets a 400.
    • Earlier, at 2c687db: a source dripping through a public tunnel got a 504 at 10.8 s, and the source server saw the disconnect at 9.9 s.
  • Not run: the production canary, which only runs in Actions and is refused for billing.

@Programcode
Programcode deleted the fix/edge-import-share-limits branch October 7, 2026 16:08

This branch was successfully deployed

2 active deployments
Preview – debatelab-staging — 72af8556 Deployed Oct 7, 2026 by vercel[bot]
Preview – debate-navigator — 72af8556 Deployed Oct 7, 2026 by vercel[bot]
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