mirror of
https://github.com/navidrome/navidrome.git
synced 2026-10-08 02:17:25 +02:00
The dial-time SSRF guard runs on the resolved IP, so the tests that prove a symbolic hostname cannot reach loopback used "localhost." — a trailing dot never matches /etc/hosts, so Go queries real DNS. Machines whose resolver does not answer "localhost." (a VPN DNS, for example) got "no such host" before the dial guard ever ran, failing three specs. Add tests.StubResolver, a net.Resolver backed by an in-memory DNS responder over net.Pipe, and let the plugin dialers take a resolver so tests can inject it. Name resolution in those specs no longer depends on the machine's DNS.
58 lines
1.5 KiB
Go
58 lines
1.5 KiB
Go
package plugins
|
|
|
|
import (
|
|
"fmt"
|
|
"net"
|
|
"slices"
|
|
|
|
"github.com/navidrome/navidrome/utils/netguard"
|
|
)
|
|
|
|
// dialResolver is nil in production (the system resolver); tests swap in a stub to avoid real DNS.
|
|
var dialResolver *net.Resolver
|
|
|
|
// checkPrivateDial runs at dial time on the resolved IP, so hostnames can't reach private addresses unless a
|
|
// literal IP/CIDR entry or a bare "*" (plugins targeting user-configured LAN services) allows it.
|
|
func checkPrivateDial(requiredHosts []string, address string) error {
|
|
if slices.Contains(requiredHosts, "*") {
|
|
return nil
|
|
}
|
|
host, _, err := net.SplitHostPort(address)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
ip := net.ParseIP(host)
|
|
if ip == nil || !netguard.IsPrivateIP(ip) {
|
|
return nil
|
|
}
|
|
for _, entry := range requiredHosts {
|
|
if ipMatchesEntry(entry, ip) {
|
|
return nil
|
|
}
|
|
}
|
|
return fmt.Errorf("dial to private/loopback address %q blocked: requires an explicit IP or CIDR in requiredHosts", address)
|
|
}
|
|
|
|
func isHostInAllowlist(requiredHosts []string, hostname string) bool {
|
|
ip := net.ParseIP(hostname)
|
|
for _, pattern := range requiredHosts {
|
|
if matchHostPattern(pattern, hostname) {
|
|
return true
|
|
}
|
|
if ip != nil && ipMatchesEntry(pattern, ip) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// ipMatchesEntry reports whether a requiredHosts entry is a literal IP or CIDR that covers ip.
|
|
func ipMatchesEntry(entry string, ip net.IP) bool {
|
|
if _, cidr, err := net.ParseCIDR(entry); err == nil {
|
|
return cidr.Contains(ip)
|
|
}
|
|
if entryIP := net.ParseIP(entry); entryIP != nil {
|
|
return entryIP.Equal(ip)
|
|
}
|
|
return false
|
|
}
|