mirror of
https://github.com/gesellix/Bose-SoundTouch.git
synced 2026-08-18 00:26:29 +00:00
sec5e: sanitize log-injection in client, discovery, testutils, cmd
Fixes CodeQL go/log-injection alerts in the final batch of packages. New logutil.go helpers: pkg/client, pkg/testutils/amazon, pkg/testutils/spotify, cmd/soundtouch-service, cmd/soundtouch-web, cmd/dummy-speaker, cmd/mdns-scanner. pkg/discovery/logger.go: added sanitizeLog and a nil-safe remoteAddrString helper to the existing file (alongside logVerbose). Call sites wrapped across 11 files — device IDs, source types, hostnames, IPs, interface names, URLs, service names, HTTP method/form values, WebSocket URLs and payloads, TLS SNI names, remote addresses. No behaviour change. golangci-lint and make check pass. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
3d8e08d11a
commit
dc8ec69c61
@@ -0,0 +1,13 @@
|
||||
package main
|
||||
|
||||
import "strings"
|
||||
|
||||
// sanitizeLog strips newline characters from s to prevent log-injection
|
||||
// (CodeQL go/log-injection). Values from speakers, HTTP requests, and
|
||||
// external APIs may contain attacker-controlled newlines.
|
||||
func sanitizeLog(s string) string {
|
||||
s = strings.ReplaceAll(s, "\n", `\n`)
|
||||
s = strings.ReplaceAll(s, "\r", `\r`)
|
||||
|
||||
return s
|
||||
}
|
||||
@@ -43,10 +43,10 @@ func main() {
|
||||
log.Fatalf("start fake speaker: %v", err)
|
||||
}
|
||||
|
||||
log.Printf("fake speaker HTTP listening on http://%s", s.HTTPAddr())
|
||||
log.Printf("fake speaker HTTP listening on http://%s", sanitizeLog(s.HTTPAddr()))
|
||||
|
||||
if addr := s.TelnetAddr(); addr != "" {
|
||||
log.Printf("fake speaker telnet listening on tcp://%s", addr)
|
||||
log.Printf("fake speaker telnet listening on tcp://%s", sanitizeLog(addr))
|
||||
}
|
||||
|
||||
if *register != "" {
|
||||
@@ -58,7 +58,7 @@ func main() {
|
||||
if err := registerWithService(*register, target); err != nil {
|
||||
log.Printf("self-register failed: %v (continuing anyway)", err)
|
||||
} else {
|
||||
log.Printf("registered %s with service at %s", target, *register)
|
||||
log.Printf("registered %s with service at %s", sanitizeLog(target), sanitizeLog(*register))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
package main
|
||||
|
||||
import "strings"
|
||||
|
||||
// sanitizeLog strips newline characters from s to prevent log-injection
|
||||
// (CodeQL go/log-injection). Values from speakers, HTTP requests, and
|
||||
// external APIs may contain attacker-controlled newlines.
|
||||
func sanitizeLog(s string) string {
|
||||
s = strings.ReplaceAll(s, "\n", `\n`)
|
||||
s = strings.ReplaceAll(s, "\r", `\r`)
|
||||
|
||||
return s
|
||||
}
|
||||
@@ -116,7 +116,7 @@ func main() {
|
||||
defer close(entries)
|
||||
|
||||
if *verbose {
|
||||
log.Printf("mDNS: Starting scan for service '%s' with timeout %v", *service, *timeout)
|
||||
log.Printf("mDNS: Starting scan for service '%s' with timeout %v", sanitizeLog(*service), *timeout)
|
||||
}
|
||||
|
||||
// Query for services
|
||||
@@ -196,7 +196,7 @@ func parseServiceEntry(entry *mdns.ServiceEntry, verbose bool) *ServiceInfo {
|
||||
|
||||
if verbose {
|
||||
log.Printf("mDNS: Received service entry: Name='%s', Host='%s', Port=%d, AddrV4=%v, AddrV6=%v",
|
||||
entry.Name, entry.Host, entry.Port, entry.AddrV4, entry.AddrV6)
|
||||
sanitizeLog(entry.Name), sanitizeLog(entry.Host), entry.Port, entry.AddrV4, entry.AddrV6)
|
||||
}
|
||||
|
||||
service := &ServiceInfo{
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
package main
|
||||
|
||||
import "strings"
|
||||
|
||||
// sanitizeLog strips newline characters from s to prevent log-injection
|
||||
// (CodeQL go/log-injection). Values from speakers, HTTP requests, and
|
||||
// external APIs may contain attacker-controlled newlines.
|
||||
func sanitizeLog(s string) string {
|
||||
s = strings.ReplaceAll(s, "\n", `\n`)
|
||||
s = strings.ReplaceAll(s, "\r", `\r`)
|
||||
|
||||
return s
|
||||
}
|
||||
@@ -71,7 +71,7 @@ func initializeDefaultSources(ds *datastore.DataStore) {
|
||||
for i := range allDevices {
|
||||
dev := &allDevices[i]
|
||||
if sources, errGet := ds.GetConfiguredSources(dev.AccountID, dev.DeviceID); errGet == nil {
|
||||
log.Printf("Initializing default Sources.xml for existing device %s", dev.DeviceID)
|
||||
log.Printf("Initializing default Sources.xml for existing device %s", sanitizeLog(dev.DeviceID))
|
||||
|
||||
// Find default sources and merge them if missing or outdated tokens.
|
||||
// claimed tracks which stored sources have already been matched by a default,
|
||||
@@ -103,13 +103,13 @@ func initializeDefaultSources(ds *datastore.DataStore) {
|
||||
claimed[foundIdx] = true
|
||||
|
||||
if sources[foundIdx].Secret == "" && def.Secret != "" {
|
||||
log.Printf("Initializing missing token for source %s on device %s", def.SourceKeyType, dev.DeviceID)
|
||||
log.Printf("Initializing missing token for source %s on device %s", sanitizeLog(def.SourceKeyType), sanitizeLog(dev.DeviceID))
|
||||
sources[foundIdx].Secret = def.Secret
|
||||
sources[foundIdx].SecretType = def.SecretType
|
||||
modified = true
|
||||
}
|
||||
} else {
|
||||
log.Printf("Adding missing default source %s (providerID=%s) to device %s", def.SourceKeyType, def.SourceProviderID, dev.DeviceID)
|
||||
log.Printf("Adding missing default source %s (providerID=%s) to device %s", sanitizeLog(def.SourceKeyType), sanitizeLog(def.SourceProviderID), sanitizeLog(dev.DeviceID))
|
||||
sources = append(sources, def)
|
||||
modified = true
|
||||
}
|
||||
@@ -117,7 +117,7 @@ func initializeDefaultSources(ds *datastore.DataStore) {
|
||||
|
||||
if modified {
|
||||
if errSave := ds.SaveConfiguredSources(dev.AccountID, dev.DeviceID, sources); errSave != nil {
|
||||
log.Printf("Failed to save updated sources for %s: %v", dev.DeviceID, errSave)
|
||||
log.Printf("Failed to save updated sources for %s: %v", sanitizeLog(dev.DeviceID), errSave)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -147,7 +147,7 @@ func initMusicServices(config serviceConfig, server *handlers.Server) {
|
||||
clientIDPrefix = clientIDPrefix[:8]
|
||||
}
|
||||
|
||||
log.Printf("Spotify service initialized (client ID: %s...)", clientIDPrefix)
|
||||
log.Printf("Spotify service initialized (client ID: %s...)", sanitizeLog(clientIDPrefix))
|
||||
}
|
||||
|
||||
if config.amazonClientID != "" {
|
||||
@@ -172,7 +172,7 @@ func initMusicServices(config serviceConfig, server *handlers.Server) {
|
||||
clientIDPrefix = clientIDPrefix[:8]
|
||||
}
|
||||
|
||||
log.Printf("Amazon Music service initialized (client ID: %s...)", clientIDPrefix)
|
||||
log.Printf("Amazon Music service initialized (client ID: %s...)", sanitizeLog(clientIDPrefix))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -188,7 +188,7 @@ func logBufferCapacityFromEnv(defaultCap int) int {
|
||||
|
||||
v, err := strconv.Atoi(raw)
|
||||
if err != nil {
|
||||
log.Printf("[Logs] Invalid SOUNDTOUCH_LOG_BUFFER_LINES=%q, using default %d", raw, defaultCap)
|
||||
log.Printf("[Logs] Invalid SOUNDTOUCH_LOG_BUFFER_LINES=%q, using default %d", sanitizeLog(raw), defaultCap)
|
||||
return defaultCap
|
||||
}
|
||||
|
||||
@@ -415,7 +415,7 @@ func main() {
|
||||
persisted := applyPersistedSettings(ds, &config)
|
||||
|
||||
if persisted.ServerURL == "" {
|
||||
log.Printf("Creating default settings.json in %s", config.dataDir)
|
||||
log.Printf("Creating default settings.json in %s", sanitizeLog(config.dataDir))
|
||||
persisted = createDefaultSettings(ds, config)
|
||||
}
|
||||
|
||||
@@ -468,7 +468,7 @@ func main() {
|
||||
server.SetShortcuts(persisted.Shortcuts)
|
||||
|
||||
for path, status := range persisted.Shortcuts {
|
||||
log.Printf("Warning: configured shortcut: %s -> %d", path, status)
|
||||
log.Printf("Warning: configured shortcut: %s -> %d", sanitizeLog(path), status)
|
||||
}
|
||||
|
||||
recorder := proxy.NewRecorder(config.dataDir)
|
||||
@@ -477,11 +477,11 @@ func main() {
|
||||
|
||||
patterns, err := proxy.LoadPatterns(patternsPath)
|
||||
if err != nil {
|
||||
log.Printf("Warning: Failed to load patterns from %s: %v", patternsPath, err)
|
||||
log.Printf("Warning: Failed to load patterns from %s: %v", sanitizeLog(patternsPath), err)
|
||||
}
|
||||
|
||||
if len(patterns) == 0 {
|
||||
log.Printf("Creating default patterns at %s", patternsPath)
|
||||
log.Printf("Creating default patterns at %s", sanitizeLog(patternsPath))
|
||||
|
||||
patterns = proxy.DefaultPatterns()
|
||||
|
||||
@@ -512,17 +512,17 @@ func main() {
|
||||
} else {
|
||||
stockholmHandler = sh
|
||||
|
||||
log.Printf("Stockholm frontend enabled from %s", config.stockholmDir)
|
||||
log.Printf("Stockholm frontend enabled from %s", sanitizeLog(config.stockholmDir))
|
||||
}
|
||||
}
|
||||
|
||||
r := setupRouter(server, stockholmHandler)
|
||||
|
||||
log.Printf("Go service starting on %s", config.serverURL)
|
||||
log.Printf("Go service starting on %s", sanitizeLog(config.serverURL))
|
||||
|
||||
// TLS cert generation can be slow on constrained hardware; run it in the
|
||||
// background so the HTTP server is available immediately.
|
||||
log.Printf("HTTPS setup running in background; %s will be available shortly", config.httpsServerURL)
|
||||
log.Printf("HTTPS setup running in background; %s will be available shortly", sanitizeLog(config.httpsServerURL))
|
||||
|
||||
go func() {
|
||||
tlsConfig, err := cm.GetServerTLSConfig(config.domains)
|
||||
@@ -652,7 +652,7 @@ func loadConfig(c *cli.Context) serviceConfig {
|
||||
|
||||
discoveryInterval, err := time.ParseDuration(discoveryIntervalStr)
|
||||
if err != nil {
|
||||
log.Printf("Warning: Failed to parse discovery interval %s, using default 5m: %v", discoveryIntervalStr, err)
|
||||
log.Printf("Warning: Failed to parse discovery interval %s, using default 5m: %v", sanitizeLog(discoveryIntervalStr), err)
|
||||
|
||||
discoveryInterval = 5 * time.Minute
|
||||
}
|
||||
@@ -1294,7 +1294,7 @@ func startHTTPSServer(httpsAddr string, r http.Handler, tlsConfig *tls.Config, h
|
||||
return &tlsConfig.Certificates[0], nil
|
||||
}
|
||||
|
||||
log.Printf("[TLS] ❌ No certificate available for %s", clientHello.ServerName)
|
||||
log.Printf("[TLS] ❌ No certificate available for %s", sanitizeLog(clientHello.ServerName))
|
||||
|
||||
return nil, fmt.Errorf("no certificate available for %s", clientHello.ServerName)
|
||||
}
|
||||
@@ -1306,7 +1306,7 @@ func startHTTPSServer(httpsAddr string, r http.Handler, tlsConfig *tls.Config, h
|
||||
ErrorLog: log.Default(), // Ensure error logging is enabled
|
||||
}
|
||||
|
||||
log.Printf("Go service starting HTTPS on %s", httpsServerURL)
|
||||
log.Printf("Go service starting HTTPS on %s", sanitizeLog(httpsServerURL))
|
||||
|
||||
go func() {
|
||||
listener, err := net.Listen("tcp", httpsAddr)
|
||||
@@ -1361,9 +1361,9 @@ func runHTTPSPreflight(httpsServerURL, serverURL string, dnsEnabled bool, resolv
|
||||
case res.Skipped:
|
||||
// Listener already on :443 — nothing to say.
|
||||
case res.NotApplicable:
|
||||
log.Printf("HTTPS pre-flight: :443 check skipped — %s", res.Reason)
|
||||
log.Printf("HTTPS pre-flight: :443 check skipped — %s", sanitizeLog(res.Reason))
|
||||
default:
|
||||
log.Printf("HTTPS pre-flight: :443 reachable at localhost and %s ✓", res.LANHost)
|
||||
log.Printf("HTTPS pre-flight: :443 reachable at localhost and %s ✓", sanitizeLog(res.LANHost))
|
||||
}
|
||||
|
||||
return
|
||||
@@ -1438,7 +1438,7 @@ func (c *loggingTLSConn) Read(b []byte) (n int, err error) {
|
||||
if strings.Contains(err.Error(), "tls:") ||
|
||||
strings.Contains(err.Error(), "handshake") ||
|
||||
strings.Contains(err.Error(), "certificate") {
|
||||
log.Printf("[TLS] ❌ Handshake failed from %s: %v", c.addr, err)
|
||||
log.Printf("[TLS] ❌ Handshake failed from %s: %v", sanitizeLog(c.addr.String()), err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
package main
|
||||
|
||||
import "strings"
|
||||
|
||||
// sanitizeLog strips newline characters from s to prevent log-injection
|
||||
// (CodeQL go/log-injection). Values from speakers, HTTP requests, and
|
||||
// external APIs may contain attacker-controlled newlines.
|
||||
func sanitizeLog(s string) string {
|
||||
s = strings.ReplaceAll(s, "\n", `\n`)
|
||||
s = strings.ReplaceAll(s, "\r", `\r`)
|
||||
|
||||
return s
|
||||
}
|
||||
@@ -86,7 +86,7 @@ func main() {
|
||||
}
|
||||
|
||||
if rawBind != "" && bindAddr != rawBind {
|
||||
log.Printf("Resolved --bind %q to %s", rawBind, bindAddr)
|
||||
log.Printf("Resolved --bind %q to %s", sanitizeLog(rawBind), sanitizeLog(bindAddr))
|
||||
}
|
||||
|
||||
rawIface := c.String("interface")
|
||||
@@ -94,7 +94,7 @@ func main() {
|
||||
|
||||
ifaceName := defaultDiscoveryInterface(rawIface, rawBind, bindAddr)
|
||||
if rawIface == "" && ifaceName != "" {
|
||||
log.Printf("Defaulting --interface to %q from --bind", ifaceName)
|
||||
log.Printf("Defaulting --interface to %q from --bind", sanitizeLog(ifaceName))
|
||||
}
|
||||
|
||||
addr := ":" + port
|
||||
@@ -131,7 +131,7 @@ func main() {
|
||||
r := chi.NewRouter()
|
||||
webApp.Mount(r, discoveryService)
|
||||
|
||||
log.Printf("AfterTouch Web UI starting on http://%s", addr)
|
||||
log.Printf("AfterTouch Web UI starting on http://%s", sanitizeLog(addr))
|
||||
|
||||
return http.ListenAndServe(addr, r)
|
||||
},
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
package client
|
||||
|
||||
import "strings"
|
||||
|
||||
// sanitizeLog strips newline characters from s to prevent log-injection
|
||||
// (CodeQL go/log-injection). Values from speakers, HTTP requests, and
|
||||
// external APIs may contain attacker-controlled newlines.
|
||||
func sanitizeLog(s string) string {
|
||||
s = strings.ReplaceAll(s, "\n", `\n`)
|
||||
s = strings.ReplaceAll(s, "\r", `\r`)
|
||||
|
||||
return s
|
||||
}
|
||||
@@ -218,7 +218,7 @@ func (ws *WebSocketClient) connectWithConfig(config *WebSocketConfig) error {
|
||||
Path: "/",
|
||||
}
|
||||
|
||||
ws.logger.Printf("Connecting to %s", wsURL.String())
|
||||
ws.logger.Printf("Connecting to %s", sanitizeLog(wsURL.String()))
|
||||
|
||||
// Create dialer with custom buffer sizes and "gabbo" protocol
|
||||
dialer := websocket.Dialer{
|
||||
@@ -245,7 +245,7 @@ func (ws *WebSocketClient) connectWithConfig(config *WebSocketConfig) error {
|
||||
go ws.readLoop(config)
|
||||
go ws.pingLoop(config)
|
||||
|
||||
ws.logger.Printf("Connected to %s", wsURL.String())
|
||||
ws.logger.Printf("Connected to %s", sanitizeLog(wsURL.String()))
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -443,7 +443,7 @@ func (ws *WebSocketClient) handleSpecialMessage(data []byte) {
|
||||
|
||||
if err != nil {
|
||||
ws.logger.Printf("Unknown special message type: %v", err)
|
||||
ws.logger.Printf("Raw message: %s", string(data))
|
||||
ws.logger.Printf("Raw message: %s", sanitizeLog(string(data)))
|
||||
|
||||
return
|
||||
}
|
||||
@@ -589,7 +589,7 @@ func (ws *WebSocketClient) PairWithAccount(accountID, userAuthToken string) erro
|
||||
return fmt.Errorf("failed to marshal pairing request: %w", err)
|
||||
}
|
||||
|
||||
ws.logger.Printf("Sending PairDeviceWithAccount for account %s", accountID)
|
||||
ws.logger.Printf("Sending PairDeviceWithAccount for account %s", sanitizeLog(accountID))
|
||||
|
||||
return ws.SendMessage(data)
|
||||
}
|
||||
|
||||
+14
-14
@@ -69,7 +69,7 @@ type DiscoveredHost struct {
|
||||
func NewDNSDiscovery(upstreamDNS []string, serviceIP, serverURL string) *DNSDiscovery {
|
||||
derived := DeriveOAuthHostnames(serverURL)
|
||||
if len(derived) > 0 {
|
||||
log.Printf("[DNS] Auto-hijacking OAuth subdomains derived from serverURL %q: %s", serverURL, strings.Join(derived, ", "))
|
||||
log.Printf("[DNS] Auto-hijacking OAuth subdomains derived from serverURL %q: %s", sanitizeLog(serverURL), strings.Join(derived, ", "))
|
||||
}
|
||||
|
||||
return &DNSDiscovery{
|
||||
@@ -207,7 +207,7 @@ func (d *DNSDiscovery) recordQuery(hostname string, isIntercepted bool, remoteAd
|
||||
d.discovered[hostname] = host
|
||||
|
||||
log.Printf("[NEW DISCOVERY] %s (Bose: %v, Intercepted: %v)",
|
||||
hostname, host.IsBoseService, host.IsIntercepted)
|
||||
sanitizeLog(hostname), host.IsBoseService, host.IsIntercepted)
|
||||
|
||||
if d.onNewDiscovery != nil {
|
||||
go d.onNewDiscovery(hostname)
|
||||
@@ -276,7 +276,7 @@ func (d *DNSDiscovery) respondWithIP(w dns.ResponseWriter, r *dns.Msg, ip string
|
||||
m.RecursionAvailable = true
|
||||
|
||||
q := r.Question[0]
|
||||
log.Printf("[DNS] Intercepted query for %s (type %d) from %s", q.Name, q.Qtype, w.RemoteAddr())
|
||||
log.Printf("[DNS] Intercepted query for %s (type %d) from %s", sanitizeLog(q.Name), q.Qtype, sanitizeLog(remoteAddrString(w)))
|
||||
|
||||
resolvedIP := ip
|
||||
if net.ParseIP(ip) == nil {
|
||||
@@ -312,9 +312,9 @@ func (d *DNSDiscovery) respondWithIP(w dns.ResponseWriter, r *dns.Msg, ip string
|
||||
if err == nil {
|
||||
m.Answer = append(m.Answer, rr)
|
||||
|
||||
log.Printf("[DNS] Returning CNAME record %s -> %s", q.Name, target)
|
||||
log.Printf("[DNS] Returning CNAME record %s -> %s", sanitizeLog(q.Name), sanitizeLog(target))
|
||||
} else {
|
||||
log.Printf("[DNS] Error creating CNAME fallback for %s: %v", target, err)
|
||||
log.Printf("[DNS] Error creating CNAME fallback for %s: %v", sanitizeLog(target), err)
|
||||
|
||||
m.Rcode = dns.RcodeServerFailure
|
||||
}
|
||||
@@ -326,9 +326,9 @@ func (d *DNSDiscovery) respondWithIP(w dns.ResponseWriter, r *dns.Msg, ip string
|
||||
if err == nil {
|
||||
m.Answer = append(m.Answer, rr)
|
||||
|
||||
log.Printf("[DNS] Returning A record %s -> %s", q.Name, resolvedIP)
|
||||
log.Printf("[DNS] Returning A record %s -> %s", sanitizeLog(q.Name), sanitizeLog(resolvedIP))
|
||||
} else {
|
||||
log.Printf("[DNS] Error creating A record for %s: %v", resolvedIP, err)
|
||||
log.Printf("[DNS] Error creating A record for %s: %v", sanitizeLog(resolvedIP), err)
|
||||
|
||||
m.Rcode = dns.RcodeServerFailure
|
||||
}
|
||||
@@ -340,15 +340,15 @@ func (d *DNSDiscovery) respondWithIP(w dns.ResponseWriter, r *dns.Msg, ip string
|
||||
if err == nil {
|
||||
m.Answer = append(m.Answer, rr)
|
||||
|
||||
log.Printf("[DNS] Returning AAAA record %s -> %s", q.Name, resolvedIP)
|
||||
log.Printf("[DNS] Returning AAAA record %s -> %s", sanitizeLog(q.Name), sanitizeLog(resolvedIP))
|
||||
} else {
|
||||
log.Printf("[DNS] Error creating AAAA record for %s: %v", resolvedIP, err)
|
||||
log.Printf("[DNS] Error creating AAAA record for %s: %v", sanitizeLog(resolvedIP), err)
|
||||
|
||||
m.Rcode = dns.RcodeServerFailure
|
||||
}
|
||||
} else {
|
||||
// Explicitly return SUCCESS with no data for AAAA to prevent fallback issues if no IPv6
|
||||
log.Printf("[DNS] Returning empty AAAA success (NODATA) for %s", q.Name)
|
||||
log.Printf("[DNS] Returning empty AAAA success (NODATA) for %s", sanitizeLog(q.Name))
|
||||
}
|
||||
default:
|
||||
log.Printf("[DNS] Returning empty success for type %d", q.Qtype)
|
||||
@@ -398,7 +398,7 @@ func (d *DNSDiscovery) forward(w dns.ResponseWriter, r *dns.Msg) {
|
||||
if err == nil {
|
||||
if in.Rcode == dns.RcodeSuccess {
|
||||
if writeErr := w.WriteMsg(in); writeErr != nil {
|
||||
log.Printf("[DNS ERROR] Failed to write forwarded response from %s: %v", upstream, writeErr)
|
||||
log.Printf("[DNS ERROR] Failed to write forwarded response from %s: %v", sanitizeLog(upstream), writeErr)
|
||||
}
|
||||
|
||||
return
|
||||
@@ -485,7 +485,7 @@ func (d *DNSDiscovery) Start(addr string) error {
|
||||
errChan := make(chan error, 2)
|
||||
|
||||
go func() {
|
||||
log.Printf("[DNS] UDP Discovery server starting on %s", addr)
|
||||
log.Printf("[DNS] UDP Discovery server starting on %s", sanitizeLog(addr))
|
||||
|
||||
if err := udpServer.ListenAndServe(); err != nil {
|
||||
errChan <- fmt.Errorf("UDP server failed: %w", err)
|
||||
@@ -493,14 +493,14 @@ func (d *DNSDiscovery) Start(addr string) error {
|
||||
}()
|
||||
|
||||
go func() {
|
||||
log.Printf("[DNS] TCP Discovery server starting on %s", addr)
|
||||
log.Printf("[DNS] TCP Discovery server starting on %s", sanitizeLog(addr))
|
||||
|
||||
if err := tcpServer.ListenAndServe(); err != nil {
|
||||
errChan <- fmt.Errorf("TCP server failed: %w", err)
|
||||
}
|
||||
}()
|
||||
|
||||
log.Printf("[DNS] Discovery servers starting on %s (upstream: %s, intercept IP: %s)", addr, d.upstreamDNS, d.serviceIP)
|
||||
log.Printf("[DNS] Discovery servers starting on %s (upstream: %s, intercept IP: %s)", sanitizeLog(addr), d.upstreamDNS, sanitizeLog(d.serviceIP))
|
||||
|
||||
// Wait for first error
|
||||
return <-errChan
|
||||
|
||||
@@ -2,6 +2,8 @@ package discovery
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
@@ -38,3 +40,24 @@ func logVerbose(format string, args ...any) {
|
||||
log.Printf(format, args...)
|
||||
}
|
||||
}
|
||||
|
||||
// sanitizeLog strips newline characters from s to prevent log-injection
|
||||
// (CodeQL go/log-injection). Values from speakers, HTTP requests, and
|
||||
// external APIs may contain attacker-controlled newlines.
|
||||
func sanitizeLog(s string) string {
|
||||
s = strings.ReplaceAll(s, "\n", `\n`)
|
||||
s = strings.ReplaceAll(s, "\r", `\r`)
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
// remoteAddrString safely converts a net.Addr to a string, returning
|
||||
// an empty string when addr is nil (dns.ResponseWriter.RemoteAddr may
|
||||
// return nil in unit-test contexts).
|
||||
func remoteAddrString(w interface{ RemoteAddr() net.Addr }) string {
|
||||
if ra := w.RemoteAddr(); ra != nil {
|
||||
return ra.String()
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -143,7 +143,7 @@ func (m *MDNSDiscoveryService) queryService(service string, entries chan<- *mdns
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("mDNS: Query '%s' (IPv4) failed: %v — falling back to dual-stack", service, err)
|
||||
log.Printf("mDNS: Query '%s' (IPv4) failed: %v — falling back to dual-stack", sanitizeLog(service), err)
|
||||
|
||||
err = mdns.Query(&mdns.QueryParam{
|
||||
Service: service,
|
||||
@@ -152,7 +152,7 @@ func (m *MDNSDiscoveryService) queryService(service string, entries chan<- *mdns
|
||||
Entries: entries,
|
||||
})
|
||||
if err != nil {
|
||||
log.Printf("mDNS: Query '%s' (dual-stack) failed: %v", service, err)
|
||||
log.Printf("mDNS: Query '%s' (dual-stack) failed: %v", sanitizeLog(service), err)
|
||||
} else {
|
||||
logVerbose("mDNS: Query '%s' (dual-stack) completed successfully", service)
|
||||
}
|
||||
@@ -188,7 +188,7 @@ func (m *MDNSDiscoveryService) serviceEntryToDevice(entry *mdns.ServiceEntry) *m
|
||||
|
||||
ips, err := net.LookupIP(entry.Host)
|
||||
if err != nil || len(ips) == 0 {
|
||||
log.Printf("mDNS: Failed to resolve hostname '%s': %v", entry.Host, err)
|
||||
log.Printf("mDNS: Failed to resolve hostname '%s': %v", sanitizeLog(entry.Host), err)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -268,12 +268,12 @@ func (m *MDNSDiscoveryService) getIPv4Interface() *net.Interface {
|
||||
if m.ifaceName != "" {
|
||||
iface, err := net.InterfaceByName(m.ifaceName)
|
||||
if err != nil {
|
||||
log.Printf("mDNS: Configured interface %q not found: %v", m.ifaceName, err)
|
||||
log.Printf("mDNS: Configured interface %q not found: %v", sanitizeLog(m.ifaceName), err)
|
||||
return nil
|
||||
}
|
||||
|
||||
if !interfaceHasIPv4(iface) {
|
||||
log.Printf("mDNS: Configured interface %q has no usable IPv4 address", m.ifaceName)
|
||||
log.Printf("mDNS: Configured interface %q has no usable IPv4 address", sanitizeLog(m.ifaceName))
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
+14
-14
@@ -185,7 +185,7 @@ func (d *Service) PerformDiscovery(ctx context.Context) ([]*models.DiscoveredDev
|
||||
log.Printf("UPnP: Discovery completed. Processed %d responses, found %d unique devices", responseCount, len(result))
|
||||
|
||||
for i, device := range result {
|
||||
log.Printf("UPnP: Device #%d: %s at %s:%d (UPnP Location: %s)", i+1, device.Name, device.Host, device.Port, device.UPnPLocation)
|
||||
log.Printf("UPnP: Device #%d: %s at %s:%d (UPnP Location: %s)", i+1, sanitizeLog(device.Name), sanitizeLog(device.Host), device.Port, sanitizeLog(device.UPnPLocation))
|
||||
}
|
||||
|
||||
return result, nil
|
||||
@@ -220,7 +220,7 @@ func (d *Service) setupUDPListener() (*net.UDPConn, error) {
|
||||
// M-SEARCH leaves through the right NIC on multi-homed hosts.
|
||||
if iface != nil {
|
||||
if err := ipv4.NewPacketConn(listener).SetMulticastInterface(iface); err != nil {
|
||||
log.Printf("UPnP: Failed to set multicast interface to %q: %v", iface.Name, err)
|
||||
log.Printf("UPnP: Failed to set multicast interface to %q: %v", sanitizeLog(iface.Name), err)
|
||||
// Continue regardless — the kernel will fall back to its own routing decision.
|
||||
}
|
||||
}
|
||||
@@ -241,13 +241,13 @@ func (d *Service) resolveListenInterface() (net.IP, *net.Interface, error) {
|
||||
|
||||
iface, err := net.InterfaceByName(d.ifaceName)
|
||||
if err != nil {
|
||||
log.Printf("UPnP: Configured interface %q not found: %v", d.ifaceName, err)
|
||||
log.Printf("UPnP: Configured interface %q not found: %v", sanitizeLog(d.ifaceName), err)
|
||||
return nil, nil, fmt.Errorf("configured interface %q not found: %w", d.ifaceName, err)
|
||||
}
|
||||
|
||||
addrs, err := iface.Addrs()
|
||||
if err != nil {
|
||||
log.Printf("UPnP: Failed to read addresses for interface %q: %v", d.ifaceName, err)
|
||||
log.Printf("UPnP: Failed to read addresses for interface %q: %v", sanitizeLog(d.ifaceName), err)
|
||||
return nil, nil, fmt.Errorf("read addresses for interface %q: %w", d.ifaceName, err)
|
||||
}
|
||||
|
||||
@@ -262,12 +262,12 @@ func (d *Service) resolveListenInterface() (net.IP, *net.Interface, error) {
|
||||
continue
|
||||
}
|
||||
|
||||
log.Printf("UPnP: Binding UDP listener to interface %q (%s)", iface.Name, ipv4Addr)
|
||||
log.Printf("UPnP: Binding UDP listener to interface %q (%s)", sanitizeLog(iface.Name), sanitizeLog(ipv4Addr.String()))
|
||||
|
||||
return ipv4Addr, iface, nil
|
||||
}
|
||||
|
||||
log.Printf("UPnP: Configured interface %q has no usable IPv4 address", d.ifaceName)
|
||||
log.Printf("UPnP: Configured interface %q has no usable IPv4 address", sanitizeLog(d.ifaceName))
|
||||
|
||||
return nil, nil, fmt.Errorf("interface %q has no usable IPv4 address", d.ifaceName)
|
||||
}
|
||||
@@ -326,7 +326,7 @@ func (d *Service) listenForResponses(ctx context.Context, listener *net.UDPConn,
|
||||
|
||||
device, err := d.parseResponse(responseText)
|
||||
if err != nil {
|
||||
log.Printf("UPnP: Failed to parse response #%d from %s: %v", responseCount, remoteAddr.String(), err)
|
||||
log.Printf("UPnP: Failed to parse response #%d from %s: %v", responseCount, sanitizeLog(remoteAddr.String()), err)
|
||||
continue // Skip invalid responses
|
||||
}
|
||||
|
||||
@@ -380,7 +380,7 @@ func (d *Service) parseResponse(response string) (*models.DiscoveredDevice, erro
|
||||
|
||||
// Check if it's a valid HTTP response
|
||||
if len(lines) < 1 || !strings.HasPrefix(lines[0], "HTTP/1.1 200") {
|
||||
log.Printf("UPnP: Invalid HTTP response, first line: '%s'", lines[0])
|
||||
log.Printf("UPnP: Invalid HTTP response, first line: '%s'", sanitizeLog(lines[0]))
|
||||
return nil, fmt.Errorf("invalid HTTP response")
|
||||
}
|
||||
|
||||
@@ -419,7 +419,7 @@ func (d *Service) parseResponse(response string) (*models.DiscoveredDevice, erro
|
||||
|
||||
// Accept both MediaRenderer and any device type for now - we'll validate it's a SoundTouch later
|
||||
if !strings.Contains(strings.ToLower(st), "mediarenderer") && !strings.Contains(strings.ToLower(st), "upnp:rootdevice") {
|
||||
log.Printf("UPnP: Device type '%s' is not a MediaRenderer, skipping", st)
|
||||
log.Printf("UPnP: Device type '%s' is not a MediaRenderer, skipping", sanitizeLog(st))
|
||||
return nil, fmt.Errorf("not a MediaRenderer device")
|
||||
}
|
||||
|
||||
@@ -436,7 +436,7 @@ func (d *Service) parseResponse(response string) (*models.DiscoveredDevice, erro
|
||||
// Extract device information from location URL
|
||||
device, err := d.parseLocationURL(location, headers["usn"])
|
||||
if err != nil {
|
||||
log.Printf("UPnP: Failed to parse location URL '%s': %v", location, err)
|
||||
log.Printf("UPnP: Failed to parse location URL '%s': %v", sanitizeLog(location), err)
|
||||
return nil, fmt.Errorf("failed to parse location URL: %w", err)
|
||||
}
|
||||
|
||||
@@ -449,7 +449,7 @@ func (d *Service) parseResponse(response string) (*models.DiscoveredDevice, erro
|
||||
if err := d.EnrichDeviceInfo(device, location); err != nil {
|
||||
logVerbose("UPnP: Could not enrich device info from location '%s': %v — accepting tentatively (will be re-verified by /info probe)", location, err)
|
||||
} else if !isBoseUPnPDevice(device) {
|
||||
log.Printf("UPnP: Rejecting non-Bose device: model=%q (manufacturer not Bose / model not SoundTouch)", device.ModelID)
|
||||
log.Printf("UPnP: Rejecting non-Bose device: model=%q (manufacturer not Bose / model not SoundTouch)", sanitizeLog(device.ModelID))
|
||||
return nil, fmt.Errorf("non-Bose UPnP device: %s", device.ModelID)
|
||||
} else {
|
||||
logVerbose("UPnP: Successfully enriched device info for %s (model=%q)", device.Name, device.ModelID)
|
||||
@@ -495,7 +495,7 @@ func (d *Service) parseLocationURL(location, usn string) (*models.DiscoveredDevi
|
||||
matches := re.FindStringSubmatch(location)
|
||||
|
||||
if len(matches) < 2 {
|
||||
log.Printf("UPnP: Location URL '%s' does not match expected format http://host:port", location)
|
||||
log.Printf("UPnP: Location URL '%s' does not match expected format http://host:port", sanitizeLog(location))
|
||||
return nil, fmt.Errorf("invalid location URL format")
|
||||
}
|
||||
|
||||
@@ -524,7 +524,7 @@ func (d *Service) EnrichDeviceInfo(device *models.DiscoveredDevice, location str
|
||||
|
||||
resp, err := d.httpClient.Get(location)
|
||||
if err != nil {
|
||||
log.Printf("UPnP: Failed to fetch device description from %s: %v", location, err)
|
||||
log.Printf("UPnP: Failed to fetch device description from %s: %v", sanitizeLog(location), err)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -554,7 +554,7 @@ func (d *Service) EnrichDeviceInfo(device *models.DiscoveredDevice, location str
|
||||
}
|
||||
|
||||
if err := xml.Unmarshal(data, &upnpRoot); err != nil {
|
||||
log.Printf("UPnP: Failed to parse device description from %s: %v", location, err)
|
||||
log.Printf("UPnP: Failed to parse device description from %s: %v", sanitizeLog(location), err)
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ func NewAmazonHandler() http.Handler {
|
||||
// HandleToken simulates the Amazon LWA token endpoint.
|
||||
// Amazon requires client_id and client_secret as POST body fields, not HTTP Basic Auth.
|
||||
func HandleToken(w http.ResponseWriter, r *http.Request) {
|
||||
log.Printf("[Amazon Mock] Token request: %s", r.Method)
|
||||
log.Printf("[Amazon Mock] Token request: %s", sanitizeLog(r.Method))
|
||||
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
@@ -36,7 +36,7 @@ func HandleToken(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
grantType := r.FormValue("grant_type")
|
||||
log.Printf("[Amazon Mock] Grant type: %s", grantType)
|
||||
log.Printf("[Amazon Mock] Grant type: %s", sanitizeLog(grantType))
|
||||
|
||||
resp := map[string]interface{}{
|
||||
"access_token": "Atza|amazon-access-token",
|
||||
@@ -71,7 +71,7 @@ func HandleToken(w http.ResponseWriter, r *http.Request) {
|
||||
// HandleProfile simulates the Amazon LWA user profile endpoint.
|
||||
// LWA returns "user_id" and "name" (not "id" / "display_name" like Spotify).
|
||||
func HandleProfile(w http.ResponseWriter, r *http.Request) {
|
||||
log.Printf("[Amazon Mock] Profile request: %s", r.Method)
|
||||
log.Printf("[Amazon Mock] Profile request: %s", sanitizeLog(r.Method))
|
||||
|
||||
auth := r.Header.Get("Authorization")
|
||||
if auth != "Bearer Atza|amazon-access-token" {
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
package amazon
|
||||
|
||||
import "strings"
|
||||
|
||||
// sanitizeLog strips newline characters from s to prevent log-injection
|
||||
// (CodeQL go/log-injection). Values from speakers, HTTP requests, and
|
||||
// external APIs may contain attacker-controlled newlines.
|
||||
func sanitizeLog(s string) string {
|
||||
s = strings.ReplaceAll(s, "\n", `\n`)
|
||||
s = strings.ReplaceAll(s, "\r", `\r`)
|
||||
|
||||
return s
|
||||
}
|
||||
@@ -23,7 +23,7 @@ func NewSpotifyHandler() http.Handler {
|
||||
|
||||
// HandleToken simulates the Spotify OAuth token endpoint.
|
||||
func HandleToken(w http.ResponseWriter, r *http.Request) {
|
||||
log.Printf("[Spotify Mock] Token request: %s", r.Method)
|
||||
log.Printf("[Spotify Mock] Token request: %s", sanitizeLog(r.Method))
|
||||
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
@@ -36,7 +36,7 @@ func HandleToken(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
grantType := r.FormValue("grant_type")
|
||||
log.Printf("[Spotify Mock] Grant type: %s", grantType)
|
||||
log.Printf("[Spotify Mock] Grant type: %s", sanitizeLog(grantType))
|
||||
|
||||
resp := map[string]interface{}{
|
||||
"access_token": "spotify-access-token",
|
||||
@@ -73,7 +73,7 @@ func HandleToken(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// HandleMe simulates the Spotify user profile endpoint.
|
||||
func HandleMe(w http.ResponseWriter, r *http.Request) {
|
||||
log.Printf("[Spotify Mock] Profile request: %s", r.Method)
|
||||
log.Printf("[Spotify Mock] Profile request: %s", sanitizeLog(r.Method))
|
||||
|
||||
auth := r.Header.Get("Authorization")
|
||||
if auth != "Bearer spotify-access-token" {
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
package spotify
|
||||
|
||||
import "strings"
|
||||
|
||||
// sanitizeLog strips newline characters from s to prevent log-injection
|
||||
// (CodeQL go/log-injection). Values from speakers, HTTP requests, and
|
||||
// external APIs may contain attacker-controlled newlines.
|
||||
func sanitizeLog(s string) string {
|
||||
s = strings.ReplaceAll(s, "\n", `\n`)
|
||||
s = strings.ReplaceAll(s, "\r", `\r`)
|
||||
|
||||
return s
|
||||
}
|
||||
Reference in New Issue
Block a user