fix(security): add SDK/sign body limits, IP validation, error sanitization, file perms, mode validation

S1: http.MaxBytesReader (64KB) on SDK decodeRequestBody S2: http.MaxBytesReader (64KB) on /v1/sign endpoint S3: net.ParseIP validation in admin IP ban handler S4: Generic error message for internal /v1/sign errors S5: Settings file permissions tightened to 0600 S6: JSON API envelope for SDK requireMethod errors S7: SetApprovalMode returns error for invalid modes

cognitive committed Mar 5, 2026 at 03:03 UTC 0a58e85248a92221e40ee0fced70d6b6a6f40464
6 files changed +30 -8
cmd/relay-server/registry.go
+2 -1
@@ -222,11 +222,12 @@ func (r *SDKRegistry) requireMethod(w http.ResponseWriter, req *http.Request, me
222 }
223
224 w.Header().Set("Allow", method)
225 - http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
225 + writeAPIError(w, http.StatusMethodNotAllowed, "method_not_allowed", "method not allowed")
226 return false
227 }
228
229 func (r *SDKRegistry) decodeRequestBody(w http.ResponseWriter, req *http.Request, dst any, logMessage string) bool {
230 + req.Body = http.MaxBytesReader(w, req.Body, 1<<16)
231 if err := json.NewDecoder(req.Body).Decode(dst); err != nil {
232 log.Error().Err(err).Msg(logMessage)
233 writeAPIError(w, http.StatusBadRequest, "invalid_request", "invalid request body")
cmd/relay-server/serve.go
+6 -1
@@ -219,6 +219,7 @@ func handleKeylessSign(w http.ResponseWriter, r *http.Request, signer *keyless.S
219 return
220 }
221
222 + r.Body = http.MaxBytesReader(w, r.Body, 1<<16)
223 defer r.Body.Close()
224
225 var req keyless.SignRequest
@@ -238,7 +239,11 @@ func handleKeylessSign(w http.ResponseWriter, r *http.Request, signer *keyless.S
239 case errors.Is(err, keyless.ErrPermissionDenied):
240 status = http.StatusForbidden
241 }
241 - writeSignError(w, status, err.Error())
242 + msg := err.Error()
243 + if status == http.StatusInternalServerError {
244 + msg = "internal signing error"
245 + }
246 + writeSignError(w, status, msg)
247 return
248 }
249
portal/admin/handler.go
+6 -1
@@ -4,6 +4,7 @@ import (
4 "encoding/base64"
5 "encoding/json"
6 "errors"
7 + "net"
8 "net/http"
9 "strings"
10
@@ -273,7 +274,7 @@ func (h *Handler) handleApprovalModeRequest(w http.ResponseWriter, r *http.Reque
274 h.writeAPIError(w, http.StatusBadRequest, "invalid_mode", "invalid mode (must be 'auto' or 'manual')")
275 return
276 }
276 - approveManager.SetApprovalMode(mode)
277 + _ = approveManager.SetApprovalMode(mode) // mode already validated above
278 h.service.SaveSettings(serv)
279 log.Info().Str("mode", string(mode)).Msg("[Admin] Approval mode changed")
280 h.writeAPIData(w, http.StatusOK, types.AdminApprovalModeResponse{
@@ -498,6 +499,10 @@ func (h *Handler) handleIPBanRequest(w http.ResponseWriter, r *http.Request, ser
499 h.writeAPIError(w, http.StatusBadRequest, "invalid_ip", "invalid IP address")
500 return
501 }
502 + if net.ParseIP(ip) == nil {
503 + h.writeAPIError(w, http.StatusBadRequest, "invalid_ip", "invalid IP address")
504 + return
505 + }
506
507 ipManager := h.service.GetIPManager()
508 if ipManager == nil {
portal/admin/service.go
+4 -2
@@ -135,7 +135,7 @@ func (s *Service) SaveSettings(serv *portal.RelayServer) {
135 }
136 }
137
138 - if err := os.WriteFile(s.settingsPath, data, 0644); err != nil {
138 + if err := os.WriteFile(s.settingsPath, data, 0600); err != nil {
139 log.Error().Err(err).Msg("[Admin] Failed to save admin settings")
140 return
141 }
@@ -180,7 +180,9 @@ func (s *Service) LoadSettings(serv *portal.RelayServer) {
180 }
181
182 if payload.ApprovalMode != "" {
183 - s.approveManager.SetApprovalMode(payload.ApprovalMode)
183 + if err := s.approveManager.SetApprovalMode(payload.ApprovalMode); err != nil {
184 + log.Warn().Err(err).Str("mode", string(payload.ApprovalMode)).Msg("[Admin] Ignoring invalid approval mode from settings")
185 + }
186 }
187
188 for _, leaseID := range payload.ApprovedLeases {
portal/admin/service_test.go
+3 -1
@@ -18,7 +18,9 @@ func TestServiceSaveLoadSettingsRoundTrip(t *testing.T) {
18 sourceServer := mustNewTestRelayServer(t)
19 sourceServer.GetLeaseManager().BanLease("lease-ban")
20 service.GetBPSManager().SetBPSLimit("lease-bps", 4096)
21 - service.GetApproveManager().SetApprovalMode(policy.ModeManual)
21 + if err := service.GetApproveManager().SetApprovalMode(policy.ModeManual); err != nil {
22 + t.Fatalf("SetApprovalMode: %v", err)
23 + }
24 service.GetApproveManager().ApproveLease("lease-approved")
25 service.GetApproveManager().DenyLease("lease-denied")
26 service.GetIPManager().BanIP("203.0.113.10")
portal/policy/approver.go
+9 -2
@@ -1,6 +1,9 @@
1 package policy
2
3 -import "sync"
3 +import (
4 + "fmt"
5 + "sync"
6 +)
7
8 // Mode represents the approval mode for new connections.
9 type Mode string
@@ -32,10 +35,14 @@ func (m *Approver) GetApprovalMode() Mode {
35 return m.approvalMode
36 }
37
35 -func (m *Approver) SetApprovalMode(mode Mode) {
38 +func (m *Approver) SetApprovalMode(mode Mode) error {
39 + if mode != ModeAuto && mode != ModeManual {
40 + return fmt.Errorf("invalid approval mode: %q", mode)
41 + }
42 m.mu.Lock()
43 defer m.mu.Unlock()
44 m.approvalMode = mode
45 + return nil
46 }
47
48 func (m *Approver) IsLeaseApproved(leaseID string) bool {