mirror of
https://github.com/gohugoio/hugo.git
synced 2026-09-03 20:22:38 +00:00
config/security: Harden the default http.urls and resolved address checks (#15285)
The default IP-literal deny rule was case-sensitive, so an uppercase scheme (e.g. HTTP://127.0.0.1/) slipped past it. Make it case-insensitive like the other default rules. CheckAllowedHTTPAddress relied on IsGlobalUnicast/IsPrivate, which admit CGNAT (100.64.0.0/10), TEST-NET, benchmarking, reserved and IPv6 documentation ranges, and NAT64 addresses embedding an internal IPv4 address. Deny those explicitly and unwrap NAT64 before classifying. Thanks to @0xdeadbab3 for finding and reporting this issue. Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
committed by
GitHub
parent
5f0d88b8a3
commit
24d5e42ffa
@@ -61,7 +61,7 @@ var DefaultConfig = Config{
|
||||
// blocks; users who need them can override security.http.urls.
|
||||
URLs: MustNewWhitelist(
|
||||
`(?i)^https?://[a-z0-9]`,
|
||||
`! ^https?://\d+\.`,
|
||||
`! (?i)^https?://\d+\.`,
|
||||
`! (?i)localhost`,
|
||||
`! (?i)^https?://[^/?#]*@`,
|
||||
),
|
||||
@@ -249,13 +249,52 @@ func (c Config) CheckAllowedHTTPAddress(network, address string) error {
|
||||
// is unexpected, so fail closed.
|
||||
return deny(address)
|
||||
}
|
||||
ip = ip.Unmap()
|
||||
if !ip.IsGlobalUnicast() || ip.IsPrivate() {
|
||||
if !isPublicAddr(ip) {
|
||||
return deny(host)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Special-purpose ranges that Go classifies as global unicast and
|
||||
// non-private, but that are never reachable on the public Internet.
|
||||
var nonPublicPrefixes = []netip.Prefix{
|
||||
netip.MustParsePrefix("100.64.0.0/10"), // Shared address space (CGNAT), RFC 6598.
|
||||
netip.MustParsePrefix("192.0.0.0/24"), // IETF protocol assignments.
|
||||
netip.MustParsePrefix("192.0.2.0/24"), // TEST-NET-1.
|
||||
netip.MustParsePrefix("198.18.0.0/15"), // Benchmarking.
|
||||
netip.MustParsePrefix("198.51.100.0/24"), // TEST-NET-2.
|
||||
netip.MustParsePrefix("203.0.113.0/24"), // TEST-NET-3.
|
||||
netip.MustParsePrefix("240.0.0.0/4"), // Reserved.
|
||||
netip.MustParsePrefix("2001:db8::/32"), // Documentation.
|
||||
netip.MustParsePrefix("3fff::/20"), // Documentation.
|
||||
netip.MustParsePrefix("2001:2::/48"), // Benchmarking.
|
||||
}
|
||||
|
||||
// nat64Prefixes embed an IPv4 address in the low 32 bits, RFC 6052/8215.
|
||||
var nat64Prefixes = []netip.Prefix{
|
||||
netip.MustParsePrefix("64:ff9b::/96"),
|
||||
netip.MustParsePrefix("64:ff9b:1::/48"),
|
||||
}
|
||||
|
||||
func isPublicAddr(ip netip.Addr) bool {
|
||||
ip = ip.Unmap()
|
||||
for _, p := range nat64Prefixes {
|
||||
if p.Contains(ip) {
|
||||
b := ip.As16()
|
||||
return isPublicAddr(netip.AddrFrom4([4]byte(b[12:])))
|
||||
}
|
||||
}
|
||||
if !ip.IsGlobalUnicast() || ip.IsPrivate() {
|
||||
return false
|
||||
}
|
||||
for _, p := range nonPublicPrefixes {
|
||||
if p.Contains(ip) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// canonicalIPv4URL rewrites an integer/hex/octal IPv4 host in rawURL to its
|
||||
// canonical dotted-decimal form (inet_aton semantics), returning ok=false when
|
||||
// the host is a normal name or already dotted-decimal.
|
||||
|
||||
@@ -135,7 +135,7 @@ func TestToTOML(t *testing.T) {
|
||||
got := DefaultConfig.ToTOML()
|
||||
|
||||
c.Assert(got, qt.Equals,
|
||||
"[security]\n allowContent = ['! ^text/html$', '! ^text/org$']\n enableInlineShortcodes = false\n\n [security.exec]\n allow = ['^(dart-)?sass$', '^go$', '^git$', '^node$', '^postcss$']\n osEnv = ['(?i)^((HTTPS?|NO)_PROXY|PATH(EXT)?|APPDATA|TE?MP|TERM|GO\\w+|(XDG_CONFIG_)?HOME|USERPROFILE|SSH_AUTH_SOCK|DISPLAY|LANG|SYSTEMDRIVE|PROGRAMDATA)$']\n\n [security.funcs]\n getenv = ['^HUGO_', '^CI$']\n\n [security.http]\n methods = ['(?i)GET|POST']\n urls = ['(?i)^https?://[a-z0-9]', '! ^https?://\\d+\\.', '! (?i)localhost', '! (?i)^https?://[^/?#]*@']\n\n [security.node]\n [security.node.permissions]\n allowAddons = ['tailwindcss']\n allowChildProcess = ['tailwindcss']\n allowRead = ['.']\n allowWorker = ['tailwindcss']\n allowWrite = []\n disable = false",
|
||||
"[security]\n allowContent = ['! ^text/html$', '! ^text/org$']\n enableInlineShortcodes = false\n\n [security.exec]\n allow = ['^(dart-)?sass$', '^go$', '^git$', '^node$', '^postcss$']\n osEnv = ['(?i)^((HTTPS?|NO)_PROXY|PATH(EXT)?|APPDATA|TE?MP|TERM|GO\\w+|(XDG_CONFIG_)?HOME|USERPROFILE|SSH_AUTH_SOCK|DISPLAY|LANG|SYSTEMDRIVE|PROGRAMDATA)$']\n\n [security.funcs]\n getenv = ['^HUGO_', '^CI$']\n\n [security.http]\n methods = ['(?i)GET|POST']\n urls = ['(?i)^https?://[a-z0-9]', '! (?i)^https?://\\d+\\.', '! (?i)localhost', '! (?i)^https?://[^/?#]*@']\n\n [security.node]\n [security.node.permissions]\n allowAddons = ['tailwindcss']\n allowChildProcess = ['tailwindcss']\n allowRead = ['.']\n allowWorker = ['tailwindcss']\n allowWrite = []\n disable = false",
|
||||
)
|
||||
}
|
||||
|
||||
@@ -196,6 +196,8 @@ func TestCheckAllowedHTTPURLHardenedDefaultsIssue14792(t *testing.T) {
|
||||
"http://LOCALHOST:8080/",
|
||||
"http://foo.localhost/",
|
||||
"http://127.0.0.1/",
|
||||
"HTTP://127.0.0.1/", // The deny rules must be case-insensitive.
|
||||
"HtTpS://169.254.169.254/",
|
||||
"http://127.1.2.3:8080/x",
|
||||
"http://user:pass@127.0.0.1/", // userinfo must not sneak past the deny.
|
||||
"http://10.0.0.1/",
|
||||
@@ -257,7 +259,8 @@ func TestCheckAllowedHTTPAddress(t *testing.T) {
|
||||
|
||||
for _, addr := range []string{
|
||||
"93.184.216.34:80",
|
||||
"[2001:db8::1]:443",
|
||||
"[2606:4700::1]:443",
|
||||
"[64:ff9b::5db8:d822]:80", // NAT64-embedded 93.184.216.34.
|
||||
} {
|
||||
c.Assert(pc.CheckAllowedHTTPAddress("tcp", addr), qt.IsNil, qt.Commentf(addr))
|
||||
}
|
||||
@@ -273,6 +276,19 @@ func TestCheckAllowedHTTPAddress(t *testing.T) {
|
||||
"[fc00::1]:80",
|
||||
"0.0.0.0:80",
|
||||
"[::ffff:127.0.0.1]:80", // IPv4-mapped loopback.
|
||||
"100.64.0.1:80", // CGNAT.
|
||||
"[::ffff:100.64.0.1]:80",
|
||||
"192.0.0.9:80",
|
||||
"192.0.2.1:80", // TEST-NET-1.
|
||||
"198.18.0.1:80",
|
||||
"198.51.100.1:80",
|
||||
"203.0.113.1:80",
|
||||
"240.0.0.1:80",
|
||||
"[2001:db8::1]:443",
|
||||
"[3fff::1]:443",
|
||||
"[64:ff9b::7f00:1]:80", // NAT64-embedded 127.0.0.1.
|
||||
"[64:ff9b::a9fe:a9fe]:80", // NAT64-embedded 169.254.169.254.
|
||||
"[64:ff9b:1::a00:1]:80", // NAT64-embedded 10.0.0.1.
|
||||
} {
|
||||
err := pc.CheckAllowedHTTPAddress("tcp", addr)
|
||||
c.Assert(err, qt.IsNotNil, qt.Commentf(addr))
|
||||
|
||||
+1
-1
@@ -2242,7 +2242,7 @@ config:
|
||||
- (?i)GET|POST
|
||||
urls:
|
||||
- (?i)^https?://[a-z0-9]
|
||||
- '! ^https?://\d+\.'
|
||||
- '! (?i)^https?://\d+\.'
|
||||
- '! (?i)localhost'
|
||||
- '! (?i)^https?://[^/?#]*@'
|
||||
node:
|
||||
|
||||
Reference in New Issue
Block a user