Bola Idor in Phoenix with Api Keys
Bola Idor in Phoenix with Api Keys — how this specific combination creates or exposes the vulnerability
Broken Object Level Authorization (BOLA) occurs when an API fails to enforce authorization checks between a user and a specific object they are trying to access. In Phoenix, a common pattern is to identify resources (e.g., a user profile, an invoice, or a document) using a public identifier such as an integer or UUID, and to rely on an API key for authentication. When API keys are used without additional ownership checks, BOLA vulnerabilities emerge.
Consider a Phoenix endpoint designed to retrieve a user’s profile: GET /api/v1/users/123. The request includes an API key header for identification, but the server does not verify that the profile ID 123 belongs to the principal associated with the provided API key. Because API keys are often long-lived and stored with higher privileges than session tokens, this misalignment between authentication and authorization makes the attack surface severe. An attacker who obtains or guesses another user’s ID can enumerate or modify resources across accounts, despite being authenticated with a valid key.
In practice, this can happen when developers use API keys for convenience but neglect to scope queries by the authenticated principal. For example, a simple Ecto query like Repo.get(User, id) without a tenant or user context allows horizontal privilege escalation across users. If the API key is leaked in logs, client-side storage, or referrer headers, the risk compounds because the key itself does not rotate as frequently as session tokens. The vulnerability maps directly to OWASP API Top 10 A1: Broken Object Level Authorization and can lead to unauthorized data access or modification, violating principles of least privilege and data isolation.
Phoenix APIs that expose numeric or predictable IDs without verifying ownership are especially prone to this class of issue. Even when rate limiting and input validation are present, BOLA persists because those controls do not address the missing ownership check. Real-world patterns include invoice services where /invoices/45 is accessible with a valid key but lacks a join to confirm the invoice belongs to the authenticated account. The same applies to multi-tenant systems where a shared API key is used across services without clear tenant boundaries, enabling cross-tenant data exposure.
Api Keys-Specific Remediation in Phoenix — concrete code fixes
Remediation focuses on ensuring that every data access decision includes both authentication (the API key) and authorization (ownership or scope). In Phoenix, this typically means enriching the connection with the authenticated principal and scoping queries accordingly.
Example 1: Scoped query with authentication context
Instead of fetching a record by raw ID, derive the query from the authenticated subject. If you use Guardian or a similar library to validate API keys, bind the subject to the connection and use it in queries:
defmodule MyAppWeb.UserController do
use MyAppWeb, :controller
alias MyApp.Accounts
alias MyApp.Accounts.User
# Assuming Guardian.Plug is used to authenticate via API key and assign current_resource
plug Guardian.Plug.EnsureAuthenticated when action in [:show, :update]
def show(conn, %{"id" => id}) do
# The authenticated subject is available via current_resource from Guardian
user = Accounts.get_user_for(conn.assigns.current_resource, id)
case user do
nil -> send_resp(conn, :not_found, "Not found")
user -> render(conn, "user.json", user: user)
end
end
end
The key is that Accounts.get_user_for/2 uses the authenticated subject (from the API key) to scope the query, ensuring the requested ID belongs to the same tenant or user.
Example 2: Context-aware authorization function
Define a policy function that explicitly checks ownership before returning a record:
defmodule MyApp.Accounts do
alias MyApp.Repo
alias MyApp.Accounts.User
def get_user_for(%{assigns: %{api_key_user: %{tenant_id: tenant_id}}}, id) do
Repo.get_by(User, id: id, tenant_id: tenant_id)
end
def get_user_for(_, _), do: nil
end
Here, the API key validation step populates assigns.api_key_user with minimal claims, including a tenant or subject identifier. The query then filters by both ID and tenant, preventing cross-user reads even when IDs are predictable.
Example 3: Plug-based subject assignment
Create a lightweight plug that resolves the subject from the API key and attaches it to the connection, keeping controllers thin and authorization explicit:
defmodule MyAppWeb.Plugs.ApiKeySubject do
import Plug.Conn
def init(opts), do: opts
def call(conn, _opts) do
case authenticate_from_header(conn) do
{:ok, claims} ->
# Assign a lightweight subject derived from the API key claims
assign(conn, :api_key_subject, claims.subject)
{:error, _} ->
send_resp(conn, :unauthorized, "Invalid API key")
halt(conn)
end
end
defp authenticate_from_header(conn) do
with [key] <- get_req_header(conn, "x-api-key"),
{:ok, claims} <- MyApp.Auth.validate_api_key(key) do
{:ok, claims}
else
_ -> {:error, :unauthorized}
end
end
end
Use this plug in your router before protected pipelines:
pipeline :api_auth do
plug MyAppWeb.Plugs.ApiKeySubject
end
scope "/api", MyAppWeb do
pipe_through [:api_auth, :ensure_authenticated]
get "/users/:id", UserController, :show
end
By coupling API key validation with explicit scoping, you eliminate the BOLA condition. This approach aligns with remediation guidance for OWASP API Top 10 and supports mapping findings to frameworks such as PCI-DSS and SOC2.
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 |