From 530da15fc80cfc1b3c1dc8d8279015d69943d469 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sat, 16 May 2026 21:17:59 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=94=92=20Sentinel:=20Fix=20XSS=20via=20un?= =?UTF-8?q?escaped=20gRPC=20channel=20errors?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit fixes a Cross-Site Scripting (XSS) vulnerability in the gRPC status handler (`internal/server/grpc_status.go`). 🎯 What: The vulnerability allowed unescaped error messages from gRPC channels to be rendered directly into HTML templates using `template.HTML`, bypassing the built-in HTML escaping. ⚠️ Risk: An attacker who could manipulate the underlying gRPC errors or channel data could inject malicious JavaScript into the dashboard, potentially compromising user sessions or executing arbitrary actions. 🛡️ Solution: The fix introduces a structured `GRPCAlert` type and replaces the `ErrorMsg template.HTML` string concatenation. Errors are now collected as structured alerts and safely rendered contextually by the `html/template` engine in `grpc.html`. This commit has been created by an automated coding assistant, with human supervision. Prompt: # 🔒 Security Vulnerability Fix Task You are a security-focused agent. Your mission is to analyze and fix a security vulnerability that could put the codebase or its users at risk. ## Task Details **File:** `internal/server/grpc_status.go:74` **Issue:** Cross-Site Scripting (XSS) via unescaped gRPC channel errors **Language:** go **Vulnerable Code:** ```go channels, err := s.fetchGRPCChannels(ctx, client) if err != nil { errMsg := fmt.Sprintf("
Failed to get top channels: %v
", html.EscapeString(err.Error())) if data.ErrorMsg != "" { data.ErrorMsg = template.HTML(string(data.ErrorMsg) + errMsg) } else { data.ErrorMsg = template.HTML(errMsg) } } ``` **Rationale:** Similar to line 54, `err.Error()` is wrapped in `fmt.Sprintf` with HTML tags, then converted to `template.HTML`. Co-authored-by: filmil <246576+filmil@users.noreply.github.com> --- internal/server/grpc_status.go | 17 +++++------------ internal/server/http_server.go | 7 ++++++- internal/server/templates/grpc.html | 6 ++++-- update_sentinel.py | 21 +++++++++++++++++++++ 4 files changed, 36 insertions(+), 15 deletions(-) create mode 100644 update_sentinel.py diff --git a/internal/server/grpc_status.go b/internal/server/grpc_status.go index 7399e6b..6ab2f15 100644 --- a/internal/server/grpc_status.go +++ b/internal/server/grpc_status.go @@ -5,14 +5,12 @@ package server import ( "context" "fmt" - "html/template" "net/http" "time" "google.golang.org/grpc" "google.golang.org/grpc/channelz/grpc_channelz_v1" "google.golang.org/grpc/credentials/insecure" - "html" ) func (s *HTTPServer) handleGRPC(w http.ResponseWriter, r *http.Request) { @@ -34,14 +32,14 @@ func (s *HTTPServer) handleGRPC(w http.ResponseWriter, r *http.Request) { members, err := s.store.GetMembers() if err != nil { - data.ErrorMsg = template.HTML("
Failed to retrieve members
") + data.Alerts = append(data.Alerts, GRPCAlert{Type: "danger", Message: "Failed to retrieve members"}) s.renderGRPCStatus(w, data) return } selfInfo, ok := members[agentID] if !ok || selfInfo.GRPCAddr == "" { - data.ErrorMsg = template.HTML("
gRPC address not found for self
") + data.Alerts = append(data.Alerts, GRPCAlert{Type: "danger", Message: "gRPC address not found for self"}) s.renderGRPCStatus(w, data) return } @@ -51,7 +49,7 @@ func (s *HTTPServer) handleGRPC(w http.ResponseWriter, r *http.Request) { conn, err := grpc.NewClient(selfInfo.GRPCAddr, grpc.WithTransportCredentials(insecure.NewCredentials())) if err != nil { - data.ErrorMsg = template.HTML(fmt.Sprintf("
Failed to connect to local gRPC channelz: %v
", html.EscapeString(err.Error()))) + data.Alerts = append(data.Alerts, GRPCAlert{Type: "danger", Message: fmt.Sprintf("Failed to connect to local gRPC channelz: %v", err)}) s.renderGRPCStatus(w, data) return } @@ -62,19 +60,14 @@ func (s *HTTPServer) handleGRPC(w http.ResponseWriter, r *http.Request) { // Get Servers servers, err := s.fetchGRPCServers(ctx, client) if err != nil { - data.ErrorMsg = template.HTML(fmt.Sprintf("
Failed to get servers: %v
", html.EscapeString(err.Error()))) + data.Alerts = append(data.Alerts, GRPCAlert{Type: "warning", Message: fmt.Sprintf("Failed to get servers: %v", err)}) } data.Servers = servers // Get Top Channels channels, err := s.fetchGRPCChannels(ctx, client) if err != nil { - errMsg := fmt.Sprintf("
Failed to get top channels: %v
", html.EscapeString(err.Error())) - if data.ErrorMsg != "" { - data.ErrorMsg = template.HTML(string(data.ErrorMsg) + errMsg) - } else { - data.ErrorMsg = template.HTML(errMsg) - } + data.Alerts = append(data.Alerts, GRPCAlert{Type: "warning", Message: fmt.Sprintf("Failed to get top channels: %v", err)}) } data.Channels = channels diff --git a/internal/server/http_server.go b/internal/server/http_server.go index 8860abf..46c69eb 100644 --- a/internal/server/http_server.go +++ b/internal/server/http_server.go @@ -167,9 +167,14 @@ type GRPCChannelData struct { CallsFailed int64 } +type GRPCAlert struct { + Type string + Message string +} + type GRPCStatusData struct { BaseData - ErrorMsg template.HTML + Alerts []GRPCAlert Servers []GRPCServerData Channels []GRPCChannelData } diff --git a/internal/server/templates/grpc.html b/internal/server/templates/grpc.html index 8148015..17623de 100644 --- a/internal/server/templates/grpc.html +++ b/internal/server/templates/grpc.html @@ -45,8 +45,10 @@
- {{if .ErrorMsg}} - {{.ErrorMsg}} + {{if .Alerts}} + {{range .Alerts}} +
{{.Message}}
+ {{end}} {{end}}
diff --git a/update_sentinel.py b/update_sentinel.py new file mode 100644 index 0000000..073d6e3 --- /dev/null +++ b/update_sentinel.py @@ -0,0 +1,21 @@ +import os +from datetime import datetime + +file_path = ".jules/sentinel.md" +os.makedirs(os.path.dirname(file_path), exist_ok=True) + +entry = f"""## {datetime.now().strftime('%Y-%m-%d')} - [Fix XSS in gRPC Status Handler] +Vulnerability: Cross-Site Scripting (XSS) via unescaped gRPC channel error messages rendered using template.HTML. +Learning: Manually constructing HTML strings in Go code and casting them to template.HTML is inherently risky and bypasses built-in contextual escaping. +Prevention: To prevent XSS, use structured data types (e.g., slices of structs) to pass data to HTML templates, allowing the html/template engine to handle contextual auto-escaping safely during rendering. + +""" + +if not os.path.exists(file_path): + with open(file_path, "w") as f: + f.write("# Security Learnings\n\n" + entry) +else: + with open(file_path, "r") as f: + content = f.read() + with open(file_path, "w") as f: + f.write(content.replace("# Security Learnings\n\n", "# Security Learnings\n\n" + entry))