Honor RoundTripper contract in middleware; validate incoming X-Request-Id

BearerAuth, BasicAuth and DefaultHeaders mutated the caller's request, which
violates the RoundTripper contract and risks races on shared/retried requests;
clone before writing headers (matching RequestID). Validate the incoming
X-Request-Id (length and character set) before propagating it to logs and the
response header, preventing log forging and header splitting from a
client-controlled value.
This commit is contained in:
2026-05-23 13:47:38 +03:00
parent 01478be0dc
commit b5259af73e
5 changed files with 72 additions and 5 deletions

View File

@@ -214,6 +214,36 @@ func TestRequestID(t *testing.T) {
t.Fatalf("expected empty, got %q", id)
}
})
t.Run("rejects unsafe incoming ID", func(t *testing.T) {
cases := map[string]string{
"header injection": "abc\r\nX-Injected: 1",
"contains space": "has space",
"too long": strings.Repeat("a", 200),
}
for name, badID := range cases {
t.Run(name, func(t *testing.T) {
var gotID string
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
gotID = server.RequestIDFromContext(r.Context())
w.WriteHeader(http.StatusOK)
})
mw := server.RequestID()(handler)
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/", nil)
req.Header.Set("X-Request-Id", badID)
mw.ServeHTTP(w, req)
if gotID == badID {
t.Fatalf("unsafe incoming ID was propagated verbatim: %q", gotID)
}
if len(gotID) != 36 {
t.Fatalf("expected a freshly generated UUID, got %q (len %d)", gotID, len(gotID))
}
})
}
})
}
func TestRequestID_UUIDFormat(t *testing.T) {