Bola Idor in Hanami with Api Keys
Bola Idor in Hanami with Api Keys — how this combination creates or exposes the vulnerability
Broken Object Level Authorization (BOLA) occurs when an API exposes an object identifier (such as a numeric ID or UUID) without verifying that the requesting actor is authorized to access that specific object. In Hanami, this typically maps to resource IDs in routes and to explicit checks inside service objects or repositories. When API keys are used for authentication but not coupled with per-request object ownership or tenant checks, BOLA vulnerabilities arise.
Consider a Hanami endpoint designed to retrieve a user profile by ID: /profiles/42. If authentication is implemented only via an API key header (e.g., X-API-Key) and the controller loads the profile by ID without confirming that the profile belongs to the account associated with that key, an attacker can enumerate IDs and read other users’ profiles. The API key proves identity at the application level (a client is allowed to call the API), but it does not enforce object-level permissions, making BOLA possible.
In Hanami, this can happen when authorization logic is implicit or scattered. For example, a controller might call ProfileRepository.new.find(params[:id]) after validating the presence of an API key, but without scoping the query to the authenticated client’s allowed records. If the key is valid but the ID is tampered with, the system may return data that should be restricted. The same risk applies to nested resources: an endpoint like /accounts/123/users/456 might check that the API key belongs to account 123, but then return user 456 without verifying that user 456 is part of account 123.
OpenAPI specifications can inadvertently encourage this if parameters are described as required without clarifying scope. For instance, a path parameter profileId typed as integer suggests it is sufficient to pass an integer, without indicating that the client must also prove ownership. Runtime scanning tools that correlate spec definitions with execution traces can highlight such mismatches by showing that an API key grants access to multiple objects without per-object authorization checks.
Real-world attack patterns include IDOR via predictable numeric IDs and enumeration attacks across UUIDs when authorization is missing. BOLA is not limited to reads: if a Hanami update or delete action uses the same pattern, an attacker can modify or remove objects they do not own. Effective defense requires tying each API key to a scope or tenant and ensuring every data access validates both the key and the target object’s membership in that scope.
Api Keys-Specific Remediation in Hanami — concrete code fixes
Remediation centers on ensuring that every object access is validated against the permissions encoded in the API key. Instead of using the key only for gateway-level authentication, model the key as a scoped credential that maps to a tenant or a set of allowed records. Then enforce scoping in repositories and use explicit checks in controllers.
Example: model your API key as a Hanami entity with associated tenant or account IDs, and use a repository that scopes queries accordingly.
# app/entities/api_key.rb
class ApiKey
include Hanami::Entity
attributes :id, :token, :account_id
end
# app/repositories/api_key_repository.rb
class ApiKeyRepository
def initialize(relation = ApiKeyRepository.relation)
@relation = relation
end
def find_by_token(token)
@relation.where(token: token).one
end
end
# app/repositories/profile_repository.rb
class ProfileRepository
def initialize(relation = ProfileRepository.relation)
@relation = relation
end
# Scoped by account_id to prevent BOLA
def find_by_id_for_account(id, account_id)
@relation.where(id: id, account_id: account_id).one
end
end
# app/controllers/profiles/show.rb
class Profiles::Show
include Hanami::Action
def initialize(api_key_repo: ApiKeyRepository.new, profile_repo: ProfileRepository.new)
@api_key_repo = api_key_repo
@profile_repo = profile_repo
end
def call(params)
key = @api_key_repo.find_by_token(params[:api_key])
halt 401, { error: 'unauthorized' }.to_json unless key
profile = @profile_repo.find_by_id_for_account(params[:id], key.account_id)
if profile
{ id: profile.id, name: profile.name, account_id: profile.account_id }.to_json
else
halt 404, { error: 'not found' }.to_json
end
end
end
In this pattern, the API key provides the account_id, and ProfileRepository#find_by_id_for_account ensures that the requested profile belongs to that account. This prevents attackers from iterating IDs across accounts even if they possess valid API keys belonging to other accounts.
For endpoints that accept UUIDs rather than integers, the same principle applies: scope by tenant or owner identifiers. If your API key represents a client with access to multiple organizations, include an organization identifier in the key and validate it on each data fetch.
# Example with UUIDs and explicit ownership check
class Articles::Show
include Hanami::Action
def initialize(repo: ArticleRepository.new)
@repo = repo
end
def call(params)
key = ApiKeyRepository.new.find_by_token(params[:api_key])
halt 401, { error: 'unauthorized' }.to_json unless key
article = @repo.find_by_id_and_org(params[:id], key.organization_id)
# ... render or serialize
end
end
Where feasible, add instrumentation to detect mismatches between declared scope and runtime access patterns. Although middleBrick does not fix code, its scans can surface inconsistencies between your OpenAPI description and runtime behavior, such as an endpoint that claims to require an API key but does not enforce object-level scoping. Use such findings to refine repository methods and controller actions, ensuring that every object load is bound to the key’s authorization scope.
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 |