mirror of
https://github.com/gesellix/Bose-SoundTouch.git
synced 2026-08-19 00:56:16 +00:00
fix(tts): resolve speak target to a known device IP (SSRF, CodeQL 305)
HandleTTSSpeak passed the request's `host` straight to client.NewClientFromHost, so the resolved value flowed into the client's baseURL and the outbound request (client.go post -> httpClient.Do) — a caller could point the service at an arbitrary host:8090 (SSRF). resolveTTSHost now always returns an IP looked up from the datastore: match by deviceId, or by host equal to a known device's IP, and return that stored IPAddress (never the caller-supplied string). Unknown hosts/devices are rejected. This both mitigates the SSRF and breaks the tainted data flow. Adds regression cases for unknown host/device. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
40633f33c8
commit
d413bf60ab
@@ -145,14 +145,18 @@ func (s *Server) HandleSpeakerAuth(w http.ResponseWriter, _ *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
// resolveTTSHost returns the speaker IP/hostname to target. An explicit Host
|
||||
// wins; otherwise DeviceID is looked up in the datastore.
|
||||
// resolveTTSHost returns the speaker IP to target, resolved from the datastore
|
||||
// so the result is always a known device address — never a value taken straight
|
||||
// from the request. This prevents the endpoint from being used as an SSRF proxy
|
||||
// to arbitrary hosts (the resolved IP flows into client.NewClientFromHost ->
|
||||
// baseURL -> the outbound request). Match by DeviceID, or by Host equal to a
|
||||
// known device's IP; either way the returned string is the datastore's
|
||||
// IPAddress, not the caller-supplied value.
|
||||
func (s *Server) resolveTTSHost(req ttsSpeakRequest) (string, error) {
|
||||
if h := strings.TrimSpace(req.Host); h != "" {
|
||||
return h, nil
|
||||
}
|
||||
deviceID := strings.TrimSpace(req.DeviceID)
|
||||
host := strings.TrimSpace(req.Host)
|
||||
|
||||
if strings.TrimSpace(req.DeviceID) == "" {
|
||||
if deviceID == "" && host == "" {
|
||||
return "", fmt.Errorf("either deviceId or host is required")
|
||||
}
|
||||
|
||||
@@ -162,16 +166,21 @@ func (s *Server) resolveTTSHost(req ttsSpeakRequest) (string, error) {
|
||||
}
|
||||
|
||||
for i := range devices {
|
||||
if devices[i].DeviceID == req.DeviceID {
|
||||
if devices[i].IPAddress == "" {
|
||||
return "", fmt.Errorf("device %s has no known IP address", req.DeviceID)
|
||||
}
|
||||
ip := devices[i].IPAddress
|
||||
if ip == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
return devices[i].IPAddress, nil
|
||||
if (deviceID != "" && devices[i].DeviceID == deviceID) || (host != "" && ip == host) {
|
||||
return ip, nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("device %s not found", req.DeviceID)
|
||||
if deviceID != "" {
|
||||
return "", fmt.Errorf("device %s not found (or has no known IP)", deviceID)
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("host %s is not a known device", host)
|
||||
}
|
||||
|
||||
// HandleTTSMedia serves a synthesized clip by id for the speaker to fetch.
|
||||
|
||||
@@ -163,6 +163,10 @@ func TestHandleTTSSpeakValidation(t *testing.T) {
|
||||
{"empty text", `{"host":"192.0.2.10","text":" "}`, http.StatusBadRequest},
|
||||
{"no target", `{"text":"hello"}`, http.StatusBadRequest},
|
||||
{"bad json", `{not json}`, http.StatusBadRequest},
|
||||
// SSRF guard: an arbitrary host that isn't a known device must be
|
||||
// rejected, not connected to.
|
||||
{"unknown host", `{"host":"203.0.113.99","text":"hello"}`, http.StatusBadRequest},
|
||||
{"unknown device", `{"deviceId":"NOPE","text":"hello"}`, http.StatusBadRequest},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
|
||||
Reference in New Issue
Block a user