feat: block outbound connections to private/custom IP ranges (SSRF protection)#1971
Open
lucoffe wants to merge 4 commits intolightpanda-io:mainfrom
Open
feat: block outbound connections to private/custom IP ranges (SSRF protection)#1971lucoffe wants to merge 4 commits intolightpanda-io:mainfrom
lucoffe wants to merge 4 commits intolightpanda-io:mainfrom
Conversation
Block outbound HTTP requests to specified IP ranges before TCP handshake using libcurl CURLOPT_OPENSOCKETFUNCTION callback. Fires after DNS resolution, reads resolved IP directly from sockaddr, does bitwise CIDR comparison. Fail-closed: unknown address families are blocked. --block_private_networks blocks RFC1918, localhost, link-local, ULA. --block_cidrs blocks additional comma-separated CIDRs. IPv4-mapped IPv6 (::ffff:x.x.x.x) is unwrapped to prevent bypass.
Contributor
Author
|
please hold review |
CIDRs prefixed with '-' are treated as allow rules that exempt matching IPs from blocking. Allow rules take precedence over both --block_private_networks and custom block CIDRs. Example: --block_private_networks --block_cidrs -10.0.0.42/32 blocks all private ranges except 10.0.0.42. Adds 3 new tests for allow-list behavior.
Contributor
Author
|
Added allow-list support to --block_cidrs. Prefix a CIDR with This blocks all private ranges except 10.0.0.42. Allow rules take precedence over both --block_private_networks and custom block CIDRs, so evaluation order is: allow list (immediate pass) -> private ranges -> custom blocks -> pass. Sorry for the hiccup |
Rename --block_private_networks to --block-private-networks and --block_cidrs to --block-cidrs to match the existing flag naming convention (e.g. --http-proxy, --proxy-bearer-token).
Contributor
Author
|
Also renamed flags to use dashes ( Ready for review 😁 |
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.
Motivation
When Lightpanda is used as a service (e.g. web scraping, rendering), user-supplied URLs can trigger requests to internal infrastructure such as cloud metadata endpoints, internal APIs, localhost services. This is a standard SSRF vector. These flags let operators block connections to internal IP ranges at the network level.
Usage
--block-private-networksblocks RFC1918, localhost, link-local, and ULA ranges.--block-cidrsblocks additional comma-separated CIDRs. Works standalone or combined.Prefix a CIDR with
-to allow (exempt from blocking), e.g.-10.0.0.42/32exempts that IP even if it falls in a blocked range. Allow rules take precedence over all block rules.How it works
sequenceDiagram participant Browser as Browser engine participant Curl as libcurl participant DNS as DNS resolver participant CB as opensocketCallback participant Filter as IpFilter participant Net as Network Browser->>Curl: HTTP request (any origin) Curl->>DNS: resolve hostname DNS-->>Curl: resolved IP (sockaddr) Curl->>CB: CURLOPT_OPENSOCKETFUNCTION(sockaddr) CB->>Filter: isBlockedSockaddr(sockaddr) alt IP matches blocked CIDR Filter-->>CB: blocked CB-->>Curl: CURL_SOCKET_BAD Curl-->>Browser: CURLE_COULDNT_CONNECT else IP allowed Filter-->>CB: allowed CB->>Net: posix.socket() Net-->>CB: fd CB-->>Curl: fd Curl->>Net: TCP SYN → endThe filter hooks into libcurl's
CURLOPT_OPENSOCKETFUNCTION: after DNS resolution but before socket creation. It reads the resolved IP directly from thesockaddrstruct (no string parsing) and does bitwise CIDR comparisons.Evaluation order: allow list first (immediate pass), then private ranges, then custom block CIDRs. The filter is fail-closed: unknown address families and null pointers are blocked.
IPv4-mapped IPv6 addresses (
::ffff:10.0.0.1) are unwrapped and checked against IPv4 rules to prevent bypass.Why
CURLOPT_OPENSOCKETFUNCTIONover CDPFetch.enable?CDP
Fetch.enableintercepts requests before DNS resolution:fetchor headlessservewithout a clientThe opensocket callback operates after DNS:
sockaddr, does bitwise CIDR comparisonChanges
src/network/IpFilter.zig-prefix for allow-listing, bitwise IPv4/IPv6 matching, IPv4-mapped-v6 unwrapping.src/network/http.zigopensocketCallback: checks filter, logs blocked IPs, returnsCURL_SOCKET_BADor a real fd.src/sys/libcurl.zigCurlSockAddrextern struct,CurlSockTypeenum,CurlOpenSocketFunctiontype,CURL_SOCKET_BADconstant.src/network/Runtime.zigIpFilter, passes to connections.src/Config.zig--block-private-networks(bool) and--block-cidrs(string, supports-prefix for allow entries) flag definitions.Tests
18 new unit tests:
::ffff:10.0.0.1matched against IPv4 rules)-prefix produces correct allow/block listsopensocketCallbackintegration (blocked returnsCURL_SOCKET_BAD, allowed returns valid fd)Limitations
CURLOPT_OPENSOCKETFUNCTIONfires for all transports including UDP). Anything that opens sockets outside of libcurl would not be covered.