HIGH cache poisoningrailsapi keys

Cache Poisoning in Rails with Api Keys

Cache Poisoning in Rails with Api Keys — how this specific combination creates or exposes the vulnerability

Cache poisoning occurs when an attacker causes a cache to store malicious content, leading to other users receiving that content. In Rails applications that use API keys for authorization, a common misconfiguration is to include sensitive headers such as Authorization or custom API key headers in the cache key. If these keys are treated as part of the public cache namespace or are not excluded properly, an authenticated request from one user can be cached and then served to another user, effectively leaking one user’s data to another.

Consider a Rails controller that caches responses based on path and an API key header:

class Api::V1::ProfilesController < ApplicationController
  before_action :authenticate_with_api_key

  def show
    fresh_when(etag: current_user, last_modified: current_user.updated_at, public: true, shared_max_age: 300)
    render json: current_user.profile
  end

  private

  def authenticate_with_api_key
    api_key = request.headers['X-API-KEY']
    @current_user = User.find_by(api_key: api_key)
    head :unauthorized unless @current_user
  end
end

If the HTTP cache (e.g., a CDN or reverse proxy) uses the full request URL plus headers such as X-API-KEY to form the cache key, and if that key is inadvertently exposed or predictable, an attacker who knows or guesses another user’s API key could retrieve cached responses intended for that user. Moreover, if the cache is shared across users and the API key is included in the cache key but the key is weak or leaked (for example, via logs or referrer headers), the vulnerability becomes easier to exploit. This pattern is problematic when the caching layer is configured to cache GET responses that are user-specific but are marked as public or improperly scoped.

Another scenario involves query parameters that contain API keys or tokens. For example:

GET /api/v1/data?api_key=abc123

If Rails caches the full URL including the api_key query parameter without stripping sensitive values, the cached entry can be reused in other contexts, or the key itself may be exposed in logs or intermediary caches. Sensitive API keys should never be used as part of cacheable identifiers, and Rails cache stores should not be used to store responses that contain user-specific authorization tokens without strict scope isolation.

The LLM/AI Security checks in middleBrick specifically test for system prompt leakage and other AI-specific issues, but cache poisoning related to API keys is a classic input validation and authorization boundary concern. Properly separating authentication from caching and ensuring that sensitive headers and parameters are excluded from cache keys mitigates the risk of one user’s cached response being served to another.

Api Keys-Specific Remediation in Rails — concrete code fixes

To remediate cache poisoning risks when using API keys in Rails, ensure that sensitive headers and parameters are excluded from cache keys and that user-specific data is never cached in a shared namespace. Below are concrete, safe patterns.

1. Exclude sensitive headers from cache keys

Configure Rails cache stores and HTTP caching to ignore authentication-related headers. If you use a CDN or reverse proxy that respects Rails cache directives, avoid including sensitive headers in the cache key. One approach is to normalize the request before caching by removing or hashing sensitive headers.

2. Use user-specific cache keys without exposing secrets

Instead of including raw API keys, derive a cache key from the user’s ID and a stable timestamp. This ensures that each user has a distinct cache entry without placing secrets into the cache namespace.

class Api::V1::ProfilesController < ApplicationController
  before_action :authenticate_with_api_key

  def show
    # Use a cache key based on user id and updated_at, not the API key itself
    cache_key = "user_profile/#{current_user.id}-#{current_user.updated_at.iso8601}"
    cached = Rails.cache.read(cache_key)
    if cached
      render json: cached
    else
      render json: current_user.profile.tap { |json| Rails.cache.write(cache_key, json, expires_in: 5.minutes) }
    end
  end

  private

  def authenticate_with_api_key
    api_key = request.headers['X-API-KEY']
    @current_user = User.find_by(api_key: api_key)
    head :unauthorized unless @current_user
  end
end

3. Avoid API keys in query parameters for cacheable endpoints

Prefer using headers for API keys and avoid query parameters for secrets. If you must support query parameters, sanitize them before using in cache lookups.

# config/initializers/cache_sanitizer.rb
module CacheSanitizer
  def self.sanitized_path(request)
    # Remove sensitive query parameters before using in cache key
    uri = URI(request.url)
    params = Rack::Utils.parse_query(uri.query)
    params.except!('api_key', 'token', 'secret')
    new_query = params.to_query unless params.empty?
    [uri.path, new_query].compact.join('?')
  end
end

Then use this sanitized path as part of the cache key:

class Api::V1::DataController < ApplicationController
  def index
    safe_path = CacheSanitizer.sanitized_path(request)
    cache_key = "shared_data/#{Digest::SHA256.hexdigest(safe_path)}"
    result = Rails.cache.fetch(cache_key, expires_in: 10.minutes) do
      ExpensiveModel.public_data.to_json
    end
    render json: result
  end
end

4. Use HTTP cache controls to limit sharing

Ensure that user-specific responses are not marked public. Use private: true or appropriate no-store directives when the response contains sensitive authorization information.

class Api::V1::ProfilesController < ApplicationController
  def show
    if current_user
      fresh_when(etag: current_user, last_modified: current_user.updated_at, private: true)
      render json: current_user.profile
    else
      head :unauthorized
    end
  end
end

By excluding API keys from cache identifiers, normalizing request parameters, and scoping caches to the correct access level, you reduce the risk of cache poisoning while still benefiting from HTTP caching performance.

Frequently Asked Questions

Should API keys ever be included in cache keys in Rails?
No. Including API keys in cache keys can cause sensitive data to be cached and potentially leaked to other users. Use user-specific identifiers like user ID and timestamps instead, and keep API keys out of cache namespaces.
How can I sanitize query parameters before caching in Rails?
Create a sanitizer that removes or hashes sensitive parameters (e.g., api_key, token) before using the request URL in cache keys. Apply this sanitizer consistently for endpoints that are cached.