Bola Idor in Phoenix with Mutual Tls
Bola Idor in Phoenix with Mutual Tls — how this specific combination creates or exposes the vulnerability
Broken Object Level Authorization (BOLA) is an API security risk where an attacker can access or modify objects they should not have access to, typically by manipulating object identifiers such as IDs or slugs. In the Phoenix web framework, routes often map directly to database records using parameters like :id. When authorization checks are incomplete or incorrectly scoped, a user can change that ID and reach another user’s data. Mutual Transport Layer Security (mTLS) strengthens transport assurance by requiring both client and server to present valid certificates, but it does not by itself enforce object-level permissions. Relying solely on mTLS can therefore create a false sense of security: the connection is authenticated and encrypted, yet the application logic still allows horizontal privilege escalation if BOLA protections are missing.
Consider a typical Phoenix endpoint that retrieves a user profile:
GET /api/profiles/:id
If the controller loads the profile by ID without confirming that the profile belongs to the authenticated subject, an authenticated mTLS client can increment the ID or guess another valid identifier to view or act on other profiles. This is a classic BOLA/IDOR issue. In this context, mTLS ensures the client possesses a trusted certificate, but it does not answer the question of whether the client is allowed to access the specific object identified by :id. Attack patterns such as ID tampering remain viable even when mTLS is in place, because the authorization boundary is defined by application logic, not by transport-layer identity.
The risk is compounded when APIs are designed around implicit trust based on mTLS alone. Developers might assume that mutual authentication replaces fine-grained authorization, inadvertently omitting checks that verify ownership or role-based access at the object level. This misalignment between transport security and authorization logic means BOLA vulnerabilities persist despite strong TLS configurations. For example, an attacker with a valid certificate could exploit iteration or enumeration techniques on numeric or UUID identifiers to uncover sensitive records, leading to data exposure or unauthorized modification.
Mutual Tls-Specific Remediation in Phoenix — concrete code fixes
To remediate BOLA while using mTLS in Phoenix, enforce explicit ownership or scope checks in your authorization logic, independent of the TLS client identity. Even when mTLS validates the client, your application must still verify that the authenticated principal is permitted to access the requested resource. Below are concrete, idiomatic examples demonstrating how to implement this correctly.
1. Define an authenticated client schema and authorization plug
Assume your mTLS setup populates a current client (user or service) in the connection via a pipeline. Define a schema and a plug that loads and authorizes the target resource:
defmodule MyApp.Accounts.Client do
use Ecto.Schema
@primary_key {:id, :binary_id, autogenerate: true}
schema "clients" do
field :name, :string
has_many :profiles, MyApp.Profiles.Profile
timestamps()
end
end
defmodule MyAppWeb.Plugs.AuthorizeProfile do
import Plug.Conn
def init(opts), do: opts
def call(conn, _opts) do
profile_id = conn.params["id"] || conn.path_params["id"]
client = conn.assigns[:client]
case MyApp.Profiles.get_profile_for_client(profile_id, client.id) do
{:ok, profile} -> assign(conn, :profile, profile)
{:error, :not_found} -> send_resp(conn, 403, "Forbidden")
end
end
end
2. Scope queries to the authenticated client
Ensure your data access layer scopes queries by both the resource ID and the authenticated client’s ID:
defmodule MyApp.Profiles do
import Ecto.Query, warn: false
alias MyApp.Repo
alias MyApp.Profiles.Profile
def get_profile_for_client(profile_id, client_id) do
query =
from p in Profile,
where: p.id == ^profile_id and p.client_id == ^client_id,
limit: 1
Repo.one(query)
end
def update_profile_for_client(profile_id, client_id, attrs) do
get_profile_for_client(profile_id, client_id)
|> case do
nil -> {:error, :not_found}
profile ->
profile
|> Profile.changeset(attrs)
|> Repo.update()
end
end
end
3. Use route binding and pipeline scoping
In your router, bind the client and scope operations to the authenticated identity provided by mTLS:
defmodule MyAppWeb.Router do
use MyAppWeb, :router
pipeline :api do
plug :accepts, ["json"]
plug MyAppWeb.Plugs.LoadClientFromMtls # sets assigns[:client]
plug MyAppWeb.Plugs.AuthorizeProfile # ensures ownership
end
scope "/api", MyAppWeb do
pipe_through :api
get "/profiles/:id", ProfileController, :show
put "/profiles/:id", ProfileController, :update
end
end
4. Controller that relies on pre-validated scope
With the plugs ensuring the profile belongs to the client, the controller can safely use the preloaded association:
defmodule MyAppWeb.ProfileController do
use MyAppWeb, :controller
def show(conn, _params) do
render(conn, "profile.json", profile: conn.assigns.profile)
end
def update(conn, %{"id" => id, "profile" => profile_params}) do
case MyApp.Profiles.update_profile_for_client(id, conn.assigns.client.id, profile_params) do
{:ok, profile} -> render(conn, "profile.json", profile: profile)
{:error, _} -> send_resp(conn, 422, "Unprocessable entity")
end
end
end
These patterns ensure that even when mTLS confirms the client’s identity, every request is verified against object ownership. This separation prevents BOLA/IDOR by making the authorization boundary explicit and independent of transport-layer authentication.
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 |