-
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathhost.go
More file actions
159 lines (129 loc) · 4.55 KB
/
Copy pathhost.go
File metadata and controls
159 lines (129 loc) · 4.55 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
package safehttp
import (
"fmt"
"net/netip"
"strings"
"golang.org/x/net/idna"
)
// dnsLookup converts DNS names into the ASCII form used for comparisons.
//
// Sources:
// - https://pkg.go.dev/golang.org/x/net/idna
// - https://www.unicode.org/reports/tr46/
var dnsLookup = idna.New(
idna.MapForLookup(),
idna.VerifyDNSLength(true),
idna.BidiRule(),
)
// hostMatcher stores the optional host allowlist in normalized ASCII form.
//
// A zero-value matcher allows every host. When configured, exact entries match
// one host and wildcard entries only match real subdomains, so "*.example.com"
// matches "api.example.com" but not "example.com" or "badexample.com".
type hostMatcher struct {
exactHosts map[string]struct{}
wildcardSuffixes []string
}
// compileHostMatcher validates host patterns once during construction.
//
// Only exact hosts and leading wildcard patterns are accepted. Broad patterns
// like "*example.com" are rejected because they are ambiguous and can allow
// suffix-confusion domains.
func compileHostMatcher(patterns []string) (hostMatcher, error) {
if len(patterns) == 0 {
return hostMatcher{}, nil
}
matcher := hostMatcher{
exactHosts: make(map[string]struct{}, len(patterns)),
}
for _, pattern := range patterns {
pattern = strings.TrimSpace(pattern)
if after, ok := strings.CutPrefix(pattern, "*."); ok {
host, err := normalizeHostPattern(after)
if err != nil {
return hostMatcher{}, fmt.Errorf("safehttp: invalid wildcard host %q: %w", pattern, err)
}
// Store wildcard patterns as ".example.com" so a suffix check can
// require a label boundary and avoid matching "badexample.com".
matcher.wildcardSuffixes = append(matcher.wildcardSuffixes, "."+host)
continue
}
if strings.Contains(pattern, "*") {
return hostMatcher{}, fmt.Errorf("safehttp: invalid host %q: wildcard must be a leading *", pattern)
}
host, err := normalizeHostPattern(pattern)
if err != nil {
return hostMatcher{}, fmt.Errorf("safehttp: invalid host %q: %w", pattern, err)
}
matcher.exactHosts[host] = struct{}{}
}
return matcher, nil
}
func (m hostMatcher) allows(host string) bool {
if len(m.exactHosts) == 0 && len(m.wildcardSuffixes) == 0 {
return true
}
if _, ok := m.exactHosts[host]; ok {
return true
}
for _, suffix := range m.wildcardSuffixes {
if strings.HasSuffix(host, suffix) && len(host) > len(suffix) {
return true
}
}
return false
}
func normalizeURLHost(host string) (string, netip.Addr, error) {
host = strings.TrimSpace(strings.TrimSuffix(host, "."))
if host == "" {
return "", netip.Addr{}, fmt.Errorf("host cannot be empty")
}
// Numeric-looking hosts are treated as IP literals and must parse
// canonically; this avoids octal, dword, and other ambiguous IPv4 forms.
addr, isLiteral, err := parseIPLiteralHost(host)
if err != nil {
return "", netip.Addr{}, err
}
if isLiteral {
// Preserve IPv4-mapped IPv6 until CheckAddr applies custom prefix policy
// and the default special-purpose range block.
return addr.String(), addr, nil
}
// DNS comparison is done on IDNA ASCII form so Unicode and punycode inputs
// share one canonical representation for host allowlists and errors.
normalized, err := dnsLookup.ToASCII(host)
return normalized, netip.Addr{}, err
}
func parseIPLiteralHost(host string) (netip.Addr, bool, error) {
if !strings.ContainsAny(host, ":%") && strings.Trim(host, "0123456789.") != "" {
return netip.Addr{}, false, nil
}
// IPv6-looking hosts and numeric IPv4-looking hosts must parse
// canonically. This rejects scoped syntax, octal IPv4, dword IPv4, and
// other ambiguous forms instead of letting them fall through to DNS.
addr, err := netip.ParseAddr(host)
if err != nil {
return netip.Addr{}, true, fmt.Errorf("ip literal is invalid")
}
return addr, true, nil
}
// normalizeHostPattern accepts host allowlist entries, not URLs.
//
// Ports and IPv6 literals are rejected in AllowHosts because port policy lives
// in AllowPorts and IPv6 literals contain colons that are ambiguous in patterns.
func normalizeHostPattern(host string) (string, error) {
host = strings.TrimSpace(strings.TrimSuffix(host, "."))
if host == "" {
return "", fmt.Errorf("host cannot be empty")
}
if strings.Contains(host, "://") || strings.ContainsAny(host, "/@") {
return "", fmt.Errorf("host must not include url syntax")
}
if strings.Contains(host, ":") {
return "", fmt.Errorf("host must not include ports or ipv6 literals")
}
if addr, err := netip.ParseAddr(host); err == nil {
return addr.Unmap().String(), nil
}
return dnsLookup.ToASCII(host)
}