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))