Broken Access Control in Hanami with Mutual Tls
Broken Access Control in Hanami with Mutual Tls — how this specific combination creates or exposes the vulnerability
Broken Access Control (BAC) in Hanami when combined with Mutual Transport Layer Security (mTLS) can allow authenticated users to access or modify resources they should not reach. mTLS authenticates both client and server, but it does not enforce authorization. If Hanami routes and policies rely only on the presence of a valid client certificate without checking scopes, roles, or tenant boundaries, BOLA/IDOR and BFLA patterns emerge.
Consider a Hanami API where an endpoint is protected by mTLS: a client presents a valid certificate, Hanami verifies the certificate chain, and the request proceeds. If the route uses the certificate’s subject or a static identifier (e.g., org_id) to scope data, an attacker with a legitimate certificate can manipulate IDs or parameters to access another org’s records. Because mTLS terminates authentication early, developers may mistakenly skip per-request authorization checks, leading to unauthenticated-appearing access for valid certificate holders performing unauthorized actions.
In a real-world configuration, Hanami might load the client identity from the certificate and skip role-based checks under the assumption that mTLS is sufficient. This can cause Property Authorization failures: endpoints returning lists of resources might expose fields or entire subcollections that should be restricted by user role. For example, an admin-only endpoint that returns billing details could be invoked by a user with a valid client cert but without an admin role, leaking sensitive data.
Input validation interacts with BAC by allowing tampering of identifiers (e.g., :id) before Hanami’s authorization logic runs. If the app does not validate that the requesting principal owns the referenced resource, an attacker can iterate IDs or exploit weak indirect references. SSRF and unsafe consumption concerns are less direct but can appear if mTLS client certificates are mishandled in forwarded requests, causing the server to make unintended internal calls.
Because mTLS provides strong authentication, developers may overlook complementary controls like scope validation, tenant isolation, and least-privilege role mapping. middleBrick’s 12 security checks, including BOLA/IDOR and Property Authorization, are designed to surface these gaps by correlating authentication signals from mTLS with runtime authorization behavior, ensuring that verified identity maps correctly to allowed actions.
Mutual Tls-Specific Remediation in Hanami — concrete code fixes
To fix Broken Access Control when mTLS is in use, enforce explicit authorization after mTLS authentication and avoid using certificate metadata as the sole authorization source. Below are concrete Hanami examples.
1. Enforce scope and role checks after mTLS verification
Do not assume a valid client certificate implies the right permissions. Use a policy object that reads the certificate’s CN or SAN, maps it to a user/role, and checks scopes.
module Web::Controllers::Accounts
class Show
include Web::Action
# After mTLS handshake, Hanami receives the client certificate via Rack environment
def call(params)
client_cert = env["ssl_client_cert"]
return halt 401, { error: "missing client certificate" } unless client_cert
identity = extract_identity(client_cert)
# Explicit authorization: ensure identity has required scope/role
if Authorizer.new(identity).can?(:read, :account)
account = AccountRepository.new.find(params[:id])
self.body = { account: AccountSerializer.new(account).serializable_hash }
else
halt 403, { error: "insufficient_scope" }
end
end
private
def extract_identity(cert)
# Parse subject or SAN to obtain a user ID or org membership
subject = cert.subject.to_s
cn = subject[/CN=([^,]+)/, 1]
org = subject[/O=([^,]+)/, 1]
{ subject: cn, org: org }
end
end
end
2. Use per-request authorization with data ownership
When exposing resources, validate that the authenticated principal owns or is permitted to access the resource, preventing BOLA/IDOR.
module Web::Controllers::Records
class Update
include Web::Action
def call(params)
client_cert = env["ssl_client_cert"]
identity = extract_identity(client_cert)
record_id = params[:id]
record = RecordRepository.new.find_by_id(record_id)
halt 404, { error: "not_found" } unless record
# Ensure the record belongs to the org the identity is allowed to manage
if record.org_id == identity[:org] && Authorizer.new(identity).can?(:update, record)
RecordUpdater.new(record, params).call
self.body = { status: "ok" }
else
halt 403, { error: "forbidden" }
end
end
private
def extract_identity(cert)
# Same helper as above
subject = cert.subject.to_s
org = subject[/O=([^,]+)/, 1]
{ org: org }
end
end
end
3. Centralize authorization logic and map to compliance frameworks
Define policies that reference roles and scopes extracted from mTLS, and align them with OWASP API Top 10 and relevant compliance mappings.
# app/policies/authorizer.rb
class Authorizer
def initialize(identity)
@identity = identity
end
def can?(action, resource)
role = RoleResolver.new(@identity).call
PolicyEngine.new(role: role, scope: @identity[:org]).allowed?(action, resource)
end
end
# config/policies.rb — mapping to compliance controls
{
read_account: { compliance: ["PCI-DSS", "GDPR"] },
write_account: { compliance: ["SOC2"] }
}
4. Validate and sanitize input before authorization
Ensure IDs and filters cannot be tampered to bypass ownership checks.
module Web::Validators
class RecordId
include Hanami::Validations::Form
validations do
required(:id).filled(:str?, format?: /\\A[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\\z/)
end
end
end
5. Use middleware to normalize mTLS identity for Hanami apps
Normalize the certificate into a stable identity that your policies consume, avoiding ad-hoc parsing in every controller.
# lib/middleware/mtls_identity.rb
class MtlsIdentity
def initialize(app)
@app = app
end
def call(env)
cert = env["ssl_client_cert"]
if cert
subject = cert.subject.to_s
org = subject[/O=([^,]+)/, 1]
user_id = subject[/CN=([^,]+)/, 1]
env["identity"] = { org: org, user_id: user_id }
else
env["identity"] = nil
end
@app.call(env)
end
end
# config/initializers/mtls.rb
Rack::Builder.new do
use MtlsIdentity
run Hanami.app
end