fix(security): harden outbound requests — IPv6 SSRF embeddings, LibreTranslate guard, proxy validation#344
Closed
rmyndharis wants to merge 1 commit into
Closed
fix(security): harden outbound requests — IPv6 SSRF embeddings, LibreTranslate guard, proxy validation#344rmyndharis wants to merge 1 commit into
rmyndharis wants to merge 1 commit into
Conversation
…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.
Merged
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
Owner
Author
|
Shipped in v0.4.3 (integrated via the release PR #354 and tagged |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Three small outbound-request hardening fixes (grouped — all touch the SSRF/egress surface).
1. IPv6 SSRF blocklist closes embedded-IPv4 gaps
isBlockedAddresshandled::ffff:(v4-mapped) but allowed every other IPv6 form, so a literal that embeds an internal IPv4 slipped through:2002::/16), e.g.2002:7f00:1::→127.0.0.1, and the compressed-zero form2002:7f00::→127.0.0.064:ff9b::/96), e.g.64:ff9b::a9fe:a9fe→169.254.169.254::/96, e.g.::127.0.0.1The 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_keybut usedredirect: followwith no guard. It now runsassertSafeFetchUrlandredirect: 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 toSSRF_ALLOWED_HOSTSwhen SSRF protection is on — documented in the plugin's config schema.3. Per-session
proxyUrlis validatedPreviously any ≤255-char string. Now
@IsUrlrequires anhttp(s)/socks4/socks5URL — 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.