Cache Poisoning in Echo Go
How Cache Poisoning Manifests in Echo Go
Cache poisoning in Echo Go typically occurs when unvalidated user input is incorporated into cache keys or cacheable responses without proper sanitization. The framework's flexible middleware chain and parameter binding make it particularly vulnerable to this class of attack.
One common manifestation involves path parameters that contain special characters. Consider an endpoint that accepts a user ID and caches responses based on the raw parameter value:
e.GET("/user/:id", func(c echo.Context) error {
id := c.Param("id")
cacheKey := fmt.Sprintf("user-%s", id)
if cached, exists := cache.Get(cacheKey); exists {
return c.JSON(http.StatusOK, cached)
}
user := getUserFromDB(id)
cache.Set(cacheKey, user, 300*time.Second)
return c.JSON(http.StatusOK, user)
})An attacker could request /user/foo/../bar or use URL-encoded characters to manipulate the cache key. The cache might store different user data under keys that resolve to the same resource, or worse, allow path traversal to access unauthorized data.
Query parameter manipulation presents another attack vector. Echo Go's automatic binding of query parameters to struct fields can lead to cache poisoning when parameters are used in cache keys:
type UserQuery struct {
ID string `query:"id"`
Sort string `query:"sort"`
Limit int `query:"limit"`
}
func getUser(c echo.Context) error {
var q UserQuery
if err := c.Bind(&q); err != nil {
return err
}
cacheKey := fmt.Sprintf("user-%s-%s-%d", q.ID, q.Sort, q.Limit)
if cached, exists := cache.Get(cacheKey); exists {
return c.JSON(http.StatusOK, cached)
}
user := queryUserDB(q.ID, q.Sort, q.Limit)
cache.Set(cacheKey, user, 300*time.Second)
return c.JSON(http.StatusOK, user)
}Attackers can manipulate the sort parameter to include characters that break cache key generation or cause cache collisions. More sophisticated attacks involve using Unicode characters that normalize differently but produce the same cache key, allowing attackers to poison cache entries for other users.
Header-based cache poisoning is particularly dangerous in Echo Go because the framework provides direct access to request headers. When caching decisions are made based on header values:
e.GET("/api/data", func(c echo.Context) error {
accept := c.Request().Header.Get("Accept")
cacheKey := fmt.Sprintf("data-%s", accept)
if cached, exists := cache.Get(cacheKey); exists {
return c.JSON(http.StatusOK, cached)
}
data := generateData()
cache.Set(cacheKey, data, 300*time.Second)
return c.JSON(http.StatusOK, data)
})An attacker can set the Accept header to values containing special characters or manipulate content negotiation to poison cache entries. This becomes especially problematic when combined with Echo Go's content negotiation features.
Echo Go-Specific Detection
Detecting cache poisoning in Echo Go applications requires a combination of static analysis and runtime testing. The framework's middleware architecture provides natural interception points for security scanning.
middleBrick's scanner specifically targets Echo Go applications by examining route definitions and parameter handling patterns. The scanner identifies endpoints that use path parameters, query parameters, or headers in cache key generation without proper validation. Here's how you would use middleBrick to scan an Echo Go API:
middlebrick scan http://localhost:8080/api/user
# Output includes:
# - Cache key generation analysis
# - Parameter validation assessment
# - Header manipulation vulnerability detection
# - Unicode normalization issuesFor runtime detection, Echo Go's middleware chain allows you to inject cache poisoning detection logic. A custom middleware can monitor cache key generation patterns:
func cachePoisoningDetectionMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
// Check for suspicious characters in path parameters
for _, param := range c.ParamNames() {
value := c.Param(param)
if containsSuspiciousChars(value) {
log.Warn().Str("param", param).Str("value", value).Msg("Potential cache poisoning")
}
}
// Check query parameters
query := c.QueryParams()
for key, values := range query {
for _, value := range values {
if containsUnicodeVariants(value) {
log.Warn().Str("query", key).Str("value", value).Msg("Unicode cache poisoning risk")
}
}
}
return next(c)
}
}The scanner also examines Echo Go's automatic content negotiation and binding features. When the framework automatically binds query parameters to struct fields, it can create cache poisoning opportunities if the struct tags aren't properly validated. middleBrick specifically checks for:
- Unvalidated path parameters used in cache keys
- Query parameters with dangerous characters (../, null bytes, control characters)
- Header values used for caching decisions without sanitization
- Unicode normalization issues in cache keys
- Cache key collisions from parameter manipulation
Echo Go's logging capabilities can be enhanced to detect cache poisoning attempts. The framework's structured logging works well with security monitoring tools:
e.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{
Format: "${time_rfc3339} ${status} ${method} ${uri} ${cache_key} ${latency}",
}))This allows security teams to monitor cache key patterns and detect anomalies that might indicate poisoning attempts.
Echo Go-Specific Remediation
Remediating cache poisoning in Echo Go requires a defense-in-depth approach that leverages the framework's built-in validation and sanitization features. The first line of defense is proper parameter validation using Echo Go's validator middleware.
Echo Go's validator middleware integrates with Go's struct validation tags to ensure parameters meet security requirements before they're used in cache keys:
type SafeUserQuery struct {
ID string `validate:"alphanum,max=32"` // Only alphanumeric, max 32 chars
Sort string `validate:"oneof=name id created"` // Whitelist allowed values
Limit int `validate:"min=1,max=100"` // Range validation
}
func getUser(c echo.Context) error {
var q SafeUserQuery
if err := c.Bind(&q); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "Invalid parameters"})
}
if err := c.Validate(&q); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "Parameter validation failed"})
}
cacheKey := fmt.Sprintf("user-%s-%s-%d", q.ID, q.Sort, q.Limit)
if cached, exists := cache.Get(cacheKey); exists {
return c.JSON(http.StatusOK, cached)
}
user := queryUserDB(q.ID, q.Sort, q.Limit)
cache.Set(cacheKey, user, 300*time.Second)
return c.JSON(http.StatusOK, user)
}For path parameters, Echo Go's route constraints provide an additional layer of protection. You can define custom constraints that validate parameter formats before they reach your handler:
func safeIDValidator(id string) bool {
// Only allow alphanumeric IDs
matched, _ := regexp.MatchString(`^[a-zA-Z0-9_-]+$`, id)
return matched
}
e.GET("/user/:id", getUser).ParamValidator("id", safeIDValidator)Echo Go's middleware system allows you to create a centralized cache key sanitizer that all endpoints use:
type CacheKeySanitizer struct {
allowedChars *regexp.Regexp
}
func NewCacheKeySanitizer() *CacheKeySanitizer {
return &CacheKeySanitizer{
allowedChars: regexp.MustCompile(`^[a-zA-Z0-9_-]+$`), // Allow only safe characters
}
}
func (s *CacheKeySanitizer) Sanitize(input string) string {
if s.allowedChars.MatchString(input) {
return input
}
// Replace unsafe characters with a safe placeholder
return "invalid"
}
func cacheKeyMiddleware(sanitizer *CacheKeySanitizer) echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
// Sanitize path parameters
for _, param := range c.ParamNames() {
sanitized := sanitizer.Sanitize(c.Param(param))
c.SetParamNames(param) // This is illustrative; actual implementation may vary
c.SetParamValues(sanitized)
}
return next(c)
}
}
}For header-based cache poisoning, Echo Go allows you to whitelist acceptable header values and normalize them before use:
func normalizeAcceptHeader(header string) string {
// Normalize to lowercase and whitelist known MIME types
normalized := strings.ToLower(header)
allowed := []string{"application/json", "text/html", "application/xml"}
for _, mime := range allowed {
if strings.Contains(normalized, mime) {
return mime
}
}
return "application/json" // Default to safe value
}
e.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
accept := c.Request().Header.Get("Accept")
normalized := normalizeAcceptHeader(accept)
c.Request().Header.Set("Accept", normalized)
return next(c)
}
})Echo Go's context binding can be enhanced with custom validators for complex types:
type SafeQueryParams struct {
Filter string `query:"filter" validate:"alphanum,max=50"`
Order string `query:"order" validate:"oneof=asc desc"`
}
// Custom validator for complex validation logic
func (q *SafeQueryParams) Validate() error {
// Additional custom validation
if strings.Contains(q.Filter, "..") {
return errors.New("filter contains invalid characters")
}
return nil
}The framework's error handling can be configured to provide consistent responses for validation failures, preventing information leakage that might aid attackers:
e.HTTPErrorHandler = func(err error, c echo.Context) {
if he, ok := err.(*echo.HTTPError); ok {
c.JSON(he.Code, map[string]string{"error": "Invalid request"})
} else {
c.JSON(http.StatusInternalServerError, map[string]string{"error": "Internal server error"})
}
}