Cache Poisoning in Hanami (Ruby)
Cache Poisoning in Hanami with Ruby — how this specific combination creates or exposes the vulnerability
Cache poisoning in Hanami (a Ruby web framework) occurs when an attacker manipulates cache keys or stored responses so that malicious content is served to other users. Because Hanami encourages explicit, developer-controlled caching and relies on Ruby objects for key generation, misconfigured caching logic can reflect attacker input into cache entries.
Three dimensions contribute to the risk in this stack:
- Language: Ruby’s flexible hash keys and string interpolation make it easy to build cache keys from untrusted sources if input is not strictly validated or normalized.
- Framework: Hanami’s architecture promotes use of repositories and view models; if caching is applied at the view or query level without normalizing parameters, an attacker can inject crafted query parameters that change cache entries.
- Application logic: Features such as per-user or per-tenant caching that incorporate identifiers from requests (e.g., subdomain, path, or headers) can inadvertently store responses keyed by attacker-controlled values, leading to cross-user contamination.
For example, using request parameters directly in cache keys without sanitization can cause an attacker to poison the cache for subsequent users. Consider a Hanami view that caches a rendered fragment based on a URL query parameter without validation:
module Web::Views::Items
class Show
include Hanami::View
def call(params)
cache_key = "items/show?category=#{params[:category]}"
cached = cache.fetch(cache_key) do
# ... render item list
end
# ... response
end
end
end
If params[:category] contains user-controlled input and is used verbatim, two different attackers can cause the application to cache and later serve different content under the same key. This can lead to data leakage or incorrect content being served.
Another scenario involves caching HTTP responses or fragments that include sensitive headers or cookies inadvertently reflected into cache storage. Hanami’s default caching integrations do not automatically strip sensitive inputs; developers must ensure that only safe, canonical parameters participate in cache key construction.
Because Hanami applications often compose multiple layers (repositories, entities, views), improper caching at any layer can propagate poisoned entries. Regular scanning with a tool like middleBrick can surface these risks by checking for missing input validation, weak cache-key construction, and improper data exposure in cached responses.
Ruby-Specific Remediation in Hanami
Remediation focuses on strict input validation, canonical normalization, and isolating cache keys from untrusted data. Below are concrete, Ruby-specific fixes tailored for Hanami applications.
- Validate and sanitize inputs: Ensure all inputs that influence cache behavior are validated against an allowlist and normalized.
- Use canonical keys: Build cache keys from normalized, non-user-controlled segments where possible, or hash user input before inclusion.
- Scope cache by tenant and user safely: If caching per user or tenant, use server-side identifiers rather than raw request values.
Example 1: Safe cache key construction
Instead of directly interpolating parameters, sanitize and hash them:
require "digest"
module Web::Views::Items
class Show
include Hanami::View
def call(params)
category = params[:category].to_s.strip.downcase
# Allowlist validation
allowed_categories = %w[books electronics food]
unless allowed_categories.include?(category)
category = "default"
end
# Canonical, hashed key
cache_key = "items/show/#{Digest::SHA256.hexdigest(category)}"
cached = cache.fetch(cache_key) do
# ... render item list
end
# ... response
end
end
end
Example 2: Scoped caching with a canonical identifier
When caching per user or tenant, use a server-side ID and avoid injecting raw request values:
module Web::Views::Dashboard
class Profile
include Hanami::View
def call(current_account)
# current_account is a server-side entity with a stable ID
cache_key = "account/#{current_account.id}/profile"
cached = cache.fetch(cache_key) do
# ... render profile
end
# ... response
end
end
end
Example 3: Middleware-level normalization (optional pattern)
For applications that need to enforce canonical request shapes before caching, use a Hanami middleware or service object to normalize parameters:
class CacheKeyNormalizer
def initialize(app)
@app = app
end
def call(env)
req = Rack::Request.new(env)
# Normalize query parameters used for caching
if req.get? && req.params["category"]
req.params["category"] = req.params["category"].strip.downcase
end
@app.call(env)
end
end
Register the middleware in your Hanami application configuration if you choose this approach. These patterns reduce the risk of cache poisoning by ensuring that only validated, canonical data influences cache storage.