Skip to content

fix(security): harden outbound requests — IPv6 SSRF embeddings, LibreTranslate guard, proxy validation#344

Closed
rmyndharis wants to merge 1 commit into
mainfrom
fix/outbound-ssrf-hardening
Closed

fix(security): harden outbound requests — IPv6 SSRF embeddings, LibreTranslate guard, proxy validation#344
rmyndharis wants to merge 1 commit into
mainfrom
fix/outbound-ssrf-hardening

Conversation

@rmyndharis

Copy link
Copy Markdown
Owner

Three small outbound-request hardening fixes (grouped — all touch the SSRF/egress surface).

1. IPv6 SSRF blocklist closes embedded-IPv4 gaps

isBlockedAddress handled ::ffff: (v4-mapped) but allowed every other IPv6 form, so a literal that embeds an internal IPv4 slipped through:

  • 6to4 (2002::/16), e.g. 2002:7f00:1::127.0.0.1, and the compressed-zero form 2002:7f00::127.0.0.0
  • NAT64 (64:ff9b::/96), e.g. 64:ff9b::a9fe:a9fe169.254.169.254
  • deprecated IPv4-compatible ::/96, e.g. ::127.0.0.1

The literal is now expanded to its 8 hextets (so a compressed all-zero embedded segment is read as 0, not skipped) and classified by the embedded address. A 6to4/NAT64/compat of a genuinely public IPv4 stays allowed. Reachability is topology-conditional (needs NAT64/6to4 routing on the host), but the guard is meant to be fail-closed.

2. LibreTranslate plugin client is SSRF-guarded

Its outbound request carries the configured api_key but used redirect: follow with no guard. It now runs assertSafeFetchUrl and redirect: error (gated on the SSRF flag), so the key can't be replayed to a redirect-controlled internal host. A blocked-host error is a deterministic config error, so it no longer trips the client's circuit breaker. Behavior note: a loopback LibreTranslate sidecar (http://localhost:7001) must be added to SSRF_ALLOWED_HOSTS when SSRF protection is on — documented in the plugin's config schema.

3. Per-session proxyUrl is validated

Previously any ≤255-char string. Now @IsUrl requires an http(s)/socks4/socks5 URL — accepting credentialed (http://user:pass@host), SOCKS, single-label/container (http://squid:3128) and IP-literal proxies, rejecting malformed/other-scheme values. The engine re-checks it (isSupportedProxyUrl, same accept set) and skips a bad value with a warning instead of crashing the browser launch.

Compatibility

No wire-format change; non-breaking except the documented LibreTranslate-sidecar allowlisting.

Tests

SSRF blocklist gains 13 IPv6 cases (incl. the compressed-LO and v4-compatible bypasses); proxy DTO validation (single-label + IP-literal accepted, malformed rejected); LibreTranslate guard (internal blocked pre-fetch, redirect: error, SSRF error doesn't open the circuit). Full backend 766/766; dashboard build + lint clean.

The IPv6-embedding tests were green on the first attempt while a compressed-low-hextet bypass (2002:7f00::) was still live — an adversarial review caught it; the hextet-expansion rewrite + new cases close it.

…Translate guard, proxy validation

isBlockedAddress now classifies IPv6 literals that embed an IPv4 — 6to4 (2002::/16), NAT64 (64:ff9b::/96) and the deprecated IPv4-compatible ::/96 — by the embedded address, expanding the literal to full hextets so a compressed all-zero segment (e.g. 2002:7f00:: -> 127.0.0.0) is not skipped.

The LibreTranslate plugin client validates its target through the SSRF guard and refuses redirects, so its api_key-bearing request can't be replayed to an internal host; a blocked-host error is treated as a config error and no longer trips the circuit breaker. A loopback LibreTranslate sidecar must be allowlisted via SSRF_ALLOWED_HOSTS when SSRF protection is on.

Per-session proxyUrl is validated as an http(s)/socks4/socks5 URL at the API (single-label/container hostnames and credentialed/SOCKS proxies accepted) and re-checked by the engine before launching the browser.
rmyndharis added a commit that referenced this pull request Jun 19, 2026
* fix(webhook): deliver session lifecycle events and key webhook hardening (#335)

* fix(security): pin outbound webhook and media fetches to validated IP (#338)

* fix(plugins): persist plugin enable/config and restore (#339)

* fix(message): persist bulk-sent messages, sanitize SSRF (#340)

* fix(engine): return the real id for forwarded messages (#341)

* fix(security): harden outbound requests, IPv6 SSRF (#344)

* fix(security): secret-file perms, key pepper, allowedIps (#345)

* fix(storage): bound tar imports, contain storage keys (#346)

* fix(session): reconcile late acks, serialize reactions (#348)

* fix(contract): webhook timeout, bounded shutdown, 501 (#350)

* feat(session): force-kill a stuck session (#352)

* merge #343

* merge #351

* chore(release): v0.4.3 — CHANGELOG, version bump (package.json/dashboard/swagger), README + docs
@rmyndharis

Copy link
Copy Markdown
Owner Author

Shipped in v0.4.3 (integrated via the release PR #354 and tagged v0.4.3). Thanks! 🎉

@rmyndharis rmyndharis closed this Jun 19, 2026
@rmyndharis rmyndharis deleted the fix/outbound-ssrf-hardening branch June 19, 2026 18:33
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