Merge master into p6: the task 02 split, guard.PreCheck, AGENTS.md rule 6
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -333,6 +333,21 @@ func ipv4Compatible(addr netip.Addr) (netip.Addr, bool) {
|
||||
return v4, true
|
||||
}
|
||||
|
||||
// PreCheck is CheckURL without name resolution: scheme, userinfo, host and port, and an IP
|
||||
// literal checked against the address rules. A hostname is not resolved here; the fetch-time
|
||||
// CheckURL and the dial-time Control still apply to it. The batch endpoint uses it to refuse a
|
||||
// target at submit time.
|
||||
func (g *Guard) PreCheck(raw string) error {
|
||||
_, literal, err := ParseURL(raw)
|
||||
if err != nil && !(literal.IsValid() && g.allows(literal)) {
|
||||
return err
|
||||
}
|
||||
if literal.IsValid() {
|
||||
return g.CheckAddr(literal)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CheckURL parses, and if the host is a name, resolves every record. Any
|
||||
// blocked address fails the whole URL (mixed-record trick).
|
||||
func (g *Guard) CheckURL(ctx context.Context, raw string) (*url.URL, []netip.Addr, error) {
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
package guard
|
||||
|
||||
// P6 (owner, before the run): PreCheck is CheckURL without name resolution, for refusing a batch target at
|
||||
// submit time.
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/netip"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestPreCheck(t *testing.T) {
|
||||
g := &Guard{
|
||||
AllowPrefixes: []netip.Prefix{netip.MustParsePrefix("127.0.0.0/8")},
|
||||
LookupIP: func(context.Context, string) ([]netip.Addr, error) {
|
||||
t.Fatal("PreCheck resolved a name")
|
||||
return nil, nil
|
||||
},
|
||||
}
|
||||
for _, u := range []string{
|
||||
"https://example.com/",
|
||||
"http://example.com/path?q=1",
|
||||
"https://internal.test/", // a name: not resolved here, so not refused here
|
||||
"http://127.0.0.1:8080/", // an allowed literal is exempt from the port rule, as in CheckURL
|
||||
"http://93.184.215.14/", // a public literal
|
||||
"http://0x7f000001:8080/", // inet_aton form of 127.0.0.1: an allowed literal
|
||||
} {
|
||||
if err := g.PreCheck(u); err != nil {
|
||||
t.Errorf("PreCheck(%q) = %v, want nil", u, err)
|
||||
}
|
||||
}
|
||||
for _, u := range []string{
|
||||
"",
|
||||
"ftp://example.com/",
|
||||
"http://user@example.com/",
|
||||
"http://example.com:8080/",
|
||||
"http://10.0.0.5/",
|
||||
"http://169.254.169.254/latest/meta-data/",
|
||||
"http://[::1]/",
|
||||
"http://0x0a000005/", // inet_aton form of 10.0.0.5
|
||||
} {
|
||||
err := g.PreCheck(u)
|
||||
if err == nil || !strings.HasPrefix(err.Error(), "blocked") {
|
||||
t.Errorf("PreCheck(%q) = %v, want a blocked error", u, err)
|
||||
}
|
||||
}
|
||||
// With no allow list, loopback is refused like any other private literal.
|
||||
var zero Guard
|
||||
if err := zero.PreCheck("http://127.0.0.1/"); err == nil {
|
||||
t.Error("a zero Guard allowed loopback")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user