Files
gitea/modules/egress/policy/matchlist_test.go
T
TheFox0x7 1b1274486c fix(git)!: use internal proxy for all git operations (#39426)
Introduces gitproxy module which spawns a small forward proxy as scanner
for git calls
Replaces hostmatcher with matchlist which supports port rules
Deprecates ALLOWED_DOMAINS/BLOCKED_DOMAINS and ALLOW_LOCALNETWORKS
settings in migration in favor of full names we have in security
configs.
Removes `external` preset in favor of lax/strict modes, strict mode
requiring explicit ports if they aren't standard http/s ones.

Breaking changes:
- `external` preset no longer works as deny rule. To enforce that, use
`strict` mode and allow ranges to connect to
- Wildcards are no longer accepted in IP addresses
- `*` is no longer allowed as entry in lists
- domain rules now use curl like syntax `*.example.com` matching
subdomains but not `example.com`, `example.com` matching itself and all
subdomains. `example.*` is not a valid rule
- In the default `lax` mode, `[security] ALLOWED_HOST_LIST` no longer
restricts public hosts, set `EGRESS_MODE = strict` to keep an exclusive
list. A startup warning flags this
- Invalid list entries are logged at startup, invalid
`BLOCKED_HOST_LIST`/`BLOCKED_DOMAINS` entries stop it

Docs: https://gitea.com/gitea/docs/pulls/557
Signed-off-by: wxiaoguang <wxiaoguang@gmail.com>
Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: bircni <bircni@icloud.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-09-29 14:43:36 +02:00

135 lines
6.6 KiB
Go

// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package policy
import (
"net/netip"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestMatchHostname(t *testing.T) {
for _, tc := range []struct {
pattern, host string
port uint16
want bool
}{
// a bare entry matches its host and subdomains like NO_PROXY, a wildcard or leading dot matches subdomains only
{pattern: "example.com", host: "example.com", port: 80, want: true},
{pattern: "example.com", host: "sub.example.com", port: 80, want: true},
{pattern: "example.com", host: "notexample.com", port: 80}, // dot-anchored
{pattern: "*.example.com", host: "sub.example.com", port: 80, want: true},
{pattern: ".example.com", host: "sub.example.com", port: 80, want: true},
{pattern: "*.example.com", host: "example.com", port: 80}, // apex never matches
{pattern: "*.example.com", host: "notexample.com", port: 80},
{pattern: ".example.com", host: "notexample.com", port: 80},
// matching is case-insensitive and tolerates spaces, a port suffix and a trailing dot
{pattern: "example.com", host: " EXAMPLE.com.:8080 ", port: 80, want: true},
// strict entries without a port cover the web ports 80 and 443
{pattern: "example.com", host: "example.com", port: 443, want: true},
{pattern: "example.com", host: "example.com", port: 8080},
{pattern: "example.com:*", host: "example.com", port: 8080, want: true},
{pattern: "example.com:8080", host: "example.com", port: 8080, want: true},
{pattern: "example.com:8080", host: "example.com", port: 80},
{pattern: "example.com:[80|443-445]", host: "example.com", port: 444, want: true},
{pattern: "example.com:[80|443-445]", host: "example.com", port: 446},
} {
assert.Equalf(t, tc.want, NewAllowList(tc.pattern, Strict).MatchHostname(tc.host, tc.port), "pattern %q host %q port %d", tc.pattern, tc.host, tc.port)
}
// lax entries without a port cover every port
assert.True(t, NewAllowList("example.com", Lax).MatchHostname("example.com", 8080))
assert.True(t, NewAllowList(" , ", Strict).IsEmpty(), "blank entries are skipped")
assert.True(t, NewAllowList("", Strict).IsEmpty())
assert.False(t, NewAllowList("example.com", Strict).IsEmpty())
}
func TestMatchIPAddr(t *testing.T) {
for _, tc := range []struct {
pattern, ip string
port uint16
want bool
}{
{pattern: "10.0.0.0/8", ip: "10.1.2.3", port: 80, want: true},
{pattern: "10.0.0.0/8", ip: "11.1.2.3", port: 80},
// a bare IP entry carries the default ports like a hostname entry
{pattern: "192.168.1.1", ip: "192.168.1.1", port: 443, want: true},
{pattern: "192.168.1.1", ip: "192.168.1.1", port: 8080},
{pattern: "10.0.0.0/8:22", ip: "10.1.2.3", port: 22, want: true},
{pattern: "10.0.0.0/8:22", ip: "10.1.2.3", port: 80},
{pattern: "2001:db8::/64", ip: "2001:db8::1", port: 443, want: true},
{pattern: "2001:db8::/64", ip: "2001:db8::1", port: 8080},
{pattern: "[2001:db8::/64]:9418", ip: "2001:db8::1", port: 9418, want: true},
{pattern: "[2001:db8::/64]:9418", ip: "2001:db9::1", port: 9418},
{pattern: "[::1]:8080", ip: "::1", port: 8080, want: true},
{pattern: "[::1]:8080", ip: "::1", port: 80},
// named ranges expand to CIDRs
{pattern: "loopback", ip: "127.0.0.1", port: 80, want: true},
{pattern: "loopback", ip: "::1", port: 80, want: true},
{pattern: "loopback", ip: "10.1.2.3", port: 80},
{pattern: "private", ip: "100.64.0.1", port: 80, want: true}, // CGNAT
{pattern: "private", ip: "8.8.8.8", port: 80},
{pattern: "private:22", ip: "fd00::1", port: 22, want: true},
{pattern: "private:22", ip: "fd00::1", port: 80},
} {
addr := netip.AddrPortFrom(netip.MustParseAddr(tc.ip), tc.port)
assert.Equalf(t, tc.want, NewAllowList(tc.pattern, Strict).MatchIPAddr(addr), "pattern %q ip %s port %d", tc.pattern, tc.ip, tc.port)
}
}
func TestBlockListDefaultPorts(t *testing.T) {
// deny entries without a port cover every port, allow entries default to the web ports in strict mode
assert.True(t, NewBlockList("example.com").MatchHostname("example.com", 22))
assert.True(t, NewBlockList("10.0.0.0/8").MatchIPAddr(netip.AddrPortFrom(netip.MustParseAddr("10.0.0.5"), 12345)))
blocked := NewBlockList("example.com:22")
assert.True(t, blocked.MatchHostname("example.com", 22))
assert.False(t, blocked.MatchHostname("example.com", 80))
}
func TestRejectedEntries(t *testing.T) {
for _, tc := range []struct{ entry, wantErr string }{
{entry: "*", wantErr: `catch-all host pattern "*" matches every host and is not allowed`},
{entry: "external", wantErr: "EGRESS_MODE"},
{entry: "0.0.0.0/0", wantErr: `catch-all CIDR "0.0.0.0/0" covers every address and is not allowed`},
{entry: "::/0", wantErr: `catch-all CIDR "::/0" covers every address and is not allowed`},
{entry: "10.0.0.5/8", wantErr: `host bits must be zero, use "10.0.0.0/8"`},
{entry: "fe80::/64:80", wantErr: "unbracketed IPv6 with port"},
{entry: "[2001:db8::]/64", wantErr: `unexpected "/64" after bracketed target`},
{entry: "fe80::1%eth0", wantErr: "address zone is not dialable"},
{entry: "999.1.1.1", wantErr: `target "999.1.1.1" looks like an IP address but is not a valid one`},
{entry: "sub.*.example.com", wantErr: "is not an IP, CIDR, named range, or valid hostname pattern"},
{entry: "exämple.com", wantErr: "is not an IP, CIDR, named range, or valid hostname pattern"},
{entry: "example.com:99999", wantErr: `invalid port "99999"`},
{entry: "example.com:443-80", wantErr: `reversed port range "443-80"`},
{entry: "example.com:[]", wantErr: `empty port set "[]"`},
{entry: ":80", wantErr: "empty host: ':80'"},
{entry: "example.com:", wantErr: "empty port: 'example.com:'"},
{entry: "[::1", wantErr: "missing closing bracket"},
{entry: "[::1]:", wantErr: "empty port"},
{entry: "[]:80", wantErr: "empty host"},
} {
list := NewAllowList(tc.entry, Strict)
require.Lenf(t, list.Rejected(), 1, "entry %q", tc.entry)
assert.Containsf(t, list.Rejected()[0], tc.wantErr, "entry %q", tc.entry)
}
// the deny list rejects the same catch-alls, a rejected entry matches nothing
for _, entry := range []string{"*", "0.0.0.0/0", "::/0"} {
block := NewBlockList(entry)
require.Lenf(t, block.Rejected(), 1, "block entry %q", entry)
assert.Truef(t, block.IsEmpty(), "block entry %q", entry)
}
// a rejected entry drops only itself
list := NewAllowList("example.com, 10.0.0.5/8, example.org", Strict)
assert.True(t, list.MatchHostname("example.com", 80))
assert.True(t, list.MatchHostname("example.org", 443))
require.Len(t, list.Rejected(), 1)
assert.Contains(t, list.Rejected()[0], `use "10.0.0.0/8"`)
assert.Empty(t, NewAllowList("example.com", Strict).Rejected())
}