diff --git a/docs/guides/SOUNDTOUCH-SERVICE.md b/docs/guides/SOUNDTOUCH-SERVICE.md index f7fde9b..e98bda0 100644 --- a/docs/guides/SOUNDTOUCH-SERVICE.md +++ b/docs/guides/SOUNDTOUCH-SERVICE.md @@ -836,6 +836,7 @@ fi - **SSH Access**: Migration requires SSH access to devices. Ensure your network security policies allow this. - **Proxy Logging**: Disable `REDACT_PROXY_LOGS` only in development environments. - **Data Protection**: The data directory contains device configurations and usage patterns. Secure appropriately. +- **Spotify / Amazon Music credential push (zeroconf)**: outbound credential-push requests are restricted to literal IP hosts on local-network ranges (loopback, RFC1918 private, IPv4/IPv6 link-local). Hostname-style URLs (DNS, mDNS `*.local`) are rejected at runtime; if you have a hostname, resolve it first (`getent hosts ` or `dig +short `) and pass the resolved IP. This guards against a malicious LAN-resident speaker pointing the credential push at a non-speaker host (server-side request forgery). ## Performance Tuning diff --git a/pkg/service/zeroconf/zeroconf.go b/pkg/service/zeroconf/zeroconf.go index 056d2ea..1dc2b80 100644 --- a/pkg/service/zeroconf/zeroconf.go +++ b/pkg/service/zeroconf/zeroconf.go @@ -192,25 +192,32 @@ func DecryptBlob(encKey, macKey, blob []byte) ([]byte, error) { func validateZcBaseURL(zcBaseURL string) (*url.URL, error) { u, err := url.Parse(zcBaseURL) if err != nil { - return nil, fmt.Errorf("parse: %w", err) + return nil, fmt.Errorf("zeroconf URL %q: parse: %w", zcBaseURL, err) } if u.Scheme != "http" && u.Scheme != "https" { - return nil, fmt.Errorf("scheme %q not allowed (expected http or https)", u.Scheme) + return nil, fmt.Errorf("zeroconf URL %q: scheme %q not allowed — must be http or https", zcBaseURL, u.Scheme) } host := u.Hostname() if host == "" { - return nil, fmt.Errorf("missing host") + return nil, fmt.Errorf("zeroconf URL %q: missing host", zcBaseURL) } ip := net.ParseIP(host) if ip == nil { - return nil, fmt.Errorf("host %q must be a literal IP (DNS/mDNS hostnames are not supported on this path; resolve to a private IP before calling)", host) + return nil, fmt.Errorf( + "zeroconf URL %q: host %q must be a literal IP — resolve the hostname to a private-network IP first "+ + "(e.g. `getent hosts %s` or `dig +short %s`) and retry with the resolved address", + zcBaseURL, host, host, host) } if !ip.IsLoopback() && !ip.IsPrivate() && !ip.IsLinkLocalUnicast() { - return nil, fmt.Errorf("host %q is not on a local network", host) + return nil, fmt.Errorf( + "zeroconf URL %q: host %q is not on a local network — only loopback (127.0.0.0/8, ::1), "+ + "RFC1918 private (10/8, 172.16/12, 192.168/16) and link-local (169.254/16, fe80::/10) "+ + "addresses are accepted", + zcBaseURL, host) } // Build a fresh URL from validated components only — the IP literal,