diff --git a/docs/middleware/csrf.md b/docs/middleware/csrf.md index 162f8a84eeb..a232e22714b 100644 --- a/docs/middleware/csrf.md +++ b/docs/middleware/csrf.md @@ -406,7 +406,7 @@ func (h *csrf.Handler) DeleteToken(c fiber.Ctx) error | CookieSameSite | `string` | SameSite attribute (**use "Lax" or "Strict"**) | `"Lax"` | | CookieSessionOnly | `bool` | Session-only cookie (expires on browser close) | `false` | | IdleTimeout | `time.Duration` | Token expiration time | `30 * time.Minute` | -| KeyGenerator | `func() string` | Token generation function | `utils.UUIDv4` | +| KeyGenerator | `func() string` | Token generation function | `utils.SecureToken` | | ErrorHandler | `fiber.ErrorHandler` | Custom error handler | `defaultErrorHandler` | | Extractor | `extractors.Extractor` | Token extraction method with metadata | `extractors.FromHeader("X-Csrf-Token")` | | DisableValueRedaction | `bool` | Disables redaction of tokens and storage keys in logs and error messages. | `false` | diff --git a/docs/middleware/requestid.md b/docs/middleware/requestid.md index f914b9dc724..5ca10a3d7dd 100644 --- a/docs/middleware/requestid.md +++ b/docs/middleware/requestid.md @@ -41,8 +41,8 @@ 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 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 +will regenerate the value using up to three attempts from the configured generator (or SecureToken when no generator is set). When a +custom generator fails to produce a valid ID, the middleware falls back to SecureToken to keep headers RFC-compliant across transports. Retrieve the request ID @@ -61,18 +61,16 @@ func handler(c fiber.Ctx) error { |:----------|:---------------------|:-----------------------------------------|:---------------| | Next | `func(fiber.Ctx) bool` | Skip when the function returns `true`. | `nil` | | Header | `string` | Header key used to store the request ID. | "X-Request-ID" | -| Generator | `func() string` | Function that generates the identifier. | utils.UUID | +| Generator | `func() string` | Function that generates the identifier. | utils.SecureToken | ## Default Config -The default config uses a fast UUID generator which will expose the number of -requests made to the server. To conceal this value for better privacy, use the -`utils.UUIDv4` generator. +The default config uses a cryptographically secure token generator for better security and privacy. ```go var ConfigDefault = Config{ Next: nil, Header: fiber.HeaderXRequestID, - Generator: utils.UUID, + Generator: utils.SecureToken, } ``` diff --git a/docs/middleware/session.md b/docs/middleware/session.md index c03afc70502..38717714d90 100644 --- a/docs/middleware/session.md +++ b/docs/middleware/session.md @@ -454,7 +454,7 @@ app.Use(session.New(session.Config{ // Session ID Extractor: extractors.FromCookie("__Host-session_id"), - KeyGenerator: utils.UUIDv4, + KeyGenerator: utils.SecureToken, // Error Handling ErrorHandler: func(c fiber.Ctx, err error) { @@ -679,7 +679,7 @@ extractors.Chain(extractors ...extractors.Extractor) extractors.Extractor | `Store` | `*session.Store` | Pre-built session store (use when you need to share/register types) | `nil` (auto-created) | | `Storage` | `fiber.Storage` | Session storage backend (used when creating a store if `Store` is nil) | `memory.New()` | | `Extractor` | `extractors.Extractor` | Session ID extraction | `extractors.FromCookie("session_id")` | -| `KeyGenerator` | `func() string` | Session ID generator | `utils.UUIDv4` | +| `KeyGenerator` | `func() string` | Session ID generator | `utils.SecureToken` | | `IdleTimeout` | `time.Duration` | Inactivity timeout | `30 * time.Minute` | | `AbsoluteTimeout` | `time.Duration` | Maximum session duration | `0` (unlimited) | | `CookieSecure` | `bool` | HTTPS only | `false` | diff --git a/docs/whats_new.md b/docs/whats_new.md index acbc2c8afef..b815aa9fcee 100644 --- a/docs/whats_new.md +++ b/docs/whats_new.md @@ -1563,6 +1563,8 @@ The session middleware has undergone significant improvements in v3, focusing on - **Absolute Timeout**: The `AbsoluteTimeout` field has been added. If you need to set an absolute session timeout, you can use this field to define the duration. The session will expire after the specified duration, regardless of activity. +- **Default KeyGenerator**: Changed from `utils.UUIDv4` to `utils.SecureToken`, producing base64-encoded tokens instead of UUID format. + For more details on these changes and migration instructions, check the [Session Middleware Migration Guide](./middleware/session.md#migration-guide). ### Timeout @@ -2783,6 +2785,8 @@ app.Use(csrf.New(csrf.Config{ - **KeyLookup Field Removal**: The `KeyLookup` field has been removed from the CSRF middleware configuration. This field was deprecated and is no longer needed as the middleware now uses a more secure approach for token management. - **DisableValueRedaction Toggle**: CSRF redacts tokens and storage keys by default; set `DisableValueRedaction` to `true` when diagnostics require the raw values. +- **Default KeyGenerator**: Changed from `utils.UUIDv4` to `utils.SecureToken`, producing base64-encoded tokens instead of UUID format. + ```go // Before app.Use(csrf.New(csrf.Config{ diff --git a/go.mod b/go.mod index aaed76b5d91..79f378daa41 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.25.0 require ( github.com/gofiber/schema v1.6.0 - github.com/gofiber/utils/v2 v2.0.0-rc.4 + github.com/gofiber/utils/v2 v2.0.0-rc.5 github.com/google/uuid v1.6.0 github.com/mattn/go-colorable v0.1.14 github.com/mattn/go-isatty v0.0.20 diff --git a/go.sum b/go.sum index b96eb17e489..592e88aa426 100644 --- a/go.sum +++ b/go.sum @@ -6,8 +6,8 @@ github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sa github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/gofiber/schema v1.6.0 h1:rAgVDFwhndtC+hgV7Vu5ItQCn7eC2mBA4Eu1/ZTiEYY= github.com/gofiber/schema v1.6.0/go.mod h1:WNZWpQx8LlPSK7ZaX0OqOh+nQo/eW2OevsXs1VZfs/s= -github.com/gofiber/utils/v2 v2.0.0-rc.4 h1:CDjwPwtwwj1OTIf6v3iRk+D2wcdjUzwk91Ghu2TMNbE= -github.com/gofiber/utils/v2 v2.0.0-rc.4/go.mod h1:gXins5o7up+BQFiubmO8aUJc/+Mhd7EKXIiAK5GBomI= +github.com/gofiber/utils/v2 v2.0.0-rc.5 h1:zosaA+j2jm9yhjuxGkFGWxILH8iL0iCoVYT6U/Qgej8= +github.com/gofiber/utils/v2 v2.0.0-rc.5/go.mod h1:8PuWXERC3IoTmoD2Fp/X7amJntq928Fa2yTHI5Orj2M= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk= diff --git a/internal/memory/memory_test.go b/internal/memory/memory_test.go index 83cb78559c4..b6df88bd0ed 100644 --- a/internal/memory/memory_test.go +++ b/internal/memory/memory_test.go @@ -60,7 +60,7 @@ func Benchmark_Memory(b *testing.B) { keyLength := 1000 keys := make([]string, keyLength) for i := range keyLength { - keys[i] = utils.UUID() + keys[i] = utils.UUIDv4() } value := []byte("joe") diff --git a/middleware/cache/manager_test.go b/middleware/cache/manager_test.go index 760a3fcd6c3..3715231b253 100644 --- a/middleware/cache/manager_test.go +++ b/middleware/cache/manager_test.go @@ -15,13 +15,13 @@ func Test_manager_get(t *testing.T) { cacheManager := newManager(nil, true) t.Run("Item not found in cache", func(t *testing.T) { t.Parallel() - it, err := cacheManager.get(context.Background(), utils.UUID()) + it, err := cacheManager.get(context.Background(), utils.UUIDv4()) require.ErrorIs(t, err, errCacheMiss) assert.Nil(t, it) }) t.Run("Item found in cache", func(t *testing.T) { t.Parallel() - id := utils.UUID() + id := utils.UUIDv4() cacheItem := cacheManager.acquire() cacheItem.body = []byte("test-body") require.NoError(t, cacheManager.set(context.Background(), id, cacheItem, 10*time.Second)) diff --git a/middleware/csrf/config.go b/middleware/csrf/config.go index 3123aae8bfa..fa7d71e6950 100644 --- a/middleware/csrf/config.go +++ b/middleware/csrf/config.go @@ -33,7 +33,7 @@ type Config struct { // KeyGenerator creates a new CSRF token. // - // Optional. Default: utils.UUIDv4 + // Optional. Default: utils.SecureToken KeyGenerator func() string // ErrorHandler is executed when an error is returned from fiber.Handler. @@ -133,7 +133,7 @@ var ConfigDefault = Config{ CookieName: "csrf_", CookieSameSite: "Lax", IdleTimeout: 30 * time.Minute, - KeyGenerator: utils.UUIDv4, + KeyGenerator: utils.SecureToken, ErrorHandler: defaultErrorHandler, Extractor: extractors.FromHeader(HeaderName), DisableValueRedaction: false, diff --git a/middleware/csrf/csrf.go b/middleware/csrf/csrf.go index a5b6805734f..aa080b3e74b 100644 --- a/middleware/csrf/csrf.go +++ b/middleware/csrf/csrf.go @@ -138,7 +138,7 @@ func New(config ...Config) fiber.Handler { // If there's no origin, enforce a referer check for HTTPS connections. if errors.Is(err, errOriginNotFound) { - if c.Scheme() == "https" { + if c.Scheme() == schemeHTTPS { err = refererMatchesHost(c, trustedOrigins, trustedSubOrigins) } else { // If it's not HTTPS, clear the error to allow the request to proceed. diff --git a/middleware/requestid/config.go b/middleware/requestid/config.go index 2e47f3b9737..997164fd6fd 100644 --- a/middleware/requestid/config.go +++ b/middleware/requestid/config.go @@ -14,7 +14,7 @@ type Config struct { // Generator defines a function to generate the unique identifier. // - // Optional. Default: utils.UUID + // Optional. Default: utils.SecureToken Generator func() string // Header is the header key where to get/set the unique request ID @@ -24,13 +24,11 @@ type Config struct { } // ConfigDefault is the default config -// It uses a fast UUID generator which will expose the number of -// requests made to the server. To conceal this value for better -// privacy, use the "utils.UUIDv4" generator. +// It uses a secure token generator for better privacy and security. var ConfigDefault = Config{ Next: nil, Header: fiber.HeaderXRequestID, - Generator: utils.UUID, + Generator: utils.SecureToken, } // Helper function to set default values diff --git a/middleware/requestid/requestid.go b/middleware/requestid/requestid.go index 06a35312f91..9019e180652 100644 --- a/middleware/requestid/requestid.go +++ b/middleware/requestid/requestid.go @@ -26,9 +26,6 @@ func New(config ...Config) fiber.Handler { return c.Next() } rid := sanitizeRequestID(c.Get(cfg.Header), cfg.Generator) - if rid == "" { - rid = utils.UUID() - } // Set new id to response header c.Set(cfg.Header, rid) @@ -42,36 +39,21 @@ 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 (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. +// it tries up to three values from the configured generator, then falls back to SecureToken. 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() + rid = generator() if isValidRequestID(rid) { return rid } } - if generator != nil { - for range 3 { - rid = utils.UUID() - if isValidRequestID(rid) { - return rid - } - } - } - - return "" + // Final fallback: SecureToken always produces a valid ID + return utils.SecureToken() } // isValidRequestID reports whether the request ID contains only visible ASCII diff --git a/middleware/requestid/requestid_test.go b/middleware/requestid/requestid_test.go index 0e53da50395..d3e38ce3d66 100644 --- a/middleware/requestid/requestid_test.go +++ b/middleware/requestid/requestid_test.go @@ -25,7 +25,7 @@ func Test_RequestID(t *testing.T) { require.Equal(t, fiber.StatusOK, resp.StatusCode) reqid := resp.Header.Get(fiber.HeaderXRequestID) - require.Len(t, reqid, 36) + require.Len(t, reqid, 43) req := httptest.NewRequest(fiber.MethodGet, "/", http.NoBody) req.Header.Add(fiber.HeaderXRequestID, reqid) @@ -68,7 +68,52 @@ 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") + require.Len(t, rid, 43, "Fallback should produce a SecureToken") +} + +func Test_RequestID_GeneratorAlwaysInvalid(t *testing.T) { + t.Parallel() + + app := fiber.New() + app.Use(New(Config{ + Generator: func() string { + return "invalid\x00id" // Always invalid due to null byte + }, + })) + + 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.Len(t, rid, 43, "Should fall back to SecureToken after 3 invalid attempts") +} + +func Test_RequestID_CustomGenerator(t *testing.T) { + t.Parallel() + + app := fiber.New() + app.Use(New(Config{ + Generator: func() string { + return "custom-valid-id" + }, + })) + + 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.Equal(t, "custom-valid-id", rid) } func Test_isValidRequestID_VisibleASCII(t *testing.T) { @@ -145,3 +190,19 @@ func Test_RequestID_FromContext(t *testing.T) { require.NoError(t, err) require.Equal(t, reqID, ctxVal) } + +func Test_RequestID_FromContext_Empty(t *testing.T) { + t.Parallel() + + app := fiber.New() + // No middleware + + app.Use(func(c fiber.Ctx) error { + ctxVal := FromContext(c) + require.Empty(t, ctxVal) + return c.SendStatus(fiber.StatusOK) + }) + + _, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", http.NoBody)) + require.NoError(t, err) +} diff --git a/middleware/session/config.go b/middleware/session/config.go index 4824ef2bae3..f0a0ff1662b 100644 --- a/middleware/session/config.go +++ b/middleware/session/config.go @@ -32,7 +32,7 @@ type Config struct { // KeyGenerator generates the session key. // - // Optional. Default: utils.UUIDv4 + // Optional. Default: utils.SecureToken KeyGenerator func() string // CookieDomain defines the domain of the session cookie. @@ -92,7 +92,7 @@ type Config struct { // ConfigDefault provides the default configuration. var ConfigDefault = Config{ IdleTimeout: 30 * time.Minute, - KeyGenerator: utils.UUIDv4, + KeyGenerator: utils.SecureToken, Extractor: extractors.FromCookie("session_id"), CookieSameSite: "Lax", } diff --git a/middleware/session/session_test.go b/middleware/session/session_test.go index e8ab39b4e74..8a298d12ac0 100644 --- a/middleware/session/session_test.go +++ b/middleware/session/session_test.go @@ -97,7 +97,7 @@ func Test_Session(t *testing.T) { require.True(t, sess.Fresh()) // this id should be randomly generated as session key was deleted - require.Len(t, sess.ID(), 36) + require.Len(t, sess.ID(), 43) sess.Release() @@ -932,7 +932,7 @@ func Test_Session_Cookie(t *testing.T) { // cookie should be set on Save ( even if empty data ) cookie := ctx.Response().Header.PeekCookie("session_id") require.NotNil(t, cookie) - require.Regexp(t, `^session_id=[a-f0-9\-]{36}; max-age=\d+; path=/; SameSite=Lax$`, string(cookie)) + require.Regexp(t, `^session_id=[A-Za-z0-9\-_]{43}; max-age=\d+; path=/; SameSite=Lax$`, string(cookie)) } // go test -run Test_Session_Cookie_SameSite