From cd3fe5d00a8fed5b4b7d14976ef3ee7751deb7d0 Mon Sep 17 00:00:00 2001 From: Rogee Date: Sat, 10 Oct 2026 05:46:32 +0800 Subject: [PATCH] fix: recognize case-insensitive signed URL schemes across log paths --- internal/logging/redact.go | 2 +- internal/logging/text_security_test.go | 54 ++++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 1 deletion(-) diff --git a/internal/logging/redact.go b/internal/logging/redact.go index 63465f1..c3077cd 100644 --- a/internal/logging/redact.go +++ b/internal/logging/redact.go @@ -158,7 +158,7 @@ func RedactJSON(body []byte) (string, error) { } var ( - urlInText = regexp.MustCompile(`(?:https?|wss?)://[^\s<>"']+`) + urlInText = regexp.MustCompile(`(?i)(?:https?|wss?)://[^\s<>"']+`) authHeaderInText = regexp.MustCompile(`(?im)\b(?:proxy-authorization|authorization|set-cookie|cookie)["']?\s*[=:]\s*[^\r\n]+`) privatePEMInText = regexp.MustCompile(`(?s)-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----.*?(?:-----END [A-Z0-9 ]*PRIVATE KEY-----|$)`) sensitiveTextLine = regexp.MustCompile(`(?im)\b(?:prompt|system[_-]?prompt|messages|transcript|text|content|audio|closing[_-]?remark)\s*[=:]\s*[^\r\n]+`) diff --git a/internal/logging/text_security_test.go b/internal/logging/text_security_test.go index f45f370..da901ae 100644 --- a/internal/logging/text_security_test.go +++ b/internal/logging/text_security_test.go @@ -61,6 +61,60 @@ func TestContentFieldClassificationAndEveryLoggingPathAgree(t *testing.T) { } } +func TestURLSchemeCaseCannotExposeSignedPartsOnAnyLoggingPath(t *testing.T) { + for _, scheme := range []string{"http", "HTTP", "HtTp", "https", "HTTPS", "hTtPs", "ws", "WS", "wS", "wss", "WSS", "wSs"} { + signed := scheme + "://storage.example/audio.wav?sv=2025-01-05&se=2026-10-10&sig=private-signature-value" + for _, debug := range []bool{false, true} { + out := installTestLogger(t, debug) + for _, media := range []string{"text/plain", "application/json"} { + body := "upload failed: " + signed + if media == "application/json" { + body = `{"resource":"` + signed + `","task_id":4,"status":"ready"}` + } + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + got, err := io.ReadAll(r.Body) + if err != nil || string(got) != body { + t.Error("wire request changed", err) + } + w.Header().Set("Content-Type", media) + _, _ = io.WriteString(w, body) + })) + req, err := http.NewRequest(http.MethodPost, ts.URL, strings.NewReader(body)) + if err != nil { + t.Fatal(err) + } + req.Header.Set("Content-Type", media) + response, err := HTTPClient(ts.Client(), "test.url", "upstream").Do(req) + if err != nil { + ts.Close() + t.Fatal(err) + } + got, err := io.ReadAll(response.Body) + _ = response.Body.Close() + ts.Close() + if err != nil || string(got) != body { + t.Fatal("wire response changed", err) + } + slog.Error("upload failed", "error", errors.New(body), "detail", body) + slog.Error(body) + } + text := out.String() + if strings.Contains(text, "private-signature-value") || strings.Contains(text, "sv=2025") || strings.Contains(text, "se=2026") { + t.Fatalf("scheme %s debug %v leak: %s", scheme, debug, text) + } + for _, keep := range []string{"upload failed", "audio.wav", "task_id", "ready"} { + if !strings.Contains(text, keep) { + t.Fatal("safe diagnostic lost", text) + } + } + plain := scheme + "://storage.example/public.wav" + if got := cleanString(plain); got != plain { + t.Fatal("unsigned URL changed", got) + } + } + } +} + func TestPercentEncodedSingleAndMixedFormsKeepWireBytesAndHideSecrets(t *testing.T) { for _, body := range []string{ "%74oken=private-access-token",