Bola Idor in Phoenix
How Bola Idor Manifests in Phoenix
BOLA/IdOR (Broken Object Level Authorization / Insecure Direct Object References) in Phoenix applications typically occurs when user input directly references objects without proper authorization checks. In Phoenix, this often manifests through dynamic URL parameters that map to database records without verifying the current user's permissions.
Consider a Phoenix controller that fetches a user profile:
def show(conn, %{"id" => id}) do
user = Accounts.get_user!(id)
render(conn, "show.html", user: user)
endThis code is vulnerable because it retrieves any user by ID without checking if the requester is authorized to view that user's data. An attacker can simply increment the ID parameter to access other users' profiles.
Another common Phoenix pattern involves nested resources:
def show(conn, %{"user_id" => user_id, "post_id" => post_id}) do
post = Posts.get_post!(post_id)
render(conn, "show.html", post: post)
endHere, the controller fetches a post by ID without verifying it belongs to the user specified in the URL. An authenticated user could access any post in the system by changing the post_id parameter.
Phoenix contexts can also introduce BOLA vulnerabilities when they don't properly scope queries:
def get_user_by_id(id) do
Repo.get(User, id)
endIf this context function is called without additional authorization logic, it becomes a direct path for object enumeration attacks.
Phoenix-Specific Detection
Detecting BOLA in Phoenix applications requires both manual code review and automated scanning. middleBrick's black-box scanning approach is particularly effective for Phoenix APIs because it tests the actual running application without requiring source code access.
For manual detection, look for these Phoenix-specific patterns:
1. Dynamic Parameter Usage
# Vulnerable: Direct parameter usage
user = Accounts.get_user!(conn.params["id"])2. Missing Authorization in Context Functions
# Vulnerable: No user scoping
Repo.all(from p in Post)3. Improper Ecto Query Construction
# Vulnerable: Raw SQL with user input
query = "SELECT * FROM users WHERE id = $1"
Repo.query(query, [conn.params["id"]])middleBrick scans Phoenix applications by sending authenticated requests with manipulated parameters to test for unauthorized access. For example, it might:
- Request /api/users/1, then /api/users/2 to check if sequential IDs return different users
- Test nested routes like /api/users/1/posts/1 vs /api/users/2/posts/1
- Check if authenticated users can access other users' data through ID manipulation
- Verify that authorization middleware properly restricts access
The scanner tests 12 security categories in parallel, including BOLA-specific checks that attempt to access objects across user boundaries. middleBrick's LLM/AI security module can also detect if your Phoenix application has unauthenticated AI endpoints that might be vulnerable to prompt injection or excessive agency.
Phoenix-Specific Remediation
Phoenix provides several native features for preventing BOLA vulnerabilities. The most effective approach combines proper context design with authorization checks.
1. Context-Level Authorization
defmodule MyApp.Accounts do
import Ecto.Query
def get_user_by_id(id, current_user_id) do
Repo.get_by(User, id: id, user_id: current_user_id)
end
def list_user_posts(user_id, current_user_id) do
if user_id == current_user_id do
Repo.all(from p in Post, where: p.user_id == ^user_id)
else
{:error, :unauthorized}
end
end
end2. Controller-Level Authorization
defmodule MyAppWeb.UserController do
use MyAppWeb, :controller
alias MyApp.Accounts
def show(conn, %{"id" => id}) do
with {:ok, user} <- Accounts.get_user_by_id(id, conn.assigns.current_user.id) do
render(conn, "show.html", user: user)
else
{:error, :unauthorized} ->
conn
|> put_status(403)
|> render("forbidden.html")
{:error, :not_found} ->
conn
|> put_status(404)
|> render("not_found.html")
end
end
end3. Using Phoenix's Plugs for Authorization
defmodule MyAppWeb.Plugs.RequireOwnership do
import Plug.Conn
def init(opts), do: opts
def call(conn, _opts) do
user_id = conn.params["user_id"]
current_user_id = conn.assigns.current_user.id
if user_id != current_user_id do
conn
|> send_resp(403, "Forbidden")
|> halt()
else
conn
end
end
end
# In router.ex
defmodule MyAppWeb.Router do
use MyAppWeb, :router
pipeline :owned_resource do
plug MyAppWeb.Plugs.RequireOwnership
end
scope "/api", MyAppWeb do
pipe_through [:api, :auth, :owned_resource]
get "/users/:user_id/posts/:id", PostController, :show
end
end4. Using Libraries like Canada or Bodyguard
# With Canada
defmodule MyApp.Policies do
import Canada
define(User, :index_posts, Post, user_id: id(user.id))
define(User, :show_post, Post, user_id: id(user.id))
end
# In controller
def show(conn, %{"id" => id}) do
post = Post |> Repo.get(id) |> authorize(conn, :show_post)
render(conn, "show.html", post: post)
endFor comprehensive protection, implement the principle of least privilege throughout your Phoenix application. Always scope database queries by the current user's ID, use Phoenix's authorization plugs, and validate ownership before returning any object data.
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 |