From dc1f811a812a822418cb6467cee03f8f6cdb9fc1 Mon Sep 17 00:00:00 2001 From: Tobias Gesellchen Date: Sun, 10 May 2026 14:27:29 +0200 Subject: [PATCH] docs(zeroconf): clearer literal-IP error and a Security Considerations note MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Building on the strict literal-IP validator from the previous commit, make the runtime error self-explanatory so anyone tripping on a hostname URL can fix it in one shot: * Errors now lead with the offending zeroconf URL and the rejected host, so wrapping by GetInfo / PushCredentials / pushSimplifiedToken doesn't bury the actual bad value. * The "host must be a literal IP" error suggests two concrete one-liner resolutions (`getent hosts ` and `dig +short `) so the user has a copy-paste fix. * The "host is not on a local network" error names the accepted ranges (loopback / RFC1918 private / link-local v4+v6) so the user knows what they're allowed to pass. docs/guides/SOUNDTOUCH-SERVICE.md gains a bullet under Security Considerations explaining the constraint and the rationale (LAN-resident SSRF surface), so the strict behaviour is documented rather than a surprise. The 17 TestValidateZcBaseURL cases still pass — only the message bodies changed. Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/guides/SOUNDTOUCH-SERVICE.md | 1 + pkg/service/zeroconf/zeroconf.go | 17 ++++++++++++----- 2 files changed, 13 insertions(+), 5 deletions(-) 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,