Api Rate Abuse in Rails with Saml
Api Rate Abuse in Rails with Saml — how this specific combination creates or exposes the vulnerability
Rate abuse in a Ruby on Rails application that uses SAML for authentication can occur when attackers bypass or overload per-request protections by exploiting the identity provider (IdP) flow and session handling. SAML relies on redirects and assertions, which introduce multiple entry points where requests can be issued without adequate rate checks.
One common scenario: an attacker uses a valid SAML identity to obtain a session cookie, then makes rapid API calls that are authenticated by the cookie rather than by SAML assertions directly. Because Rails controllers may skip rate limits for requests that pass cookie-based authentication, the API’s unauthenticated attack surface includes endpoints that are reachable after SAML login.
Another vector is the SAML Single Logout (SLO) flow. If logout requests or artifact resolution endpoints are not rate-limited, an attacker can generate many logout or binding requests to degrade service or trigger excessive processing. Because SAML endpoints often accept unsigned or lightly validated requests in certain configurations, Rails routes like /saml/consume or /saml/logout can be hammered without per-identity throttling.
Additionally, when Rails apps rely on the SAML response’s NameID or attributes for authorization, missing mapping to a stable rate-limit key can cause the app to fall back to IP-based limits, which are easily bypassed via proxies or NAT. Without tying rate limits to the authenticated identity (e.g., NameID or a mapped user ID), repeated SAML-authenticated sessions from the same user can exceed intended quotas.
The interplay of SAML’s redirect-based flow, Rails’ default route handling, and inconsistent application of rate limits to SAML-bound identities expands the attack surface. Findings for Rate Limiting in middleBrick scans often highlight missing limits on SAML consumer routes and session endpoints, exposing identity-based endpoints to enumeration and brute-force patterns that would be blocked if limits were enforced on authenticated user identifiers.
Saml-Specific Remediation in Rails — concrete code fixes
Apply rate limits to SAML-specific routes and tie them to the authenticated identity rather than IP. Use a stable identifier from the SAML assertion (such as NameID or a mapped user ID) as the rate-limit key. Below are concrete patterns and code examples for Rails.
1. Rate-limit the SAML consumer endpoint by NameID
In your SAML consumer controller, extract the NameID and use it with a throttling gem like rack-attack.
class SamlController < ApplicationController
skip_before_action :verify_authenticity_token, only: [:consume]
def consume
response = OneLogin::RubySaml::Response.new(params[:SAMLResponse], settings: saml_settings)
if response.success?
nameid = response.name_id # e.g. "[email protected]"
if rate_limited_by_nameid?(nameid)
render json: { error: 'rate limit exceeded' }, status: 429
else
# your sign-in logic
render json: { status: 'ok' }
end
else
render json: { error: 'invalid assertion' }, status: 400
end
end
private
def rate_limited_by_nameid?(nameid)
key = "saml:#{nameid}"
begin
# Using redis-rb as an example; adapt to your store
hits = $redis.incr(key)
$redis.expire(key, 60) if hits == 1
hits > 10 # limit to 10 requests per minute per NameID
rescue
false # fail open to avoid blocking auth during store issues
end
end
end
2. Protect SAML Single Logout (SLO) and artifact resolution
Rate-limit logout and artifact endpoints by the NameID or session ID to prevent DoS via repeated logout requests.
class Saml::LogoutController < ApplicationController
skip_before_action :verify_authenticity_token, only: [:slo]
def slo
nameid = params[:NameID] || session[:nameid]
if nameid && rate_limited_by_nameid?(nameid)
logger.warn("Rate limit on SLO for #{nameid}")
end
# process SAML logout request
head 200
end
private
def rate_limited_by_nameid?(nameid)
key = "saml_logout:#{nameid}"
# Allow at most 5 logout requests per minute per identity
hits = $redis.incr(key)
$redis.expire(key, 60) if hits == 1
hits > 5
rescue
false
end
end
3. Map SAML attributes to a user and apply authenticated rate limits in API controllers
In your API controllers, ensure that authenticated endpoints use the mapped user ID rather than IP for rate limiting.
class Api::V1::ProfilesController < ApplicationController
before_action :authenticate_from_saml_session
before_action :throttle_by_user_id, only: [:show, :update]
def show
render json: { profile: current_user.profile }
end
private
def authenticate_from_saml_session
# Assuming you stored NameID in session after SAML login
nameid = session[:nameid]
@current_user = User.find_by(saml_nameid: nameid)
head 401 unless @current_user
end
def throttle_by_user_id
key = "api_user:#{@current_user.id}"
hits = $redis.incr(key)
$redis.expire(key, 60) if hits == 1
if hits > 100 # 100 requests/minute per authenticated user
render json: { error: 'rate limit exceeded' }, status: 429
end
rescue
false
end
end
4. Use consistent SAML settings and OneLogin RubySAML configuration
Ensure your SAML settings are centralized and that NameID is preserved for mapping. Example config/initializers/saml.rb:
SAML_SETTINGS = {
issuer: 'https://app.example.com/saml/metadata',
assertion_consumer_service_url: 'https://app.example.com/saml/consume',
idp_sso_target_url: 'https://idp.example.com/sso',
idp_sso_logout_url: 'https://idp.example.com/slo',
cert_fingerprint: 'AB:CD:EF:...',
name_identifier_format: 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress',
security: {
authn_requests_signed: false,
logout_requests_signed: false,
logout_responses_signed: false,
embed_sign: false,
want_assertions_signed: true,
want_response_signed: false
}
}.freeze