fix: address spec compliance gaps - tenant check, XFF fallback, rate limiting

- Gap 1: Add tenant ID verification after device lookup in SSH relay handleSSH,
  closing cross-tenant token reuse vulnerability
- Gap 2: Add X-Forwarded-For fallback (last entry) when X-Real-IP is absent in
  SSH relay source IP extraction; import strings package
- Gap 3: Add @limiter.limit("10/minute") to POST /winbox-session and POST
  /ssh-session using existing slowapi pattern from app.middleware.rate_limit
- Gap 4: Add TODO comment in open_ssh_session explaining that SSH session count
  enforcement is at the poller level; no NATS subject exists yet for API-side
  pre-check

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jason Staack
2026-03-12 15:51:14 -05:00
parent a4e1c78744
commit 7aaaeaa1d1
2 changed files with 26 additions and 1 deletions

View File

@@ -6,6 +6,7 @@ import (
"fmt"
"log/slog"
"net/http"
"strings"
"sync"
"time"
@@ -123,8 +124,15 @@ func (s *Server) handleSSH(w http.ResponseWriter, r *http.Request) {
}
ws.SetReadLimit(1 << 20)
// Extract source IP (nginx sets X-Real-IP)
// Extract source IP (nginx sets X-Real-IP, fall back to X-Forwarded-For then RemoteAddr)
sourceIP := r.Header.Get("X-Real-IP")
if sourceIP == "" {
if xff := r.Header.Get("X-Forwarded-For"); xff != "" {
// Use last entry (closest proxy)
parts := strings.Split(xff, ",")
sourceIP = strings.TrimSpace(parts[len(parts)-1])
}
}
if sourceIP == "" {
sourceIP = r.RemoteAddr
}
@@ -137,6 +145,13 @@ func (s *Server) handleSSH(w http.ResponseWriter, r *http.Request) {
return
}
// Verify device belongs to the tenant in the token
if dev.TenantID != payload.TenantID {
slog.Warn("ssh: tenant mismatch", "device_tenant", dev.TenantID, "token_tenant", payload.TenantID)
ws.Close(websocket.StatusPolicyViolation, "unauthorized")
return
}
// Decrypt credentials — GetCredentials returns (username, password, error)
username, password, err := s.credCache.GetCredentials(
dev.ID,