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)
end

This 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
end

2. 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
end

Without 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
end

This 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, true

Always 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: 86400

This 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
end

2. 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
end

3. 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
end

4. 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
end

Frequently Asked Questions

How can I scan my Phoenix API for security vulnerabilities?
middleBrick can scan any Phoenix API endpoint without requiring credentials or configuration. Simply provide your Phoenix API URL and middleBrick will test for common Phoenix-specific vulnerabilities like missing authorization checks, insecure channel authentication, and unsafe parameter handling. The scanner runs in 5-15 seconds and provides a security score with prioritized findings and remediation guidance. You can use the web dashboard, CLI tool, or GitHub Action to integrate security scanning into your development workflow.
Does Phoenix's default CSRF protection cover all API endpoints?
Phoenix's CSRF protection is enabled by default for browser-based requests but doesn't automatically protect API endpoints that use token-based authentication. For JSON APIs, you need to implement additional security measures like proper CORS configuration, rate limiting, and authorization checks. middleBrick's scanner tests your API's authentication and authorization mechanisms to identify gaps in your security posture, including whether your Phoenix API properly handles unauthenticated requests and protects against common attacks like BOLA (Broken Object Level Authorization).
Can middleBrick detect vulnerabilities specific to Phoenix Channels?
Yes, middleBrick includes security checks specifically designed for Phoenix's channel-based architecture. The scanner tests for common Phoenix Channel misconfigurations such as missing authentication in the 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.