Broken Access Control in Phoenix with Api Keys
Broken Access Control in Phoenix with Api Keys — how this specific combination creates or exposes the vulnerability
Broken Access Control occurs when authorization checks are missing, incomplete, or bypassed, allowing an authenticated user to access or modify resources they should not. In Phoenix applications that rely on API keys for authentication, this risk is pronounced because API keys are often treated as static secrets without sufficient scoping or ownership checks. If a key is issued for a tenant or user but the endpoint does not validate that the requesting key maps to the resource being accessed, an attacker can manipulate identifiers (e.g., changing an ID in the URL) and access data or perform actions belonging to another tenant or user. This is a classic BOLA/IDOR scenario intertwined with weak authorization tied to API keys.
Phoenix typically uses plug pipelines to authenticate requests. When API keys are used, developers might authenticate the request by verifying the key against a database but then skip or incorrectly implement per-resource authorization. For example, an endpoint like /api/organizations/:org_id/members might authenticate the key and fetch the organization record, but if the code does not ensure that the key’s associated organization matches org_id, an attacker can iterate IDs to view or modify other organizations. This is exacerbated when keys are long-lived and leaked in logs or client-side code, increasing exposure. The absence of rate limiting and insufficient audit logging further enable enumeration and abuse.
The 12 parallel security checks in middleBrick highlight these risks for Phoenix APIs. Unauthenticated scanning can detect endpoints that accept API keys but still expose BOLA/IDOR or improper property authorization. Input validation checks reveal whether IDs and parameters are sanitized and constrained, while Authentication and Unsafe Consumption checks assess how keys are accepted and stored in requests. Because middleBrick performs black-box testing using OpenAPI specs and runtime analysis, it can surface mismatches between declared scopes in the spec and actual runtime behavior, such as missing authorization checks on sensitive paths. These findings map to OWASP API Top 10 A01:2023 — Broken Access Control and align with relevant PCI-DSS and SOC2 controls, emphasizing the need for robust authorization tied to API keys.
Api Keys-Specific Remediation in Phoenix — concrete code fixes
Remediation centers on ensuring that every authorized request not only presents a valid API key but also that the key is explicitly linked to the resource being accessed. Store keys with metadata such as tenant ID or user ID, and enforce ownership checks in your pipelines or controllers. Avoid relying solely on plug-based authentication without subsequent authorization. Below are concrete examples using Phoenix controllers and pipelines that demonstrate how to bind an API key to a resource and validate access.
First, define a schema and changeset for API keys with an associated tenant identifier:
defmodule MyApp.Accounts.ApiKey do
use Ecto.Schema
schema "api_keys" do
field :key, :string
field :tenant_id, :id
field :scopes, {:array, :string}, default: []
timestamps()
end
def changeset(api_key, attrs) do
api_key
|> Ecto.Changeset.cast(attrs, [:key, :tenant_id, :scopes])
|> Ecto.Changeset.validate_required([:key, :tenant_id])
end
end
Next, create a pipeline that loads the key and assigns the tenant, ensuring the key exists and is active:
defmodule MyAppWeb.ApiKeyPipeline do
import Plug.Conn
import MyAppWeb.Gettext
def init(opts), do: opts
def call(conn, _opts) do
case get_api_key(conn) do
{:ok, api_key} ->
# Attach tenant context to the connection
assign(conn, :current_tenant_id, api_key.tenant_id)
{:error, _reason} ->
conn
|> put_status(:unauthorized)
|> json(%{error: gettext("Invalid API key")})
|> halt()
end
end
defp get_api_key(conn) do
api_key = conn
|> get_req_header("authorization")
|> List.first()
|> String.replace("ApiKey ", "")
if api_key && byte_size(api_key) > 0 do
case MyApp.Accounts.get_api_key_by_key(api_key) do
%MyApp.Accounts.ApiKey{} = key -> {:ok, key}
nil -> {:error, :invalid}
end
else
{:error, :missing}
end
end
end
In your controller, use the assigned tenant ID to scope queries, preventing BOLA/IDOR regardless of which API key is presented:
defmodule MyAppWeb.OrganizationController do
use MyAppWeb, :controller
alias MyApp.Organizations
alias MyApp.Organizations.Organization
plug MyAppWeb.ApiKeyPipeline when action in [:index, :show, :update]
def index(conn, _params) do
tenant_id = conn.assigns.current_tenant_id
organizations = Organizations.list_organizations_for_tenant(tenant_id)
render(conn, :index, organizations: organizations)
end
def show(conn, %{"id" => id}) do
tenant_id = conn.assigns.current_tenant_id
with %Organization{} = organization <- Organizations.get_organization_by_tenant(id, tenant_id) do
render(conn, :show, organization: organization)
else
nil -> send_resp(conn, :not_found, "Not found")
end
end
end
The critical part is get_organization_by_tenant, which includes the tenant ID in the query, ensuring that even if an attacker guesses or iterates an ID, they cannot access organizations outside their tenant. Combine this with proper key rotation, scoped keys, and audit logging. middleBrick’s Pro plan supports continuous monitoring and can integrate into your CI/CD pipeline to fail builds if risk scores degrade, helping you maintain secure authorization over time.