Phoenix API Security
Phoenix Security Posture
Phoenix Framework provides a solid security foundation out of the box. The framework includes CSRF protection, secure session management, and built-in parameter filtering. However, Phoenix's security defaults are designed for convenience rather than maximum security, leaving several critical attack surfaces exposed.
The framework's channel-based architecture introduces unique security considerations. Phoenix Channels use Phoenix.Token for authentication, which is cryptographically secure but can be misconfigured. The default Phoenix endpoint configuration allows all origins for CORS, potentially exposing APIs to cross-origin attacks. Additionally, Phoenix's Ecto schemas don't enforce authorization checks by default, making it easy to accidentally expose sensitive data through simple controller actions.
Phoenix's Plug pipeline system is both a strength and a potential weakness. While it allows granular control over request processing, developers often forget to add essential security plugs like plug CORSPlug or plug :protect_from_forgery to their custom pipelines. The framework's emphasis on developer productivity means security features must be explicitly enabled rather than enforced by default.
Top 5 Security Pitfalls in Phoenix
1. Missing Authorization Checks in Ecto Queries
Phoenix developers frequently write controller actions that fetch database records without proper authorization. A common pattern:
def show(conn, %{"id" => id}) do
user = Accounts.get_user!(id)
render(conn, "show.html", user: user)
endThis code allows any authenticated user to view any user's profile. The fix requires adding authorization logic:
def show(conn, %{"id" => id}) do
user = Accounts.get_user!(id)
if user.id == conn.assigns.current_user.id do
render(conn, "show.html", user: user)
else
conn
|> put_flash(:error, "Unauthorized access")
|> redirect(to: Routes.user_path(conn, :index))
end
end2. Insecure Channel Authentication
Phoenix Channels default to allowing any connection to join without authentication. Developers often forget to implement the authorize! callback:
def join("user_data:" <> user_id, params, socket) do
if authorized?(socket, user_id) do
{:ok, socket}
else
{:error, %{reason: "unauthorized"}}
end
endWithout this check, any user can subscribe to any other user's channel data.
3. Unsafe Parameter Handling
Phoenix's fetch_query_params/2 and fetch_query_params/2 functions don't automatically sanitize input. Developers often directly use conn.params without validation:
def create(conn, %{"user" => user_params}) do
Accounts.create_user(user_params) # Vulnerable to mass assignment
endThis allows attackers to set any field, including is_admin or balance.
4. Exposed Debug Information
Phoenix applications in development mode include detailed error pages with stack traces and environment variables. If accidentally deployed to production, these pages reveal sensitive information:
config :phoenix, :stacktrace_depth, 20
config :phoenix, :format_encoders, ["html": Phoenix.HTML.Engine]
config :phoenix, :serve_endpoints, trueAlways ensure config/prod.exs disables detailed error pages.
5. Insecure Default CORS Configuration
The default Phoenix CORS configuration allows all origins:
config :cors_plug,
origin: ["*"],
max_age: 86400This enables any website to make requests to your API, potentially exposing sensitive data. Production applications should restrict origins to specific domains.
Security Hardening Checklist
1. Implement Authorization Middleware
Create a plug that verifies user permissions before controller actions:
defmodule MyAppWeb.Plugs.Authorization do
import Plug.Conn
import Phoenix.Controller
def init(opts), do: opts
def call(conn, _opts) do
current_user = conn.assigns[:current_user]
resource = conn.assigns[:resource]
if authorized?(current_user, resource) do
conn
else
conn
|> put_status(403)
|> json(%{error: "Forbidden"})
|> halt()
end
end
defp authorized?(user, resource) do
# Implement your authorization logic
user.id == resource.user_id
end
end2. Secure Channel Authentication
Always implement proper channel authentication:
defmodule MyAppWeb.UserChannel do
use MyAppWeb, :channel
alias MyApp.Accounts
def join("user_data:" <> user_id, _params, socket) do
user_id = String.to_integer(user_id)
if socket.assigns.current_user.id == user_id do
{:ok, socket}
else
{:error, %{reason: "unauthorized"}}
end
end
end3. Input Validation with Schemas
Use Ecto schemas with strict casting:
defmodule MyApp.Accounts.User do
use Ecto.Schema
import Ecto.Changeset
schema "users" do
field :email, :string
field :name, :string
field :is_admin, :boolean, default: false
field :balance, :decimal
end
def changeset(user, attrs) do
user
|> cast(attrs, [:email, :name])
|> validate_required([:email, :name])
|> validate_format(:email, ~r/@/)
|> unique_constraint(:email)
end
end4. Production Security Configuration
Update your production config:
config :phoenix, :serve_endpoints, true
config :phoenix, :format_encoders, ["html": Phoenix.HTML.Engine]
config :phoenix, :stacktrace_depth, 20
config :cors_plug,
origin: ["https://yourdomain.com"],
max_age: 86400
config :my_app, MyAppWeb.Endpoint,
secret_key_base: "your-secret-key-base",
url: [host: "yourapp.com", port: 443],
force_ssl: [rewrite_on: [:x_forwarded_proto]]5. Rate Limiting
Implement rate limiting to prevent brute force attacks:
defmodule MyAppWeb.Plugs.RateLimiter do
import Plug.Conn
import Phoenix.Controller
def init(opts), do: opts
def call(conn, _opts) do
user_id = conn.assigns[:current_user].id
key = "rate_limit:#{user_id}"
case Redix.command(:redix, ["INCR", key]) do
{:ok, 1} ->
Redix.command(:redix, ["EXPIRE", key, 60])
conn
{:ok, count} when count > 100 ->
conn
|> put_status(429)
|> json(%{error: "Rate limit exceeded"})
|> halt()
{:ok, _} ->
conn
end
end
endFrequently Asked Questions
How can I scan my Phoenix API for security vulnerabilities?
Does Phoenix's default CSRF protection cover all API endpoints?
Can middleBrick detect vulnerabilities specific to Phoenix Channels?
join/3 callback, improper token validation, and channel authorization bypasses. middleBrick also tests for input validation issues in channel message handling and ensures that your Phoenix Channels properly enforce user permissions before allowing access to real-time data streams.