Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -61,4 +61,4 @@ gem 'will_paginate', '~> 4.0'
gem 'rack-cors', '~> 2.0', '>= 2.0.2'

gem 'rack-attack', '~> 6.7'
gem 'redis', '>= 4.0.1'
gem 'redis', '~> 5.0'
6 changes: 3 additions & 3 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -229,8 +229,8 @@ GEM
prism (>= 1.6.0)
rbs (>= 4.0.0)
tsort
redis (6.0.0)
redis-client (= 0.30.1)
redis (5.4.1)
redis-client (>= 0.22.0)
redis-client (0.30.1)
connection_pool
regexp_parser (2.12.0)
Expand Down Expand Up @@ -290,7 +290,7 @@ DEPENDENCIES
rack-attack (~> 6.7)
rack-cors (~> 2.0, >= 2.0.2)
rails (~> 7.2.3, >= 7.2.3.2)
redis (>= 4.0.1)
redis (~> 5.0)
rubocop (~> 1)
tzinfo-data
will_paginate (~> 4.0)
Expand Down
15 changes: 9 additions & 6 deletions config/initializers/rack_attack.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,15 @@ class Attack
redis_url = ENV['RACK_ATTACK_REDIS_URL'].presence || ENV['REDIS_URL'].presence

Rack::Attack.cache.store = if redis_url
ActiveSupport::Cache::RedisCacheStore.new(
url: redis_url,
namespace: 'rack_attack'
)
elsif Rails.env.production?
raise 'RACK_ATTACK_REDIS_URL or REDIS_URL is required in production'
begin
ActiveSupport::Cache::RedisCacheStore.new(
url: redis_url,
namespace: 'rack_attack'
)
rescue StandardError => e
Rails.logger.warn("Rack::Attack: Redis failed (#{e.message}), using MemoryStore")
ActiveSupport::Cache::MemoryStore.new
end
Comment on lines +9 to +17

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail
: "${REDIS_URL:?Set REDIS_URL to a disposable Redis endpoint}"

ruby -e '
require "active_support/cache/redis_cache_store"
store = ActiveSupport::Cache::RedisCacheStore.new(
  url: ENV.fetch("REDIS_URL"),
  namespace: "rack_attack"
)
abort "Redis client lacks `#with`" unless store.redis.respond_to?(:with)
store.redis.with(&:ping)
puts "Redis connection succeeded"
'

Repository: Euler-B/Telurify-API

Length of output: 228


🌐 Web query:

Rails 7.2.3 ActiveSupport::Cache::RedisCacheStore error_handler redis with ping source

💡 Result:

In Rails 7.2.3, the ActiveSupport::Cache::RedisCacheStore uses a default error handler defined as DEFAULT_ERROR_HANDLER [1][2]. This handler is responsible for logging errors via the Rails logger and reporting them to the ActiveSupport error reporter with the source "redis_cache_store.active_support" [1][2]. The default error handler is defined as: DEFAULT_ERROR_HANDLER = -> (method:, returning:, exception:) do if logger logger.error { "RedisCacheStore: #{method} failed, returned #{returning.inspect}: #{exception.class}: #{exception.message}" } end ActiveSupport.error_reporter&.report( exception, severity::warning, source: "redis_cache_store.active_support",) end The RedisCacheStore internal implementation includes a failsafe method that wraps cache operations and calls this @error_handler if a Redis::BaseError, ConnectionPool::Error, or ConnectionPool::TimeoutError occurs [2]. Regarding your mention of "ping": ActiveSupport::Cache::RedisCacheStore does not include a specific "ping" option or native mechanism within its error handler configuration to automatically ping Redis upon an error [1][2]. If you need to perform health checks or pings, you would typically implement that logic within a custom error handler provided to the RedisCacheStore during initialization [1][2]. You can override the default behavior by passing a custom error_handler proc when creating your RedisCacheStore instance: # Example of providing a custom error handler config.cache_store =:redis_cache_store, { error_handler: -> (method:, returning:, exception:) do # Add your custom logic here, such as logging, metrics, or pinging Redis Rails.logger.error("Redis error in #{method}: #{exception.message}") end } If you are encountering specific errors, ensure they are among those caught by the store's internal failsafe mechanism (Redis::BaseError, ConnectionPool::Error, or ConnectionPool::TimeoutError) [2].

Citations:


🌐 Web query:

Rack::Attack cache store increment nil behavior source

💡 Result:

In Rack::Attack, the cache store's increment method is handled with a fallback mechanism to accommodate differences in how various cache stores (such as Redis or Memcached) behave when incrementing a key that does not yet exist [1][2]. The relevant logic is located in the Rack::Attack::Cache#do_count method within the lib/rack/attack/cache.rb source file [1][2]. When Rack::Attack attempts to increment a counter, it calls store.increment and checks the result [1][2]: 1. Detection of Nil: Some cache stores return nil when an attempt is made to increment a key that has not been initialized [1][2]. 2. Fallback Logic: If the result of the increment call is nil, Rack::Attack immediately executes store.write(key, 1, expires_in: expires_in) to manually initialize the key with a value of 1 [1][2]. 3. Return Value: The method ultimately returns result || 1, ensuring that the application receives a valid integer (typically 1 for a new entry) even if the underlying store returned nil [1][2]. This implementation is designed to handle potential inconsistencies across different ActiveSupport::Cache stores, ensuring that rate limiting counters remain accurate even when the cache backend does not support atomic "increment-or-initialize" behavior [1][2].

Citations:


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail
python3 - <<'PY'
from urllib.request import urlopen

urls = {
    "redis_cache_store": "https://raw.githubusercontent.com/rails/rails/v7.2.3/activesupport/lib/active_support/cache/redis_cache_store.rb",
    "rack_attack_cache": "https://raw.githubusercontent.com/rack/rack-attack/main/lib/rack/attack/cache.rb",
}
for name, url in urls.items():
    print(f"--- {name} ---")
    text = urlopen(url, timeout=10).read().decode()
    lines = text.splitlines()
    for i, line in enumerate(lines, 1):
        if any(token in line for token in ("def failsafe", "error_handler", "def increment", "result || 1", "def do_count")):
            lo, hi = max(1, i - 8), min(len(lines), i + 22)
            print("\n".join(f"{n}: {lines[n-1]}" for n in range(lo, hi + 1)))
            print()
PY

Repository: Euler-B/Telurify-API

Length of output: 2470


Rate-limit Failure-open (CWE-799)

Reachability: External · Exploitability: Moderate

Make Redis failure handling fail closed for rate limiting.

MemoryStore is process-local, so clustered Puma workers do not share fallback counters. Later Redis failures also return nil; Rack Attack converts that result to count 1, which disables throttling.

Use MemoryStore only outside production, probe Redis during initialization, and raise on production cache errors:

begin
  store = ActiveSupport::Cache::RedisCacheStore.new(
    url: redis_url,
    namespace: 'rack_attack',
    error_handler: lambda do |method:, returning:, exception:|
      raise exception if Rails.env.production?

      Rails.logger.warn("Rack::Attack: Redis #{method} failed")
    end
  )
  store.redis.with(&:ping)
  store
rescue StandardError => e
  Rails.logger.warn("Rack::Attack: Redis failed (#{e.message}), using MemoryStore")
  raise if Rails.env.production?

  ActiveSupport::Cache::MemoryStore.new
end
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@config/initializers/rack_attack.rb` around lines 9 - 17, Update the
Rack::Attack cache-store initialization around
ActiveSupport::Cache::RedisCacheStore to probe Redis with a ping and configure
an error_handler that raises cache errors in production while warning in
non-production. In the rescue path, retain MemoryStore only for non-production
environments and re-raise the exception in production, preserving the existing
warning context.

Source: Path instructions

else
ActiveSupport::Cache::MemoryStore.new
end
Expand Down
Loading