Refuse connections to non-public addresses - #73
Merged
Conversation
- Reject a resolved address that is loopback, link local, multicast, unspecified, or reserved. - Restrict the protocol set to HTTP and HTTPS. - Cap the redirect chain at three hops. - Add DimsAllowPrivateAddresses and DimsAllowlistSigned. - Route the sizer and the watermark overlay through the same guard. - Allocate the request record with apr_pcalloc.
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.
What
Every fetch now passes a network guard before a socket opens.
src/netguard.canswers three questions: is the scheme one this service speaks, is the resolved address one a real image origin can hold, and is the host one the operator named.The guard has three tiers.
Tier one always applies, on every endpoint. A resolved address that is loopback, link local, multicast, unspecified, or reserved is refused. So is any scheme other than HTTP and HTTPS. So is a redirect chain longer than three hops. No deployment fetches an image from one of these, so refusing them cannot break an upgrade. The address check is a
CURLOPT_OPENSOCKETFUNCTIONcallback, which runs once per connection, after the name resolves. A name that resolves to a refused address is refused whatever it is called, and every redirect hop arrives at the check on its own.Tier two is
DimsAllowPrivateAddresses, defaultOn. It covers 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, and IPv6 unique local. Some deployments serve images from a private origin, so refusing these by default would break them.Tier three is
DimsAllowlistSigned, valueslogandenforce, defaultlog. It decides whether the host allowlist applies to a signed request and to a redirect target. Inlogmode the guard records what enforcing would refuse and lets the request through, so an operator can fill the allowlist from the log before changing the value.The sizer and the watermark overlay reach the fetch without passing the handler, so both now route through the guard. The sizer also gains the allowlist check the handler has always made.
The request record moves from
apr_palloctoapr_pcalloc. The field list that follows it sets one field at a time, so a field it does not name held whatever the pool handed over.Why
C1: the service passed the caller's URL to libcurl with no restriction. There was no protocol set, no redirect cap, and no address check. The host allowlist was the only gate, and the handler turned it off for every signed request, which is the path that carries the traffic. A caller reaching 169.254.169.254 gets IAM credentials on EC2 and ECS.
C2: the allowlist ran once, against the hostname in the request.
CURLOPT_FOLLOWLOCATIONwas then set with no limit and no re-check, so an allowlisted origin returning302 Location: http://127.0.0.1:11211/sent the module to loopback.M1: the sizer parsed the URL and fetched it directly. It consulted no allowlist and validated no signature, so any caller could ask the service to connect to any address and learn whether the response decoded as an image.
H8, in part: the request record was allocated uninitialized and filled in field by field.
Verify
166 cases pass, 155 before. No golden file changes.
The suite gains three servers. Port 8003 sets
DimsAllowlistSigned enforce; port 8004 setsDimsAllowPrivateAddresses off, which refuses every host on the compose network and shows the check runs at the socket. The cases on port 8000 assert the shipped defaults, including the two that keep this a drop-in upgrade: a signed request still reaches a host outside the allowlist, and a redirect off the allowlist is still followed.test/unit/test_netguard.cportsTestIsPublicAddress,TestHostAllowed, andTestValidateImageURLfrom../go-dims/internal/core/network_test.go. The fetch cases intest/http/test_allowlist.cport the rest from../go-dims/internal/source/http_test.goand lose their expected failure markers.A refusal answers 400, which is the status go-dims returns for the same refusal.
Breaking
Nothing that a correct deployment does today stops working. Both new directives default to the behavior the module had before.
A deployment that fetches images from loopback, link local, or a reserved range now fails. So does one that uses a scheme other than HTTP or HTTPS, or that relies on a redirect chain longer than three hops.
DimsHostnameNotInWhiteliststill answers 500 underDimsAllowlistSigned enforce, because a host outside the allowlist has reported that way since the module was written. The status for every refusal moves together, behind its own directive.The configuration pages do not exist yet, so neither directive is documented outside the
httpd -Lhelp text.DimsMaxSourceBytesis in the same position.