mirror of
https://github.com/we-promise/sure.git
synced 2026-09-05 06:41:08 +00:00
* security: throttle every credential-guessing endpoint, fix duplicate Rack::Attack middleware Follow-up on #1087 (Findings H4, M7). PR 4 of the 6-PR series. Enumerated every endpoint that checks a password, TOTP code, or backup code (grepped for User.authenticate_by/#authenticate/#verify_otp? across app/controllers, not just the ones named in the issue) — six in total, none previously throttled: - POST /sessions (SessionsController#create) — web login - POST /mfa/verify (MfaController#verify_code) — TOTP + backup codes (#verify_otp? handles both internally, so no separate endpoint to add) - POST /password_reset (PasswordResetsController#create) — also M7 - POST /api/v1/auth/login (Api::V1::AuthController#login) — mobile/API - POST /oidc_account/create_link (OidcAccountsController#create_link) — password check gating SSO-identity linking, not sign-in; easy to miss grepping routes.rb for "session"/"login" - POST /api/v1/auth/sso_link (Api::V1::AuthController#sso_link) — same as above for the mobile app Each gets two throttles (ip AND normalized email, or ip AND the MFA step-up's session-bound user id where there's no email param) so an attacker can't bypass by rotating IPs against one target, nor by spraying many emails from one IP — Rack::Attack requires every matching throttle to pass. limit: 10/minute, matching the existing oauth/token and admin/ip throttles already in this file. Also fixed a latent, unrelated-but-adjacent bug found while confirming these throttles would actually enforce the limits documented in their own comments: config/application.rb had an explicit `config.middleware.use Rack::Attack` alongside the gem's own Railtie doing the same thing (`bin/rails middleware` listed it twice) — every throttle's counter was incrementing twice per request, so all of them, old and new, were silently firing at half their documented limit. Removed the redundant explicit registration. Race-condition check (per standing instruction): Rack::Attack's counter increments are atomic within its cache store, so concurrent requests at the threshold don't undercount. No new race introduced. New tests in test/integration/rack_attack_test.rb: - Registration checks for all 6 new throttle keys (existing convention in this file). - Direct block-level tests for the discriminator logic (right path matched, right value extracted, blank/missing input produces nil rather than a bogus key) — Rack::Attack's cache backs onto Rails.cache, which is :null_store in the test environment, so no amount of request volume in a normal integration test can ever actually trip a throttle here; calling the registered block directly against a constructed Rack::Attack::Request is what makes the assertions meaningful instead of just checking string keys exist. - Regression test asserting Rack::Attack appears exactly once in the middleware stack. Verified against the NAS sure_test_web container: full restart, bin/rails test (8/8 rack_attack tests green; ran the full test/integration suite plus sessions/mfa/password_resets/api-auth/oidc_accounts controller tests too — 6 pre-existing failures, confirmed identical on the unmodified baseline before concluding they're the known WebAuthn-RP-ID-mismatch and AI-disabled environmental categories, not a regression), bin/rubocop, bin/brakeman. Also did a live demonstration against the running container (which runs RAILS_ENV=production, where Rack::Attack is actually enabled): 12 rapid POSTs to /sessions with bad credentials — requests 1-10 got 422, 11 and 12 got 429, exactly matching limit: 10. Container restored to its original state and restarted afterward. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * security: extract email from JSON bodies for credential-guess throttles Rack::Attack runs before Rails' JSON parameter parsing, so request.params only exposed query/form fields. The documented api/v1/auth/login and .../sso_link JSON format bypassed the per-email throttle entirely, letting an attacker rotate IPs against one target's account. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * security: guard JSON email peek against non-rewindable input and non-object payloads Rack 3 no longer requires rack.input to be rewindable, and a bare JSON.parse(body)["email"] raises NoMethodError on valid non-Hash JSON (null, arrays, scalars) — either would 500 the request instead of just skipping the email throttle. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * security: assert non-rewindable JSON bodies stay readable by the controller Only checking that the throttle discriminator returned nil left a gap: an implementation that read the body and then discarded the result on error would pass the same assertion while leaving the controller with an exhausted stream. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * security: close credential-guessing throttle bypass via format-suffixed paths request.path == "/sessions" (etc.) never matched "/sessions.json", which Rails still routes to the same controller action since none of these routes are declared format: false. Match the optional format suffix explicitly instead, per jjmata's review on PR #3263. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * security: match Rails' actual format-segment charset in credential_guess_path \w excludes hyphens, but Rails' default (.:format) segment matches [^./?]+, which does include them — e.g. "/api/v1/auth/login.rate-limit" still routed and bypassed the throttle. Match the real charset instead, per CodeRabbit's follow-up on PR #3263. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --------- Co-authored-by: Gerald <248542187+gfr-free@users.noreply.github.com> Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
74 lines
3.3 KiB
Ruby
74 lines
3.3 KiB
Ruby
require_relative "boot"
|
|
|
|
require "rails/all"
|
|
|
|
# Require the gems listed in Gemfile, including any gems
|
|
# you've limited to :test, :development, or :production.
|
|
Bundler.require(*Rails.groups)
|
|
|
|
module Sure
|
|
class Application < Rails::Application
|
|
# Initialize configuration defaults for originally generated Rails version.
|
|
config.load_defaults 8.1
|
|
|
|
# Please, add to the `ignore` list any other `lib` subdirectories that do
|
|
# not contain `.rb` files, or that should not be reloaded or eager loaded.
|
|
# Common ones are `templates`, `generators`, or `middleware`, for example.
|
|
config.autoload_lib(ignore: %w[assets tasks generators])
|
|
|
|
# Configuration for the application, engines, and railties goes here.
|
|
#
|
|
# These settings can be overridden in specific environments using the files
|
|
# in config/environments, which are processed later.
|
|
#
|
|
# config.time_zone = "Central Time (US & Canada)"
|
|
# config.eager_load_paths << Rails.root.join("extras")
|
|
|
|
# TODO: This is here for incremental adoption of localization. This can be removed when all translations are implemented.
|
|
config.i18n.fallbacks = true
|
|
|
|
config.app_mode = (ENV["SELF_HOSTED"] == "true" || ENV["SELF_HOSTING_ENABLED"] == "true" ? "self_hosted" : "managed").inquiry
|
|
|
|
# Self hosters can optionally set their own encryption keys if they want to use ActiveRecord encryption.
|
|
if Rails.application.credentials.active_record_encryption.present?
|
|
config.active_record.encryption = Rails.application.credentials.active_record_encryption
|
|
end
|
|
|
|
config.view_component.preview_controller = "LookbooksController"
|
|
config.lookbook.preview_display_options = {
|
|
theme: [ "light", "dark" ] # available in view as params[:theme]
|
|
}
|
|
|
|
# Enable Skylight instrumentation for ActiveJob (background workers)
|
|
# Developers can opt-in to Skylight locally by setting SKYLIGHT_ENABLED=true
|
|
if defined?(Skylight) && config.respond_to?(:skylight)
|
|
config.skylight.probes << "active_job"
|
|
if ENV["SKYLIGHT_ENABLED"] == "true"
|
|
config.skylight.environments += [ "development" ]
|
|
end
|
|
end
|
|
|
|
# Rack::Attack's own Railtie (lib/rack/attack/railtie.rb in the gem)
|
|
# already inserts it into the middleware stack — this explicit `use` was
|
|
# a second, redundant insertion (confirmed via `bin/rails middleware`,
|
|
# which listed Rack::Attack twice). Since Rack::Attack's counters
|
|
# increment once per middleware pass, every throttle in
|
|
# config/initializers/rack_attack.rb was silently firing at half its
|
|
# documented limit. Removed rather than kept as a second layer, since
|
|
# nothing in the app relies on it running twice and the halved limits
|
|
# were undocumented/accidental, not a deliberate stricter policy.
|
|
|
|
config.x.ui = ActiveSupport::OrderedOptions.new
|
|
default_layout = ENV.fetch("DEFAULT_UI_LAYOUT", "dashboard")
|
|
config.x.ui.default_layout = default_layout.in?(%w[dashboard intro]) ? default_layout : "dashboard"
|
|
|
|
config.x.debug_log = ActiveSupport::OrderedOptions.new
|
|
retention_days = ENV.fetch("DEBUG_LOG_RETENTION_DAYS", "90").to_i
|
|
config.x.debug_log.retention_days = retention_days.positive? ? retention_days : 90
|
|
|
|
# Handle OmniAuth/OIDC errors gracefully (must be before OmniAuth middleware)
|
|
require_relative "../app/middleware/omniauth_error_handler"
|
|
config.middleware.use OmniauthErrorHandler
|
|
end
|
|
end
|