HIGH cache poisoningrailsjwt tokens

Cache Poisoning in Rails with Jwt Tokens

Cache Poisoning in Rails with Jwt Tokens — how this specific combination creates or exposes the vulnerability

Cache poisoning occurs when an attacker causes a cache to store malicious content and serve it to other users. In Rails applications that use JWT tokens for authentication, a common misconfiguration can lead to cached responses being shared across users with different authorization states. This happens when fragment or page caching is based on a URL that does not include the token or a user-specific claim, so a response that includes sensitive data for an authenticated user may be served to an unauthenticated or lower-privileged user.

For example, if a controller action caches output using caches_action or low-level caching with Rails.cache.fetch keyed only by the request path, the cached entry can be reused. When JWT tokens are validated after cache lookup (or skipped via a before_action condition), a cached response that was generated while a valid JWT was present might later be served to a request without a valid JWT or with a different user’s JWT. This can expose profile information, CSRF tokens embedded in views, or even sensitive headers that were present in the original authenticated response.

Because JWTs are often used to convey roles and scopes, cached content that reflects role-specific UI or data can be served to users who should not see it. If the cache key does not incorporate a per-user or per-token claim such as sub or a hash of the token payload, the boundary between users is broken. In black-box scanning, this manifests as BOLA/IDOR findings or Data Exposure findings when cached endpoints return different representations depending on authorization state but the cache does not differentiate them.

Rails’ default static asset host and reverse proxy caches can also inadvertently cache responses that include authorization-sensitive headers when the JWT is passed in an Authorization header but the cache key excludes it. Because the scanner tests unauthenticated attack surfaces, it may detect endpoints where a cached response leaks data that should be gated by a JWT, highlighting the need to include user context in cache keys and to avoid caching sensitive representations.

Jwt Tokens-Specific Remediation in Rails — concrete code fixes

To remediate cache poisoning risks with JWT tokens in Rails, ensure cache keys incorporate claims that differentiate authorization states and users. Avoid caching responses that contain sensitive data unless the cache key includes a stable, non-sensitive user identifier or token hash. Below are concrete code examples that demonstrate secure patterns.

1. Include a token-derived or user-derived cache key

When using low-level caching, derive part of the cache key from the sub claim or a hash of the JWT payload. Do not rely on request path alone.

# app/controllers/application_controller.rb
before_action :set_cache_context_from_jwt

def set_cache_context_from_jwt
  if request.headers['Authorization'].to_s.start_with?('Bearer ')
    token = request.headers['Authorization'].split(' ').last
    # decode without verification only for cache context; validation still happens in auth check
    begin
      decoded = JWT.decode(token, Rails.application.secrets.secret_key_base, true, { algorithm: 'HS256' })
      @current_user_id = decoded.first['sub']
    rescue JWT::DecodeError
      @current_user_id = nil
    end
  end
  @cache_key_user = @current_user_id || 'public'
end

Then use @cache_key_user in your cache keys:

# app/models/article.rb
class Article < ApplicationRecord
  def self.cached_list(user_key)
    Rails.cache.fetch("articles/index-v2/#{user_key}", expires_in: 12.hours) do
      Article.published.to_a
    end
  end
end

In the controller:

# app/controllers/articles_controller.rb
def index
  @articles = Article.cached_list(@cache_key_user)
  respond_with @articles
end

2. Skip caching for authenticated, sensitive endpoints

If an endpoint returns user-specific data, avoid caching entirely when a valid JWT is present, or ensure per-user fragmentation of the cache.

# app/controllers/concerns/caching_controls.rb
module CachingControls
  extend ActiveSupport::Concern

  def conditional_cache_store(enabled: true)
    return unless enabled
    request_key = "#{request.path}"
    request_key += "/user-#{@cache_key_user}" if @cache_key_user
    Rails.cache.fetch(request_key, expires_in: 5.minutes) { yield }
  end
end

Use the concern to wrap actions that must remain private:

# app/controllers/profile_controller.rb
include CachingControls

def show
  profile_data = conditional_cache_store(enabled: true) do
    current_user.as_json(only: [:id, :display_name], methods: [:avatar_url])
  end
  render json: profile_data
end

3. Validate JWT before cache lookup when feasible

In some architectures, verifying the token before checking the cache prevents using a cached response derived from a different token. This can increase latency but reduces risk of mixing user contexts.

# app/services/jwt_cache_validator.rb
class JwtCacheValidator
  def self.user_id_from_token(token)
    decoded = JWT.decode(token, Rails.application.secrets.secret_key_base, true, { algorithm: 'HS256' })
    decoded.first['sub']
  rescue JWT::DecodeError
    nil
  end
end

Then in the controller, choose whether to include the user identifier in the cache key or bypass cache when tokens differ.

4. Use Vary headers for HTTP caches

When using HTTP caches (e.g., CDN or reverse proxy), ensure responses include a Vary: Authorization header so caches do not serve a response authenticated for one user to another. In Rails, you can set this in the controller:

response.headers['Vary'] = 'Authorization'

Frequently Asked Questions

How can I verify my cache keys properly differentiate JWT user contexts?
Log the cache key used for each request in development and compare keys for different users and authentication states. Ensure the key changes when the JWT payload’s sub or roles change, and avoid key components that are purely path-based.
Does including the JWT in the cache key compromise token security?
Including a hash of the token or the sub claim in cache keys does not expose the token if the cache storage is properly secured. Do not log full tokens or store them in logs; use a stable user identifier or a salted hash of the token payload instead.