Skip to content

Request monitor/API exposes persisted prompts and responses without an access boundary #35

Description

@chenshj73

Hi, thanks for building the request visualizer. I noticed a privacy hardening issue in the current main branch (02c9c766679e): the monitor stores full request/response content and exposes it through UI/API routes, but I could not find an access boundary around those routes.

Relevant code:

proxy/cmd/proxy/main.go

52 corsHandler := handlers.CORS(
53     handlers.AllowedOrigins([]string{"*"}),
54     handlers.AllowedMethods([]string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}),
55     handlers.AllowedHeaders([]string{"*"}),
56 )

Lines 52-56 allow any origin, method, and header.

65 r.HandleFunc("/", h.UI).Methods("GET")
66 r.HandleFunc("/ui", h.UI).Methods("GET")
67 r.HandleFunc("/api/requests", h.GetRequests).Methods("GET")
68 r.HandleFunc("/api/requests", h.DeleteRequests).Methods("DELETE")
69 r.HandleFunc("/api/conversations", h.GetConversations).Methods("GET")
70 r.HandleFunc("/api/conversations/{id}", h.GetConversationByID).Methods("GET")
71 r.HandleFunc("/api/conversations/project", h.GetConversationsByProject).Methods("GET")

Lines 65-71 expose the dashboard and request-history APIs. I did not find an auth middleware around these routes.

75 srv := &http.Server{
76     Addr: ":" + cfg.Server.Port,
77     Handler: corsHandler(r),

Lines 75-77 bind the HTTP server to all interfaces on the configured port.

proxy/internal/handler/handlers.go

78 requestLog := &model.RequestLog{
79     RequestID:     requestID,
80     Timestamp:     time.Now().Format(time.RFC3339),
81     Method:        r.Method,
82     Endpoint:      r.URL.Path,
83     Headers:       SanitizeHeaders(r.Header),
84     Body:          req,
85     Model:         decision.OriginalModel,
86     OriginalModel: decision.OriginalModel,
87     RoutedModel:   decision.TargetModel,

Lines 78-87 save sanitized headers, the full request body, and routing metadata.

183 allRequests, err := h.storageService.GetAllRequests(modelFilter)
...
213 w.Header().Set("Content-Type", "application/json")
214 json.NewEncoder(w).Encode(struct {
215     Requests []model.RequestLog `json:"requests"`
216     Total    int                `json:"total"`
217 }{
218     Requests: requests,
219     Total:    total,
220 })

Lines 183 and 213-220 fetch stored requests and return them as JSON.

274 var fullResponseText strings.Builder
276 var streamingChunks []string
...
282 scanner := bufio.NewScanner(resp.Body)
283 for scanner.Scan() {
284     line := scanner.Text()
285     if line == "" || !strings.HasPrefix(line, "data:") {
286         continue
287     }
289     streamingChunks = append(streamingChunks, line)
290     fmt.Fprintf(w, "%s\n\n", line)

Lines 274-290 both forward streaming chunks to the client and keep a copy in memory for logging.

373 responseLog := &model.ResponseLog{
374     StatusCode:      resp.StatusCode,
375     Headers:         SanitizeHeaders(resp.Header),
376     StreamingChunks: streamingChunks,
...
413 responseLog.Body = json.RawMessage(responseBodyBytes)
415 requestLog.Response = responseLog
416 if err := h.storageService.UpdateRequestWithResponse(requestLog); err != nil {

Lines 373-416 persist streaming chunks and a reconstructed response body.

427 func (h *Handler) handleNonStreamingResponse(...)
428     responseBytes, err := io.ReadAll(resp.Body)
...
448     responseLog.Body = json.RawMessage(responseBytes)
...
453     responseLog.BodyText = string(responseBytes)
...
460 requestLog.Response = responseLog
461 if err := h.storageService.UpdateRequestWithResponse(requestLog); err != nil {

Lines 427-461 persist non-streaming responses.

config.yaml.example:41-44 documents the persistent SQLite database path as requests.db.
README.md:19 describes SQLite-based logging of all API interactions.
README.md:295-297 documents searchable request history and request/response body inspection.

Why this matters:

The UI is useful, but these records can include user prompts, source code, tool results, model routing decisions, and assistant responses. If someone runs the proxy in Docker, on a server, or on a LAN, anyone who can reach the port can read /api/requests or the dashboard. They can also clear the history through DELETE /api/requests.

Suggested direction:

  • Add an optional monitor/API token, enabled by default for non-local binds.
  • Alternatively, add a monitor.enabled or monitor_auth_token setting and require it before exposing stored request/response bodies.
  • Bind to 127.0.0.1 by default, or add a local-only guard unless the user explicitly configures public listening.
  • Keep the current header sanitization. The missing boundary is around stored bodies and responses rather than header redaction.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions