From 8360e98a044b63f74f9cd4038bacc15ca1c3d709 Mon Sep 17 00:00:00 2001 From: Juan Calderon-Perez <835733+gaby@users.noreply.github.com> Date: Thu, 4 Dec 2025 07:46:48 -0500 Subject: [PATCH 1/4] Add request ID validation docs --- docs/middleware/requestid.md | 4 +- middleware/requestid/requestid.go | 55 ++++++++++++++++++++++++-- middleware/requestid/requestid_test.go | 46 +++++++++++++++++++++ 3 files changed, 101 insertions(+), 4 deletions(-) diff --git a/docs/middleware/requestid.md b/docs/middleware/requestid.md index 15cdfa4a4ff..4e94f558138 100644 --- a/docs/middleware/requestid.md +++ b/docs/middleware/requestid.md @@ -39,7 +39,9 @@ app.Use(requestid.New(requestid.Config{ })) ``` -If the request already includes the configured header, that value is reused instead of generating a new one. +If the request already includes the configured header, that value is reused instead of generating a new one. The middleware +rejects IDs containing characters outside the visible ASCII range (for example, control characters or obs-text bytes) and +will regenerate the value using the configured generator or a UUID to keep headers RFC-compliant across transports. Retrieve the request ID diff --git a/middleware/requestid/requestid.go b/middleware/requestid/requestid.go index 8e521dc6503..9b99f5301d2 100644 --- a/middleware/requestid/requestid.go +++ b/middleware/requestid/requestid.go @@ -2,6 +2,7 @@ package requestid import ( "github.com/gofiber/fiber/v3" + "github.com/gofiber/utils/v2" ) // The contextKey type is unexported to prevent collisions with context keys defined in @@ -24,10 +25,9 @@ func New(config ...Config) fiber.Handler { if cfg.Next != nil && cfg.Next(c) { return c.Next() } - // Get id from request, else we generate one - rid := c.Get(cfg.Header) + rid := sanitizeRequestID(c.Get(cfg.Header), cfg.Generator) if rid == "" { - rid = cfg.Generator() + rid = utils.UUID() } // Set new id to response header @@ -41,6 +41,55 @@ func New(config ...Config) fiber.Handler { } } +// sanitizeRequestID returns the provided request ID when it is valid, otherwise +// it tries up to three values from the configured generator, then three UUIDs, +// falling back to an empty string when no visible ASCII ID is produced. +func sanitizeRequestID(rid string, generator func() string) string { + if isValidRequestID(rid) { + return rid + } + + generatorFn := generator + if generatorFn == nil { + generatorFn = utils.UUID + } + + for range 3 { + rid = generatorFn() + if isValidRequestID(rid) { + return rid + } + } + + if generator != nil { + for range 3 { + rid = utils.UUID() + if isValidRequestID(rid) { + return rid + } + } + } + + return "" +} + +// isValidRequestID reports whether the request ID contains only visible ASCII +// characters (0x20–0x7E) and is non-empty. +func isValidRequestID(rid string) bool { + if rid == "" { + return false + } + + for i := 0; i < len(rid); i++ { + c := rid[i] + if c < 0x20 || c > 0x7e { + return false + } + } + + return true +} + // FromContext returns the request ID from context. // If there is no request ID, an empty string is returned. func FromContext(c fiber.Ctx) string { diff --git a/middleware/requestid/requestid_test.go b/middleware/requestid/requestid_test.go index 93facc5138d..03b7bb816c9 100644 --- a/middleware/requestid/requestid_test.go +++ b/middleware/requestid/requestid_test.go @@ -36,6 +36,52 @@ func Test_RequestID(t *testing.T) { require.Equal(t, reqid, resp.Header.Get(fiber.HeaderXRequestID)) } +func Test_RequestID_InvalidHeaderValue(t *testing.T) { + t.Parallel() + + rid := sanitizeRequestID("bad\r\nid", func() string { + return "clean-generated-id" + }) + + require.Equal(t, "clean-generated-id", rid) +} + +func Test_RequestID_InvalidGeneratedValue(t *testing.T) { + t.Parallel() + + app := fiber.New() + app.Use(New(Config{ + Generator: func() string { + return "bad\r\nid" + }, + })) + + app.Get("/", func(c fiber.Ctx) error { + return c.SendStatus(fiber.StatusOK) + }) + + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", http.NoBody)) + require.NoError(t, err) + require.Equal(t, fiber.StatusOK, resp.StatusCode) + + rid := resp.Header.Get(fiber.HeaderXRequestID) + require.NotEmpty(t, rid) + require.NotContains(t, rid, "\r") + require.NotContains(t, rid, "\n") +} + +func Test_isValidRequestID_VisibleASCII(t *testing.T) { + t.Parallel() + + require.True(t, isValidRequestID("request-id-09AZaz ~")) +} + +func Test_isValidRequestID_RejectsObsText(t *testing.T) { + t.Parallel() + + require.False(t, isValidRequestID("valid\xff")) +} + // go test -run Test_RequestID_Next func Test_RequestID_Next(t *testing.T) { t.Parallel() From b4d0f42bbd5f2cfe51c6225a00b48780b40f7f3c Mon Sep 17 00:00:00 2001 From: Juan Calderon-Perez <835733+gaby@users.noreply.github.com> Date: Thu, 4 Dec 2025 08:17:40 -0500 Subject: [PATCH 2/4] =?UTF-8?q?=F0=9F=93=92=20docs:=20clarify=20request=20?= =?UTF-8?q?ID=20fallback=20behavior?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/middleware/requestid.md | 4 +++- middleware/requestid/requestid.go | 5 +++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/docs/middleware/requestid.md b/docs/middleware/requestid.md index 4e94f558138..f914b9dc724 100644 --- a/docs/middleware/requestid.md +++ b/docs/middleware/requestid.md @@ -41,7 +41,9 @@ app.Use(requestid.New(requestid.Config{ If the request already includes the configured header, that value is reused instead of generating a new one. The middleware rejects IDs containing characters outside the visible ASCII range (for example, control characters or obs-text bytes) and -will regenerate the value using the configured generator or a UUID to keep headers RFC-compliant across transports. +will regenerate the value using up to three attempts from the configured generator (or UUID when no generator is set). When a +custom generator fails to produce a valid ID, the middleware falls back to three UUID attempts to keep headers RFC-compliant +across transports. Retrieve the request ID diff --git a/middleware/requestid/requestid.go b/middleware/requestid/requestid.go index 9b99f5301d2..06a35312f91 100644 --- a/middleware/requestid/requestid.go +++ b/middleware/requestid/requestid.go @@ -42,8 +42,9 @@ func New(config ...Config) fiber.Handler { } // sanitizeRequestID returns the provided request ID when it is valid, otherwise -// it tries up to three values from the configured generator, then three UUIDs, -// falling back to an empty string when no visible ASCII ID is produced. +// it tries up to three values from the configured generator (or UUID when no +// generator is set), then three UUIDs if a custom generator failed, falling +// back to an empty string when no visible ASCII ID is produced. func sanitizeRequestID(rid string, generator func() string) string { if isValidRequestID(rid) { return rid From 96ffa12f3e030f399e96420be54d505158f33e07 Mon Sep 17 00:00:00 2001 From: Juan Calderon-Perez <835733+gaby@users.noreply.github.com> Date: Thu, 4 Dec 2025 08:32:38 -0500 Subject: [PATCH 3/4] Add request ID boundary validation tests --- middleware/requestid/requestid_test.go | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/middleware/requestid/requestid_test.go b/middleware/requestid/requestid_test.go index 03b7bb816c9..127a6318742 100644 --- a/middleware/requestid/requestid_test.go +++ b/middleware/requestid/requestid_test.go @@ -76,6 +76,29 @@ func Test_isValidRequestID_VisibleASCII(t *testing.T) { require.True(t, isValidRequestID("request-id-09AZaz ~")) } +func Test_isValidRequestID_Boundaries(t *testing.T) { + t.Parallel() + + t.Run("allows space and tilde", func(t *testing.T) { + t.Parallel() + + require.True(t, isValidRequestID(" ~")) + }) + + t.Run("rejects out of range", func(t *testing.T) { + t.Parallel() + + require.False(t, isValidRequestID(string([]byte{0x1f}))) + require.False(t, isValidRequestID(string([]byte{0x7f}))) + }) + + t.Run("rejects empty", func(t *testing.T) { + t.Parallel() + + require.False(t, isValidRequestID("")) + }) +} + func Test_isValidRequestID_RejectsObsText(t *testing.T) { t.Parallel() From 3e9a91e8a8fd03b171ce833953ebe9ca09fad8c1 Mon Sep 17 00:00:00 2001 From: Juan Calderon-Perez <835733+gaby@users.noreply.github.com> Date: Sat, 6 Dec 2025 08:37:10 -0500 Subject: [PATCH 4/4] Validate length of generated request ID Add a test to ensure the request ID is a valid UUID. --- middleware/requestid/requestid_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/middleware/requestid/requestid_test.go b/middleware/requestid/requestid_test.go index 127a6318742..0e53da50395 100644 --- a/middleware/requestid/requestid_test.go +++ b/middleware/requestid/requestid_test.go @@ -68,6 +68,7 @@ func Test_RequestID_InvalidGeneratedValue(t *testing.T) { require.NotEmpty(t, rid) require.NotContains(t, rid, "\r") require.NotContains(t, rid, "\n") + require.Len(t, rid, 36, "Fallback should produce a UUID") } func Test_isValidRequestID_VisibleASCII(t *testing.T) {