Cache Poisoning in Rails (Ruby)
Cache Poisoning in Rails with Ruby — how this specific combination creates or exposes the vulnerability
Cache poisoning in Ruby on Rails occurs when an attacker causes a cache entry to store malicious or incorrect data that is later served to other users. This typically arises when cached responses include user-controlled data that should not be part of the cache key or when unsafe caching practices combine with dynamic content in Rails.
Rails provides fragment, page, and action caching, and these mechanisms rely on cache keys that can incorporate parameters from the request. If parameters such as params[:user_id] or params[:host] are used directly in cache keys without strict validation, an attacker can manipulate these values to poison the cache for subsequent users. For example, a cached page fragment that includes the requester’s hostname or subdomain may inadvertently share cached content across different tenants or users if the cache key does not isolate values correctly.
Ruby’s string interpolation and symbol-to-proc patterns commonly used in cache key generation can inadvertently introduce risks if the developer does not sanitize inputs. Consider a scenario where a cached partial includes the current locale or a user-provided sort option:
# Unsafe: user-controlled value used directly in cache key
cache ["products", params[:category], params[:sort_by]] do
render @products
end
An attacker could change params[:sort_by] to arbitrary values, causing different users to receive cached responses intended for other users or contexts. In multi-tenant setups, failing to include the tenant identifier in the cache key can lead to one tenant’s data being served to another tenant, a form of BOLA that manifests through the caching layer.
Rails’ default caching stores entries on the filesystem or in memory, and if the cache key does not incorporate sufficient request isolation, the poisoned entry may persist across requests and users. This is especially dangerous when the cached content includes sensitive headers, cookies, or per-user information that should not be shared. The Ruby on Rails caching stack does not automatically validate that cached data is safe for reuse; it relies on the developer to construct cache keys that correctly scope data.
Additional risk arises when using low-level caching with raw Ruby objects that may contain instance variables modified by request-specific logic. If such objects are cached without deep duplication or proper serialization safeguards, mutated state can leak across requests. Therefore, careful attention to what is included in cache keys and how cached data is constructed is essential when using Rails with Ruby to avoid cache poisoning.
Ruby-Specific Remediation in Rails — concrete code fixes
To prevent cache poisoning in Rails with Ruby, ensure cache keys are deterministic, scoped, and free of attacker-influenced values unless explicitly sanitized. Use strong parameter validation and isolate tenant or user context explicitly in cache key construction.
1. Isolate tenant or user context in cache keys
Include a verified tenant identifier or current user ID in the cache key to prevent cross-user contamination. Avoid using raw params without filtering.
# Safe: explicitly scope by current_user.id and a sanitized category
category = params[:category].presence || "default"
cache ["products", current_user.id, category] do
render @products
end
2. Sanitize and normalize inputs used in cache keys
Normalize user input to a safe set of values and avoid injecting raw strings into cache keys. Use Rails’ built-in helpers to transform input safely.
# Safe: whitelist sort options and use a symbol
SORT_OPTIONS = %w[asc desc created_at updated_at]
sort_by = SORT_OPTIONS.include?(params[:sort_by]) ? params[:sort_by] : "created_at"
cache ["products", sort_by.to_sym] do
render @products
end
3. Avoid request-specific headers or cookies in cache keys
Do not include values like request.host, request.subdomain, or cookies in cache keys unless you explicitly intend to scope by them and have validated their format.
# Safe: include tenant subdomain only after validation
subdomain = request.subdomain.match?(/[a-z0-9]+/) ? request.subdomain : "public"
cache ["dashboard", subdomain] do
render @widgets
end
4. Use Russian doll nesting with explicit dependencies
When using nested fragment caching, ensure parent caches expire correctly when child records change, and keep keys stable and scoped.
# Safe: nested caching with stable keys
@products.each do |product|
cache ["product", product.id, product.updated_at] do
render product
end
end
5. Prefer cache versioning with ActiveSupport::Cache::Entry features
Use timestamp-based versioning to automatically expire entries and reduce the window for poisoned data to persist.
# Safe: versioned cache key
cache ["products", "v1", current_user.id] do
render @products
end
6. Validate and sanitize headers used in caching logic
If your caching logic inspects headers (e.g., Accept-Language), normalize and validate them to prevent header injection affecting cache keys.
# Safe: normalize locale from a known set
available_locales = %w[en es fr]
locale = available_locales.include?(params[:locale]) ? params[:locale] : "en"
cache ["content", locale] do
render @data
end
7. Leverage Rails cache store metadata where supported
Some Rails cache stores support metadata like :expires_in and :race_condition_ttl; use these to reduce stale or poisoned cache reuse.
# Safe: set expiration to limit poisoned entry lifetime
Rails.cache.write("api/products_v2", @payload, expires_in: 15.minutes, race_condition_ttl: 10.seconds)