From dd7ae483c40412b81276894c70da849a9b22d15d Mon Sep 17 00:00:00 2001 From: Jan Svabenik Date: Mon, 6 Apr 2026 07:39:25 +0200 Subject: [PATCH] control: reject unexpected bodies on control POSTs --- internal/control/control.go | 30 ++++++++++++++++++++++ internal/control/control_test.go | 43 ++++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+) diff --git a/internal/control/control.go b/internal/control/control.go index dd1ac59..07cb355 100644 --- a/internal/control/control.go +++ b/internal/control/control.go @@ -54,6 +54,7 @@ type Server struct { const ( maxConfigBodyBytes = 64 << 10 // 64 KiB configContentTypeHeader = "application/json" + noBodyErrMsg = "request must not include a body" ) func isJSONContentType(r *http.Request) bool { @@ -89,6 +90,26 @@ func NewServer(cfg config.Config) *Server { return &Server{cfg: cfg} } +func hasRequestBody(r *http.Request) bool { + if r.ContentLength > 0 { + return true + } + for _, te := range r.TransferEncoding { + if strings.EqualFold(te, "chunked") { + return true + } + } + return false +} + +func rejectBody(w http.ResponseWriter, r *http.Request) bool { + if !hasRequestBody(r) { + return true + } + http.Error(w, noBodyErrMsg, http.StatusBadRequest) + return false +} + func (s *Server) SetTXController(tx TXController) { s.mu.Lock() s.tx = tx @@ -200,6 +221,9 @@ func (s *Server) handleRuntimeFaultReset(w http.ResponseWriter, r *http.Request) http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } + if !rejectBody(w, r) { + return + } s.mu.RLock() tx := s.tx s.mu.RUnlock() @@ -263,6 +287,9 @@ func (s *Server) handleTXStart(w http.ResponseWriter, r *http.Request) { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } + if !rejectBody(w, r) { + return + } s.mu.RLock() tx := s.tx s.mu.RUnlock() @@ -283,6 +310,9 @@ func (s *Server) handleTXStop(w http.ResponseWriter, r *http.Request) { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } + if !rejectBody(w, r) { + return + } s.mu.RLock() tx := s.tx s.mu.RUnlock() diff --git a/internal/control/control_test.go b/internal/control/control_test.go index 8a86cd2..846b24d 100644 --- a/internal/control/control_test.go +++ b/internal/control/control_test.go @@ -6,6 +6,7 @@ import ( "errors" "net/http" "net/http/httptest" + "strings" "testing" "github.com/jan/fm-rds-tx/internal/audio" @@ -288,6 +289,20 @@ func TestRuntimeFaultResetSuccess(t *testing.T) { } } +func TestRuntimeFaultResetRejectsBody(t *testing.T) { + srv := NewServer(cfgpkg.Default()) + srv.SetTXController(&fakeTXController{}) + rec := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodPost, "/runtime/fault/reset", bytes.NewReader([]byte("nope"))) + srv.Handler().ServeHTTP(rec, req) + if rec.Code != http.StatusBadRequest { + t.Fatalf("expected 400 when body present, got %d", rec.Code) + } + if !strings.Contains(rec.Body.String(), "request must not include a body") { + t.Fatalf("unexpected response body: %q", rec.Body.String()) + } +} + func TestAudioStreamRequiresSource(t *testing.T) { srv := NewServer(cfgpkg.Default()) rec := httptest.NewRecorder() @@ -349,6 +364,34 @@ func TestTXStartWithoutController(t *testing.T) { } } +func TestTXStartRejectsBody(t *testing.T) { + srv := NewServer(cfgpkg.Default()) + srv.SetTXController(&fakeTXController{}) + rec := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodPost, "/tx/start", bytes.NewReader([]byte("body"))) + srv.Handler().ServeHTTP(rec, req) + if rec.Code != http.StatusBadRequest { + t.Fatalf("expected 400 when body present, got %d", rec.Code) + } + if !strings.Contains(rec.Body.String(), "request must not include a body") { + t.Fatalf("unexpected response body: %q", rec.Body.String()) + } +} + +func TestTXStopRejectsBody(t *testing.T) { + srv := NewServer(cfgpkg.Default()) + srv.SetTXController(&fakeTXController{}) + rec := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodPost, "/tx/stop", bytes.NewReader([]byte("body"))) + srv.Handler().ServeHTTP(rec, req) + if rec.Code != http.StatusBadRequest { + t.Fatalf("expected 400 when body present, got %d", rec.Code) + } + if !strings.Contains(rec.Body.String(), "request must not include a body") { + t.Fatalf("unexpected response body: %q", rec.Body.String()) + } +} + func TestConfigPatchUpdatesSnapshot(t *testing.T) { srv := NewServer(cfgpkg.Default()) srv.SetTXController(&fakeTXController{})