Skip to content

Security: SSRF denylist at the capture URL seams - #4

Merged
Seanathon merged 1 commit into
mainfrom
dev-ssrf-denylist
Jun 24, 2026
Merged

Security: SSRF denylist at the capture URL seams#4
Seanathon merged 1 commit into
mainfrom
dev-ssrf-denylist

Conversation

@Seanathon

Copy link
Copy Markdown
Owner

Re-opens the SSRF work from #3 (auto-closed when its stacked base branch was deleted on PR #2's merge). Same change, now based directly on main.

Adds capture/net-guard.ts#assertCapturableUrl, called at the two seams every server user-URL fetch routes through (dispatchCapture + the snapshot capture()): scheme allowlist + private/loopback/link-local/ULA ranges (net.BlockList, IPv4-mapped IPv6, numeric-encoded IPv4) + localhost-family hostnames + a DNS-resolve-and-recheck (the real evil.com → 10.0.0.5 vector). Fail-closed: a blocked URL throws → the item's capture-error path marks it error. Zero new deps.

Residuals documented: DNS-rebinding TOCTOU; octal/mixed numeric encodings; IPv6 forms net.isIP rejects. Table-driven bypass test + two seam tests. Full suite green.

Server-side capture could be steered at internal addresses (cloud metadata
169.254.169.254, localhost admin panels, RFC1918) — pre-existing and app-wide,
reachable from every create path (POST /items, /share, assign, refetch) plus
archival snapshots. Add assertCapturableUrl() and call it at the two seams every
server user-URL fetch routes through: dispatchCapture (url-screenshot +
url-readable) and the snapshot capture(). Buffer (manual-upload) sources fall
through; operator CLIs (add.ts) are out of scope.

Guard = scheme allowlist (http/https) + private/loopback/link-local/ULA/CGNAT
ranges via net.BlockList (IPv4, IPv6 ::1, IPv4-mapped) + numeric-encoded IPv4
(decimal/hex) + localhost-family hostnames + a DNS-resolve-and-recheck (the real
evil.com→10.0.0.5 vector). Fail-closed/clean: a blocked URL throws → the item's
capture-error path marks it `error`, no crash. Zero new deps (node:net/dns).

Residuals (documented): DNS-rebinding TOCTOU (puppeteer/fetch re-resolve at
connect time); octal-per-octet / mixed numeric encodings; IPv6 forms net.isIP
rejects. Table-driven bypass test (userinfo, [::ffff:127.0.0.1], 2130706433,
0x7f000001, scheme, DNS→private). Suite 508 pass / 0 fail.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@Seanathon
Seanathon merged commit 1b14879 into main Jun 24, 2026
4 checks passed
@Seanathon
Seanathon deleted the dev-ssrf-denylist branch June 24, 2026 06:47
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