Combine turnstile and rack-attack#410
Conversation
Coverage Report for CI Build 28115565855Warning Build has drifted: This PR's base is out of sync with its target branch, so coverage data may include unrelated changes. Coverage increased (+0.003%) to 98.356%Details
Uncovered ChangesNo uncovered changes found. Coverage Regressions1 previously-covered line in 1 file lost coverage.
Coverage Stats
💛 - Coveralls |
❌ 2 blocking issues (2 total)
|
There was a problem hiding this comment.
Pull request overview
Adds Rack::Attack-based protections for the expensive /results and /record endpoints and ties throttling into the existing Turnstile flow so legitimate users can recover from throttling via challenge verification.
Changes:
- Introduces a global RPS throttle plus a stricter per-IP throttle for
/resultsand/record. - Overrides Rack::Attack throttled responses on those endpoints to redirect to the Turnstile challenge instead of returning 429.
- After successful Turnstile verification, writes a short-lived “verified” cache key intended to suppress re-challenges for that IP.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 8 comments.
| File | Description |
|---|---|
| README.md | Documents new ENV knobs for the results/record throttles and Turnstile grace period. |
| config/initializers/rack_attack.rb | Adds new throttles and custom throttled response logic for /results and /record. |
| app/controllers/turnstile_controller.rb | Writes a cache-based grace-period marker after successful Turnstile verification. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
jazairi
left a comment
There was a problem hiding this comment.
I think all of Copilot's suggested feedback is worth implementing, particularly the fix for the potential feedback loop. Once that is in place, this is ready to merge from my perspective.
I was pondering adding unit tests for the grace period and throttle_response logic, but I couldn't figure a good way to do so that wouldn't just be testing implementation details. In this case, I think manual testing is sufficient. Anything more would likely have to be a system test, which would be new to us and well out of scope of this urgent change.
Also fixes up docs and adds tests
Also more tests and docs fixups
| cache_key = "turnstile_verified:#{request.ip}" | ||
| grace_period = ENV.fetch('TURNSTILE_GRACE_PERIOD') { 15 }.to_i.minutes | ||
| Rails.cache.write(cache_key, true, expires_in: grace_period) |
| - `REQUESTS_PER_PERIOD` - number of requests that can be made for general throttles per `REQUEST_PERIOD` | ||
| - `REQUEST_PERIOD` - time in minutes used along with `REQUESTS_PER_PERIOD` |
| - `RESULTS_THROTTLE_PERIOD` - time in minutes for `/results` and `/record` endpoint throttle (default 1 minute). Throttled requests are redirected to Turnstile for verification. | ||
| - `TURNSTILE_GRACE_PERIOD` - time in minutes that an IP is whitelisted from throttling after successfully passing Turnstile verification (default 15 minutes). This prevents users from being re-challenged repeatedly. | ||
| - `REDIRECT_REQUESTS_PER_PERIOD`- number of requests that can be made that the query string starts with our legacy redirect parameter to throttle per `REQUEST_PERIOD` | ||
| - `REDIRECT_REQUEST_PERIOD`- time in minutes used along with `REDIRECT_REQUEST_PERIOD` |
| # IMPORTANT: Only redirect if the matched throttle is one that has a grace period cache check | ||
| # (results/global or req/ip/results). Other throttles (req/ip, etc.) don't have grace period | ||
| # exemptions, so redirecting would create an infinite loop. |
| cache_key = "turnstile_verified:#{request.ip}" | ||
| grace_period = ENV.fetch('TURNSTILE_GRACE_PERIOD') { 15 }.to_i.minutes | ||
| Rails.cache.write(cache_key, true, expires_in: grace_period) |
| post turnstile_verify_path, | ||
| params: { 'cf-turnstile-response' => 'mocked', return_to: '/results?q=test' }, | ||
| headers: { 'REMOTE_ADDR' => ip } | ||
|
|
| post turnstile_verify_path, | ||
| params: { 'cf-turnstile-response' => 'mocked', return_to: '/results?q=test' }, | ||
| headers: { 'REMOTE_ADDR' => ip } | ||
|
|
| post turnstile_verify_path, | ||
| params: { 'cf-turnstile-response' => 'mocked', return_to: '/results?q=test' }, | ||
| headers: { 'REMOTE_ADDR' => ip } | ||
|
|
| post turnstile_verify_path, | ||
| params: { 'cf-turnstile-response' => 'mocked', return_to: '/results?q=test' }, | ||
| headers: { 'REMOTE_ADDR' => ip1 } | ||
|
|
| post turnstile_verify_path, | ||
| params: { 'cf-turnstile-response' => 'mocked', return_to: '/results?q=test' }, | ||
| headers: { 'REMOTE_ADDR' => ip2 } | ||
|
|
| post turnstile_verify_path, | ||
| params: { return_to: '/results?q=test' }, | ||
| headers: { 'REMOTE_ADDR' => ip } | ||
|
|
|
|
||
| test 'verify applies default grace period when TURNSTILE_GRACE_PERIOD env var not set' do | ||
| with_bot_detection_enabled do | ||
| default_grace_period = 15 # minutes (the default from the controller) |
| require 'test_helper' | ||
|
|
||
| class RackAttackBlocklistTest < ActionDispatch::IntegrationTest | ||
| def test_blocked_user_agent_returns_403 |
Developer
Accessibility
New ENV
Approval beyond code review
Additional context needed to review
E.g., if the PR includes updated dependencies and/or data
migration, or how to confirm the feature is working.
Code Reviewer
Code
added technical debt.
Documentation
(not just this pull request message).
Testing