mirror of
https://github.com/zap-proto/fiber.git
synced 2026-10-30 15:06:23 +00:00
Fork of gofiber/fiber v3 based on the v3.2.0 tag. Rewrites all self-imports github.com/gofiber/fiber/v3 -> github.com/zap-proto/fiber/v3. Upstream gofiber/schema and gofiber/utils/v2 dependencies are left untouched. Co-authored-by: Hanzo Dev <[email protected]>
598 lines
19 KiB
Go
598 lines
19 KiB
Go
package cache
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"sync"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/zap-proto/fiber/v3"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// Test_Cache_Security_DoS_ExcessiveQueryParams tests protection against DoS via excessive query parameters
|
|
func Test_Cache_Security_DoS_ExcessiveQueryParams(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
app := fiber.New()
|
|
app.Use(New(Config{Expiration: 1 * time.Hour}))
|
|
|
|
var count int
|
|
app.Get("/", func(c fiber.Ctx) error {
|
|
count++
|
|
return c.SendString("response")
|
|
})
|
|
|
|
// Build a URL with more than maxQueryParams (128) parameters
|
|
queryParams := make([]string, 150)
|
|
for i := 0; i < 150; i++ {
|
|
queryParams[i] = fmt.Sprintf("param%d=value%d", i, i)
|
|
}
|
|
url := "/?" + strings.Join(queryParams, "&")
|
|
|
|
// First request should be cached (but with hashed key due to param limit)
|
|
resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, url, http.NoBody))
|
|
require.NoError(t, err)
|
|
require.Equal(t, fiber.StatusOK, resp.StatusCode)
|
|
require.Equal(t, cacheMiss, resp.Header.Get("X-Cache"))
|
|
require.Equal(t, 1, count)
|
|
|
|
// Second request should hit cache
|
|
resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, url, http.NoBody))
|
|
require.NoError(t, err)
|
|
require.Equal(t, fiber.StatusOK, resp.StatusCode)
|
|
require.Equal(t, cacheHit, resp.Header.Get("X-Cache"))
|
|
require.Equal(t, 1, count, "Handler should not be called on cache hit")
|
|
}
|
|
|
|
// Test_Cache_Security_DoS_ExcessiveQueryBuffer tests protection against DoS via query buffer growth
|
|
func Test_Cache_Security_DoS_ExcessiveQueryBuffer(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
app := fiber.New(fiber.Config{
|
|
ReadBufferSize: 16384, // Increase buffer to accommodate large query strings for testing
|
|
})
|
|
app.Use(New(Config{Expiration: 1 * time.Hour}))
|
|
|
|
var count int
|
|
app.Get("/", func(c fiber.Ctx) error {
|
|
count++
|
|
return c.SendString("response")
|
|
})
|
|
|
|
// Build a URL with parameters that expand significantly when URL-escaped
|
|
// Using characters that require escaping (e.g., "=" becomes "%3D", 3x larger)
|
|
specialChars := strings.Repeat("=", 50) // Each "=" becomes "%3D"
|
|
queryParams := make([]string, 30)
|
|
for i := 0; i < 30; i++ {
|
|
queryParams[i] = fmt.Sprintf("key%d=%s", i, specialChars)
|
|
}
|
|
url := "/?" + strings.Join(queryParams, "&")
|
|
|
|
// Request should not crash or exhaust memory
|
|
resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, url, http.NoBody))
|
|
require.NoError(t, err)
|
|
require.Equal(t, fiber.StatusOK, resp.StatusCode)
|
|
require.Equal(t, cacheMiss, resp.Header.Get("X-Cache"))
|
|
|
|
// Second request should hit cache (with hashed key due to buffer limit)
|
|
resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, url, http.NoBody))
|
|
require.NoError(t, err)
|
|
require.Equal(t, fiber.StatusOK, resp.StatusCode)
|
|
require.Equal(t, cacheHit, resp.Header.Get("X-Cache"))
|
|
require.Equal(t, 1, count, "Handler should not be called on cache hit")
|
|
}
|
|
|
|
// Test_Cache_Security_DoS_ExcessiveVaryHeaders tests protection against DoS via excessive Vary headers
|
|
func Test_Cache_Security_DoS_ExcessiveVaryHeaders(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
app := fiber.New()
|
|
app.Use(New(Config{Expiration: 1 * time.Hour}))
|
|
|
|
var count int
|
|
app.Get("/", func(c fiber.Ctx) error {
|
|
count++
|
|
// Generate more than maxVaryHeaders (32) headers
|
|
varyHeaders := make([]string, 50)
|
|
for i := 0; i < 50; i++ {
|
|
varyHeaders[i] = fmt.Sprintf("X-Custom-Header-%d", i)
|
|
}
|
|
c.Set(fiber.HeaderVary, strings.Join(varyHeaders, ", "))
|
|
return c.SendString("response")
|
|
})
|
|
|
|
// First request should not be cached due to excessive Vary headers (treated as Vary: *)
|
|
resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", http.NoBody))
|
|
require.NoError(t, err)
|
|
require.Equal(t, fiber.StatusOK, resp.StatusCode)
|
|
require.Equal(t, cacheUnreachable, resp.Header.Get("X-Cache"))
|
|
require.Equal(t, 1, count)
|
|
|
|
// Second request should also not be cached
|
|
resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/", http.NoBody))
|
|
require.NoError(t, err)
|
|
require.Equal(t, fiber.StatusOK, resp.StatusCode)
|
|
require.Equal(t, cacheUnreachable, resp.Header.Get("X-Cache"))
|
|
require.Equal(t, 2, count, "Handler should be called each time when uncacheable")
|
|
}
|
|
|
|
// Test_Cache_Security_LongPathSegmentHashed tests that long path segments are properly hashed
|
|
func Test_Cache_Security_LongPathSegmentHashed(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
app := fiber.New()
|
|
app.Use(New(Config{Expiration: 1 * time.Hour}))
|
|
|
|
var count int
|
|
// Create a very long path (>192 chars which is maxKeyDimensionSegmentLength)
|
|
longPath := "/" + strings.Repeat("a", 300)
|
|
app.Get(longPath, func(c fiber.Ctx) error {
|
|
count++
|
|
return c.SendString("response")
|
|
})
|
|
|
|
// First request
|
|
resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, longPath, http.NoBody))
|
|
require.NoError(t, err)
|
|
require.Equal(t, fiber.StatusOK, resp.StatusCode)
|
|
require.Equal(t, cacheMiss, resp.Header.Get("X-Cache"))
|
|
require.Equal(t, 1, count)
|
|
|
|
// Second request should hit cache with hashed key
|
|
resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, longPath, http.NoBody))
|
|
require.NoError(t, err)
|
|
require.Equal(t, fiber.StatusOK, resp.StatusCode)
|
|
require.Equal(t, cacheHit, resp.Header.Get("X-Cache"))
|
|
require.Equal(t, 1, count, "Handler should not be called on cache hit")
|
|
}
|
|
|
|
// Test_Cache_Security_MalformedQueryString tests handling of malformed query strings
|
|
func Test_Cache_Security_MalformedQueryString(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
app := fiber.New()
|
|
app.Use(New(Config{Expiration: 1 * time.Hour}))
|
|
|
|
var count int
|
|
app.Get("/", func(c fiber.Ctx) error {
|
|
count++
|
|
return c.SendString("response")
|
|
})
|
|
|
|
// Malformed query string with invalid encoding
|
|
malformedURL := "/?invalid=%ZZ%XX"
|
|
|
|
// Should not crash and should handle gracefully
|
|
resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, malformedURL, http.NoBody))
|
|
require.NoError(t, err)
|
|
require.Equal(t, fiber.StatusOK, resp.StatusCode)
|
|
require.Equal(t, cacheMiss, resp.Header.Get("X-Cache"))
|
|
|
|
// Second request should hit cache (malformed query is hashed as-is)
|
|
resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, malformedURL, http.NoBody))
|
|
require.NoError(t, err)
|
|
require.Equal(t, fiber.StatusOK, resp.StatusCode)
|
|
require.Equal(t, cacheHit, resp.Header.Get("X-Cache"))
|
|
require.Equal(t, 1, count)
|
|
}
|
|
|
|
// Test_Cache_Security_HeaderInjection tests that header values cannot inject into cache keys
|
|
func Test_Cache_Security_HeaderInjection(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
app := fiber.New()
|
|
app.Use(New(Config{
|
|
Expiration: 1 * time.Hour,
|
|
KeyHeaders: []string{"X-Custom-Header"},
|
|
}))
|
|
|
|
var count int
|
|
app.Get("/", func(c fiber.Ctx) error {
|
|
count++
|
|
return c.SendString("response-" + c.Get("X-Custom-Header"))
|
|
})
|
|
|
|
// Try to inject delimiters used in key generation (avoiding null bytes which are invalid in HTTP)
|
|
injectionAttempts := []string{
|
|
"value|q=injected",
|
|
"value|h=injected",
|
|
"value|c=injected",
|
|
"value|vary|injected",
|
|
"sha256:fakehash",
|
|
}
|
|
|
|
for _, injection := range injectionAttempts {
|
|
req := httptest.NewRequest(fiber.MethodGet, "/", http.NoBody)
|
|
req.Header.Set("X-Custom-Header", injection)
|
|
|
|
resp, err := app.Test(req)
|
|
require.NoError(t, err)
|
|
require.Equal(t, fiber.StatusOK, resp.StatusCode)
|
|
|
|
body, err := io.ReadAll(resp.Body)
|
|
require.NoError(t, err)
|
|
require.Equal(t, "response-"+injection, string(body))
|
|
}
|
|
|
|
// Each injection attempt should create a distinct cache entry
|
|
// (no collision through injection)
|
|
require.Equal(t, len(injectionAttempts), count)
|
|
}
|
|
|
|
// Test_Cache_Security_CookieInjection tests that cookie values cannot inject into cache keys
|
|
func Test_Cache_Security_CookieInjection(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
app := fiber.New()
|
|
app.Use(New(Config{
|
|
Expiration: 1 * time.Hour,
|
|
KeyCookies: []string{"session"},
|
|
}))
|
|
|
|
var count int
|
|
app.Get("/", func(c fiber.Ctx) error {
|
|
count++
|
|
return c.SendString("response-" + c.Cookies("session"))
|
|
})
|
|
|
|
// Try to inject delimiters (avoiding null bytes which are invalid in HTTP)
|
|
injectionAttempts := []string{
|
|
"value|injected",
|
|
"value:injected",
|
|
"value|vary|injected",
|
|
"sha256:fakehash",
|
|
}
|
|
|
|
for _, injection := range injectionAttempts {
|
|
req := httptest.NewRequest(fiber.MethodGet, "/", http.NoBody)
|
|
req.Header.Set("Cookie", "session="+injection)
|
|
|
|
resp, err := app.Test(req)
|
|
require.NoError(t, err)
|
|
require.Equal(t, fiber.StatusOK, resp.StatusCode)
|
|
}
|
|
|
|
// Each injection attempt should create distinct cache entries
|
|
require.Equal(t, len(injectionAttempts), count)
|
|
}
|
|
|
|
// Test_Cache_Security_Concurrent_QueryParamDoS tests concurrent requests with excessive params
|
|
func Test_Cache_Security_Concurrent_QueryParamDoS(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
app := fiber.New()
|
|
app.Use(New(Config{Expiration: 1 * time.Hour}))
|
|
|
|
var count atomic.Int32
|
|
app.Get("/", func(c fiber.Ctx) error {
|
|
count.Add(1)
|
|
return c.SendString("response")
|
|
})
|
|
|
|
// Build URL with excessive parameters
|
|
queryParams := make([]string, 200)
|
|
for i := 0; i < 200; i++ {
|
|
queryParams[i] = fmt.Sprintf("p%d=v%d", i, i)
|
|
}
|
|
url := "/?" + strings.Join(queryParams, "&")
|
|
|
|
// Run concurrent requests
|
|
const numRequests = 100
|
|
var wg sync.WaitGroup
|
|
wg.Add(numRequests)
|
|
|
|
// Track errors in goroutines
|
|
var errCount atomic.Int32
|
|
|
|
for i := 0; i < numRequests; i++ {
|
|
go func() {
|
|
defer wg.Done()
|
|
resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, url, http.NoBody))
|
|
if err != nil {
|
|
errCount.Add(1)
|
|
return
|
|
}
|
|
if resp.StatusCode != fiber.StatusOK {
|
|
errCount.Add(1)
|
|
}
|
|
}()
|
|
}
|
|
|
|
wg.Wait()
|
|
|
|
// Should have handled all requests without crashing or errors
|
|
require.Equal(t, int32(0), errCount.Load(), "No errors should occur during concurrent requests")
|
|
|
|
// First request creates cache, rest should hit it
|
|
require.LessOrEqual(t, count.Load(), int32(numRequests))
|
|
}
|
|
|
|
// Test_Cache_Security_QueryParameterRepeated tests handling of repeated query parameters
|
|
func Test_Cache_Security_QueryParameterRepeated(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
app := fiber.New()
|
|
app.Use(New(Config{Expiration: 1 * time.Hour}))
|
|
|
|
var count int
|
|
app.Get("/", func(c fiber.Ctx) error {
|
|
count++
|
|
return c.SendString("response")
|
|
})
|
|
|
|
// Test with 100 values for the same parameter
|
|
values := make([]string, 100)
|
|
for i := 0; i < 100; i++ {
|
|
values[i] = fmt.Sprintf("key=%d", i)
|
|
}
|
|
url := "/?" + strings.Join(values, "&")
|
|
|
|
resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, url, http.NoBody))
|
|
require.NoError(t, err)
|
|
require.Equal(t, fiber.StatusOK, resp.StatusCode)
|
|
require.Equal(t, cacheMiss, resp.Header.Get("X-Cache"))
|
|
|
|
// Second request should hit cache (hashed due to param count)
|
|
resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, url, http.NoBody))
|
|
require.NoError(t, err)
|
|
require.Equal(t, fiber.StatusOK, resp.StatusCode)
|
|
require.Equal(t, cacheHit, resp.Header.Get("X-Cache"))
|
|
require.Equal(t, 1, count)
|
|
}
|
|
|
|
// Test_Cache_Security_EmptyVaryHeaders tests handling of empty vary header entries
|
|
func Test_Cache_Security_EmptyVaryHeaders(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
app := fiber.New()
|
|
app.Use(New(Config{Expiration: 1 * time.Hour}))
|
|
|
|
var count int
|
|
app.Get("/", func(c fiber.Ctx) error {
|
|
count++
|
|
// Empty and whitespace vary entries should be ignored
|
|
c.Set(fiber.HeaderVary, "Accept, , , , ,Accept-Encoding")
|
|
return c.SendString("response")
|
|
})
|
|
|
|
resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", http.NoBody))
|
|
require.NoError(t, err)
|
|
require.Equal(t, fiber.StatusOK, resp.StatusCode)
|
|
require.Equal(t, cacheMiss, resp.Header.Get("X-Cache"))
|
|
|
|
resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/", http.NoBody))
|
|
require.NoError(t, err)
|
|
require.Equal(t, fiber.StatusOK, resp.StatusCode)
|
|
require.Equal(t, cacheHit, resp.Header.Get("X-Cache"))
|
|
require.Equal(t, 1, count)
|
|
}
|
|
|
|
// Test_Cache_Security_MultiDimensionInjection tests injection with multiple headers and cookies
|
|
func Test_Cache_Security_MultiDimensionInjection(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
app := fiber.New()
|
|
app.Use(New(Config{
|
|
Expiration: 1 * time.Hour,
|
|
KeyHeaders: []string{"X-Header-1", "X-Header-2"},
|
|
KeyCookies: []string{"cookie1", "cookie2"},
|
|
}))
|
|
|
|
var count int
|
|
app.Get("/", func(c fiber.Ctx) error {
|
|
count++
|
|
return c.SendString(fmt.Sprintf("h1=%s,h2=%s,c1=%s,c2=%s",
|
|
c.Get("X-Header-1"), c.Get("X-Header-2"),
|
|
c.Cookies("cookie1"), c.Cookies("cookie2")))
|
|
})
|
|
|
|
// Test combinations that should create distinct cache entries
|
|
testCases := []struct {
|
|
header1 string
|
|
header2 string
|
|
cookie1 string
|
|
cookie2 string
|
|
}{
|
|
// Normal values
|
|
{"value1", "value2", "cookie1", "cookie2"},
|
|
// Injection attempts with delimiters
|
|
{"value|injected", "normal", "normal", "normal"},
|
|
{"normal", "value:injected", "normal", "normal"},
|
|
{"normal", "normal", "value|injected", "normal"},
|
|
{"normal", "normal", "normal", "value:injected"},
|
|
// Multiple delimiters
|
|
{"value|with|pipes", "value:with:colons", "normal", "normal"},
|
|
// Backslashes
|
|
{"value\\with\\backslash", "normal", "normal", "normal"},
|
|
{"normal", "normal", "cookie\\value", "normal"},
|
|
// Combined escapes
|
|
{"value\\|mixed", "normal", "normal", "normal"},
|
|
{"normal", "value\\:mixed", "normal", "normal"},
|
|
}
|
|
|
|
for i, tc := range testCases {
|
|
req := httptest.NewRequest(fiber.MethodGet, "/", http.NoBody)
|
|
req.Header.Set("X-Header-1", tc.header1)
|
|
req.Header.Set("X-Header-2", tc.header2)
|
|
req.Header.Set("Cookie", fmt.Sprintf("cookie1=%s; cookie2=%s", tc.cookie1, tc.cookie2))
|
|
|
|
resp, err := app.Test(req)
|
|
require.NoError(t, err, "Test case %d failed", i)
|
|
require.Equal(t, fiber.StatusOK, resp.StatusCode, "Test case %d failed", i)
|
|
|
|
body, err := io.ReadAll(resp.Body)
|
|
require.NoError(t, err, "Test case %d failed", i)
|
|
expected := fmt.Sprintf("h1=%s,h2=%s,c1=%s,c2=%s", tc.header1, tc.header2, tc.cookie1, tc.cookie2)
|
|
require.Equal(t, expected, string(body), "Test case %d failed", i)
|
|
}
|
|
|
|
// Each test case should create a distinct cache entry (no collisions)
|
|
require.Equal(t, len(testCases), count, "All test cases should create distinct cache entries")
|
|
}
|
|
|
|
// Test_Cache_Security_BackslashEscaping tests that backslashes are properly escaped
|
|
func Test_Cache_Security_BackslashEscaping(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
app := fiber.New()
|
|
app.Use(New(Config{
|
|
Expiration: 1 * time.Hour,
|
|
KeyHeaders: []string{"X-Custom-Header"},
|
|
}))
|
|
|
|
var count int
|
|
app.Get("/", func(c fiber.Ctx) error {
|
|
count++
|
|
return c.SendString("response-" + c.Get("X-Custom-Header"))
|
|
})
|
|
|
|
// Test backslash escaping scenarios
|
|
testCases := []string{
|
|
"\\", // Single backslash
|
|
"\\\\", // Double backslash
|
|
"\\p", // Escaped pipe character
|
|
"\\c", // Escaped colon character
|
|
"value\\|test", // Backslash before pipe
|
|
"value\\:test", // Backslash before colon
|
|
"\\\\p", // Double backslash then p
|
|
"\\\\c", // Double backslash then c
|
|
}
|
|
|
|
for i, tc := range testCases {
|
|
req := httptest.NewRequest(fiber.MethodGet, "/", http.NoBody)
|
|
req.Header.Set("X-Custom-Header", tc)
|
|
|
|
resp, err := app.Test(req)
|
|
require.NoError(t, err, "Test case %d (%s) failed", i, tc)
|
|
require.Equal(t, fiber.StatusOK, resp.StatusCode, "Test case %d (%s) failed", i, tc)
|
|
|
|
body, err := io.ReadAll(resp.Body)
|
|
require.NoError(t, err, "Test case %d (%s) failed", i, tc)
|
|
require.Equal(t, "response-"+tc, string(body), "Test case %d (%s) failed", i, tc)
|
|
}
|
|
|
|
// Each test case should create a distinct cache entry
|
|
require.Equal(t, len(testCases), count, "All backslash test cases should create distinct cache entries")
|
|
}
|
|
|
|
// Test_Cache_Security_DelimiterCollisionPrevention verifies that escaped delimiters don't collide
|
|
func Test_Cache_Security_DelimiterCollisionPrevention(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
app := fiber.New()
|
|
app.Use(New(Config{
|
|
Expiration: 1 * time.Hour,
|
|
KeyHeaders: []string{"X-Header"},
|
|
KeyCookies: []string{"session"},
|
|
}))
|
|
|
|
var responses []string
|
|
app.Get("/", func(c fiber.Ctx) error {
|
|
response := fmt.Sprintf("h=%s,c=%s", c.Get("X-Header"), c.Cookies("session"))
|
|
responses = append(responses, response)
|
|
return c.SendString(response)
|
|
})
|
|
|
|
// These pairs should NOT collide after escaping
|
|
testCases := []struct {
|
|
header string
|
|
cookie string
|
|
}{
|
|
{"value1|part2", "normal"}, // Pipe in header
|
|
{"value1", "part2|normal"}, // Different structure but similar
|
|
{"value:test", "cookie"}, // Colon in header
|
|
{"value", "test:cookie"}, // Colon in cookie
|
|
{"a|b:c", "d"}, // Mixed delimiters
|
|
{"a", "b:c|d"}, // Different arrangement
|
|
{"\\|", "test"}, // Backslash-pipe sequence
|
|
{"\\", "|test"}, // Separated backslash and pipe
|
|
}
|
|
|
|
for i, tc := range testCases {
|
|
req := httptest.NewRequest(fiber.MethodGet, "/", http.NoBody)
|
|
req.Header.Set("X-Header", tc.header)
|
|
req.Header.Set("Cookie", "session="+tc.cookie)
|
|
|
|
resp, err := app.Test(req)
|
|
require.NoError(t, err, "Test case %d failed", i)
|
|
require.Equal(t, fiber.StatusOK, resp.StatusCode, "Test case %d failed", i)
|
|
|
|
body, err := io.ReadAll(resp.Body)
|
|
require.NoError(t, err, "Test case %d failed", i)
|
|
expected := fmt.Sprintf("h=%s,c=%s", tc.header, tc.cookie)
|
|
require.Equal(t, expected, string(body), "Test case %d failed", i)
|
|
}
|
|
|
|
// All test cases should create distinct cache entries (no collisions from injection)
|
|
require.Len(t, responses, len(testCases), "All test cases should create distinct cache entries")
|
|
|
|
// Verify all responses are unique
|
|
seen := make(map[string]bool)
|
|
for _, resp := range responses {
|
|
require.False(t, seen[resp], "Response should be unique: %s", resp)
|
|
seen[resp] = true
|
|
}
|
|
}
|
|
|
|
// Test_Cache_Security_EscapeKeyDelimiters_Unit is a direct regression test for the
|
|
// escapeKeyDelimiters function, ensuring backslashes are escaped to prevent collisions
|
|
// between e.g. a literal "a\pb" and the escaped form of "a|b" → "a\pb".
|
|
func Test_Cache_Security_EscapeKeyDelimiters_Unit(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
input string
|
|
expected string
|
|
}{
|
|
// Fast path: no special characters
|
|
{"hello", "hello"},
|
|
{"", ""},
|
|
{"foo/bar?baz=1", "foo/bar?baz=1"},
|
|
// Pipe escaping
|
|
{"a|b", "a\\pb"},
|
|
// Colon escaping
|
|
{"a:b", "a\\cb"},
|
|
// Backslash escaping (regression: fast path must also check for \)
|
|
{"a\\b", "a\\\\b"},
|
|
// Backslash-pipe sequence must not collide with escaped pipe
|
|
{"a\\pb", "a\\\\pb"}, // literal \p → \\p (differs from escaped | → \p)
|
|
{"a\\cb", "a\\\\cb"}, // literal \c → \\c (differs from escaped : → \c)
|
|
// Mixed delimiters
|
|
{"k|v:w\\x", "k\\pv\\cw\\\\x"},
|
|
// Multiple consecutive
|
|
{"||", "\\p\\p"},
|
|
{"::", "\\c\\c"},
|
|
{"\\\\", "\\\\\\\\"},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(fmt.Sprintf("escape_%q", tt.input), func(t *testing.T) {
|
|
t.Parallel()
|
|
result := escapeKeyDelimiters(tt.input)
|
|
require.Equal(t, tt.expected, result, "escapeKeyDelimiters(%q)", tt.input)
|
|
})
|
|
}
|
|
|
|
// Verify no collisions between pairs that would collide without backslash escaping
|
|
collisionPairs := [][2]string{
|
|
{"a\\pb", "a|b"}, // literal \p vs escaped |
|
|
{"a\\cb", "a:b"}, // literal \c vs escaped :
|
|
{"\\\\", "\\"}, // double backslash vs single
|
|
{"x\\py", "x|y"},
|
|
}
|
|
for _, pair := range collisionPairs {
|
|
t.Run(fmt.Sprintf("no_collision_%q_vs_%q", pair[0], pair[1]), func(t *testing.T) {
|
|
t.Parallel()
|
|
a := escapeKeyDelimiters(pair[0])
|
|
b := escapeKeyDelimiters(pair[1])
|
|
require.NotEqual(t, a, b, "escapeKeyDelimiters(%q) must differ from escapeKeyDelimiters(%q)", pair[0], pair[1])
|
|
})
|
|
}
|
|
}
|