Skip to content

Harden remote-file ingestion and inbound endpoints#586

Merged
kelvinkipruto merged 2 commits into
mainfrom
claude/github-issue-573-662baa
Jul 20, 2026
Merged

Harden remote-file ingestion and inbound endpoints#586
kelvinkipruto merged 2 commits into
mainfrom
claude/github-issue-573-662baa

Conversation

@kelvinkipruto

Copy link
Copy Markdown
Contributor

Closes #573

What this does

Creates a safe boundary for Meedan remote downloads and Airtable uploads, and adds abuse controls to state-changing inbound endpoints.

Remote downloads (src/utils/files.ts, src/utils/ssrf.ts)

  • HTTPS required for all remote downloads; Meedan image URLs must additionally match the MEEDAN_ALLOWED_IMAGE_HOSTS allowlist (default checkmedia.org,*.checkmedia.org), applied in both the webhook route and the workflow sync.
  • SSRF filtering: private, loopback, link-local (incl. 169.254.169.254 metadata), CGNAT, multicast, reserved, and IPv6-equivalent (mapped/NAT64/6to4/ULA) addresses are rejected for IP-literal hosts, for every DNS result, and for every redirect destination (redirects are followed manually and re-validated per hop).
  • Bounded: max redirects (DOWNLOAD_MAX_REDIRECTS, default 3), response-header timeout per hop (30s), total download deadline (5min), streaming byte limit, and a per-instance download concurrency semaphore (default 4).

Uploads (/api/airtable-upload)

  • Extension and MIME metadata must both be allowed for the upload kind and agree with each other, and the file's magic bytes must match the claimed type (src/utils/fileSignature.ts) — spoofed files are rejected with 415.
  • SVG removed from every ingestion path (uploads and Meedan images). Locally served media (/api/media/file/*) now gets X-Content-Type-Options: nosniff and a sandboxing CSP.
  • Per-IP rate limit, per-instance concurrency gate, and a rolling daily byte budget bound storage/cost from a leaked token.

Inbound endpoints

  • Meedan webhook secret is now compared in constant time (shared safeCompare, also used by the upload token check).
  • Replay/idempotency: request bodies are fingerprinted (SHA-256); duplicates of completed requests replay the recorded response without re-processing, in-flight duplicates get 409, and 5xx outcomes stay retryable.
  • Webhook rate limiting per client IP.

Docs

Quotas, env vars, deployment notes (per-instance guard semantics), and residual-risk notes are documented in docs/remote-file-ingestion.md; .env.example updated.

Behaviour changes to be aware of

  • Plain-HTTP document links from Airtable are now rejected (the task marks the row failed so editors can fix the link).
  • SVG uploads/images are no longer accepted anywhere.
  • extractDocuments keeps fetching the app's own media from NEXT_PUBLIC_APP_URL via an explicit internal-only exception.

Testing

  • 149 integration tests pass (14 files), including ~80 new tests covering: SSRF variants (IP literals, DNS to private ranges, metadata endpoint, mapped IPv6), redirect bounding/downgrades/private destinations, hostname allowlisting, spoofed file signatures, extension/MIME mismatches, oversized files, rate limiting, and webhook replay + duplicate/concurrent delivery.
  • Lint passes; no new type errors (pre-existing Share.tsx errors untouched).

🤖 Generated with Claude Code

Closes #573.

Remote downloads (Meedan images, Airtable documents):
- Require HTTPS and reject private, loopback, link-local, CGNAT,
  cloud-metadata, and other non-public addresses for the initial URL,
  every DNS result, and every redirect destination (src/utils/ssrf.ts)
- Follow redirects manually with a bounded hop count, add response and
  total-download timeouts, and cap concurrent downloads per instance
- Meedan image URLs must match the MEEDAN_ALLOWED_IMAGE_HOSTS allowlist
  (defaults to checkmedia.org,*.checkmedia.org)

Uploads (/api/airtable-upload):
- Extension and MIME metadata must both be allowed and consistent, and
  file magic bytes must match the claimed type (src/utils/fileSignature.ts)
- SVG is no longer accepted from any ingestion path; locally served media
  gets nosniff + sandboxing CSP headers
- Per-IP rate limit, concurrency gate, and rolling daily byte budget

Inbound endpoints:
- Meedan webhook secret compared in constant time
- Duplicate webhook deliveries are idempotent: completed responses replay
  from a body-fingerprint cache, in-flight duplicates get 409, and 5xx
  outcomes stay retryable
- Meedan webhook rate limiting per client IP

Quotas and env vars are documented in docs/remote-file-ingestion.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Jul 17, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
PromiseTracker Ready Ready Preview, Comment Jul 20, 2026 6:16am

Request Review

Comment thread src/utils/files.ts Dismissed
… sandbox CSP

The global headers rule from #585 applies first; the /api/media/file/*
rule follows so its sandboxing CSP overrides the site-wide policy for
served upload files (nosniff already comes from the global rule).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@kelvinkipruto
kelvinkipruto merged commit 8d64066 into main Jul 20, 2026
5 checks passed
@kelvinkipruto
kelvinkipruto deleted the claude/github-issue-573-662baa branch July 20, 2026 06:18
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.

Harden remote-file ingestion and inbound endpoints

2 participants