Bola Idor in Rails with Mutual Tls
Bola Idor in Rails with Mutual Tls — how this specific combination creates or exposes the vulnerability
Broken Object Level Authorization (BOLA) occurs when an API allows one user to access or modify another user’s resources by manipulating object identifiers such as IDs. In Ruby on Rails, this commonly manifests when controllers use path or query parameters (e.g., params[:id]) to locate records without ensuring the requesting user has permission to access that specific record. Adding mutual TLS (mTLS) changes the threat surface but does not automatically remove authorization checks; it introduces new dimensions to how authentication and authorization interact.
With mTLS, the server requests a client certificate during the TLS handshake and validates it against a trusted certificate authority. In Rails, you typically terminate TLS at a load balancer or reverse proxy (e.g., NGINX, HAProxy, or a cloud load balancer) and forward client certificate details in headers such as X-SSL-Client-Cert or X-SSL-Client-DN. If the application uses these headers to identify a user (e.g., by mapping a certificate subject to a user ID) but then relies solely on that identifier to authorize access, BOLA can still occur. An attacker who can cause the server to use a different certificate mapping—perhaps by manipulating session state, impersonating a valid certificate through a misconfigured trust store, or leveraging a business logic flow where the same certificate is shared across roles—might be able to substitute predictable IDs (e.g., /accounts/123) and access other accounts.
Consider an endpoint like GET /api/accounts/:id. If the controller does not scope the query to the authenticated principal—for example, Account.find(params[:id]) without verifying that the account belongs to the requesting user—BOLA is present regardless of mTLS. mTLS may provide strong authentication of the client, but if the authorization check is identity-based only (e.g., "does this certificate map to a user?"), an attacker who can control or predict IDs can traverse horizontally across other users’ accounts. Worse, if certificate-to-user mapping is handled in application code without considering role or tenant boundaries, vertical privilege escalation becomes possible when a certificate with broader permissions is used to access endpoints that should enforce stricter ownership checks.
Another subtle issue arises when mTLS is used for authentication but the API continues to use opaque identifiers (non-sequential UUIDs or tokens) for resources. If the mapping between certificate identity and resource ownership is not enforced at the database query level, an attacker might still iterate through valid resource identifiers. This is especially risky in Rails when default routes and resourceful patterns encourage developers to expose numeric IDs directly. Even with mTLS providing transport-layer assurance, missing scoping such as current_user.accounts.find(params[:id]) leaves a gap that BOLA probes target.
To detect this with a scanner like middleBrick, unauthenticated or low-privilege probes can attempt to access known resource IDs while using different certificate mappings or omitted client certificates, checking whether authorization is enforced consistently. The scanner compares the OpenAPI spec, where paths may imply ownership through parameters, against runtime behavior to highlight endpoints where object-level authorization is missing or incomplete, even when mTLS is in use.
Mutual Tls-Specific Remediation in Rails — concrete code fixes
Remediation centers on ensuring that every data access is scoped to the requesting principal and that mTLS-derived identity is treated as an input subject to the same authorization rules as any other authentication mechanism. Below are concrete patterns you can apply in Rails controllers and models.
1. Enforce ownership scoping with a before_action
Use a before_action to load the current user from the client certificate and scope all record lookups through that user. This ensures that even if params[:id] is manipulated, the query will fail unless the record belongs to the authenticated principal.
class AccountsController < ApplicationController
before_action :set_current_user_from_client_cert
before_action :set_account, only: [:show, :update, :destroy]
def show
# @account is already scoped to current_user, so BOLA is prevented
render json: @account
end
private
def set_current_user_from_client_cert
# Example: extract subject DN and map to a User; handle missing certs
cert_dn = request.headers["X-SSL-Client-DN"]
raise ActionController::Unauthorized, "Client certificate required" unless cert_dn
@current_user = User.find_by(certificate_dn: cert_dn)
raise ActionController::Unauthorized, "Invalid certificate" unless @current_user
end
def set_account
# Critical: scope by current_user to prevent BOLA
@account = current_user.accounts.find(params[:id])
rescue ActiveRecord::RecordNotFound
render json: { error: "Not found" }, status: :not_found
end
end
2. Use has_secure_token with non-guessable identifiers and enforce scoping
Avoid exposing sequential IDs for sensitive resources. Use UUIDs or secure tokens and still scope by user. This reduces the risk of ID enumeration even when mTLS is used.
class Account < ApplicationRecord
has_secure_token :public_id
belongs_to :user
# Ensure user can only find their own accounts by public_id
def self.find_by_public_id!(public_id, user)
user.accounts.find_by!(public_id: public_id) || raise(ActiveRecord::RecordNotFound)
end
end
# In controller
class AccountsController < ApplicationController
before_action :set_current_user_from_client_cert
def show
@account = Account.find_by_public_id!(params[:id], @current_user)
render json: @account
end
end
3. Validate tenant or role boundaries when mTLS maps to groups
If a client certificate maps to a role or tenant, include tenant_id or role checks in your queries to prevent horizontal or vertical escalation across boundaries.
class ReportsController < ApplicationController
before_action :set_current_user_from_client_cert
def index
# Assume current_user has_many :reports through :memberships
@reports = current_user.reports.where(tenant_id: @current_user.tenant_id)
render json: @reports
end
end
4. Secure mTLS setup at the proxy and forward headers safely
Ensure your load balancer or reverse proxy is configured to require valid client certificates and that it strips or validates incoming X-SSL-Client-* headers to prevent spoofing. In Rails, you may want to whitelist these headers and reject requests that provide them from untrusted sources.
# config/application.rb or an initializer
Rails.application.config.middleware.insert_before 0, Rack::SslEnforcer, hsts: false # example placeholder
# In a controller concern to validate headers
module ClientCertValidation
extend ActiveSupport::Concern
included do
before_action :validate_client_certificate_header
end
private
def validate_client_certificate_header
unless request.headers["X-SSL-Client-Cert"].present? && trusted_certificate?(request.headers["X-SSL-Client-Cert"])
raise ActionController::Unauthorized, "Invalid or missing client certificate"
end
end
def trusted_certificate?(cert)
# Implement certificate pinning or validation against a store
true # placeholder
end
end
5. Combine mTLS with other Rails security features
Use strong parameters, CSRF protection where applicable, and ensure that session management does not override mTLS identity. Avoid using certificate subject alone for authorization without checking tenant or role attributes embedded in the certificate.
Related CWEs: bolaAuthorization
| CWE ID | Name | Severity |
|---|---|---|
| CWE-250 | Execution with Unnecessary Privileges | HIGH |
| CWE-639 | Insecure Direct Object Reference | CRITICAL |
| CWE-732 | Incorrect Permission Assignment | HIGH |