MEDIUM clickjackingphoenix

Clickjacking in Phoenix

How Clickjacking Manifests in Phoenix

Clickjacking in Phoenix applications typically occurs when a malicious site embeds your Phoenix app within an iframe, tricking users into interacting with your interface without their knowledge. This is particularly dangerous for Phoenix applications with admin panels, financial transactions, or any user actions that could be exploited.

Consider a Phoenix LiveView banking dashboard. An attacker creates a malicious page that embeds your legitimate Phoenix application in an invisible iframe. When users visit the attacker's page, they might see a seemingly harmless button (like "Click here for a prize"), but underneath is your Phoenix app's "Transfer Funds" button. The user thinks they're clicking one thing while actually performing a financial transaction in your app.

# A vulnerable Phoenix controller action
defmodule MyAppWeb.DashboardController do
  use MyAppWeb, :controller

  def admin_panel(conn, _params) do
    render(conn, "admin_panel.html")
  end
end

The vulnerability here is that the controller doesn't prevent the page from being embedded in an iframe. An attacker can create a page like:

<!DOCTYPE html>
<html>
<body>
  <h2>Click here for a prize!</h2>
  <button onclick="clickPrize()" style="position: absolute; top: 100px; left: 100px;">Click Me!</button>
  <iframe src="https://your-phoenix-app.com/admin/panel" style="opacity: 0; position: absolute; top: 95px; left: 95px; width: 100px; height: 50px;"></iframe>
  
  <script>
    function clickPrize() {
      document.querySelector('iframe').contentWindow.click();
    }
  </script>
</body>
</html>

In Phoenix LiveView applications, clickjacking becomes even more dangerous because of real-time interactions. An attacker could intercept WebSocket connections or manipulate LiveView's client-side state, making the attack more sophisticated and harder to detect.

Phoenix-Specific Detection

Detecting clickjacking in Phoenix applications requires both manual inspection and automated scanning. middleBrick's black-box scanner can identify clickjacking vulnerabilities by attempting to embed your Phoenix endpoints in iframes and checking for proper X-Frame-Options headers.

For manual detection in your Phoenix application, examine your controller actions and LiveView mounts. Any endpoint that renders sensitive UI or performs actions should be protected. middleBrick scans for:

  • Missing X-Frame-Options headers on sensitive endpoints
  • Missing Content-Security-Policy frame-ancestors directives
  • Endpoints that should be protected but lack clickjacking defenses
  • LiveView endpoints vulnerable to iframe embedding
  • Admin panels, dashboards, and transaction interfaces

You can test your Phoenix app's vulnerability by attempting to embed it in an iframe yourself:

# In a Phoenix controller test
defmodule MyAppWeb.DashboardControllerTest do
  use MyAppWeb.ConnCase, async: true

  test "dashboard is not vulnerable to clickjacking" do
    conn = get(build_conn(), "/dashboard")
    assert get_resp_header(conn, "x-frame-options") == ["DENY"]
  end
end

middleBrick's scanner goes further by actually attempting to render your Phoenix endpoints in iframes and checking if they can be successfully embedded, providing a practical assessment of your clickjacking defenses.

Phoenix-Specific Remediation

Phoenix provides several native mechanisms to prevent clickjacking. The most straightforward approach is using the :x_frame_options plug, which Phoenix includes by default in new applications but may be missing in older codebases or custom configurations.

# In your Phoenix endpoint configuration (lib/my_app_web/endpoint.ex)
defmodule MyAppWeb.Endpoint do
  use Phoenix.Endpoint, otp_app: :my_app

  # Add this plug to prevent clickjacking
  plug :put_x_frame_options_header

  # ... rest of your endpoint configuration
end

The :put_x_frame_options_header plug adds the X-Frame-Options: SAMEORIGIN header by default, which prevents your Phoenix app from being embedded in iframes on different domains. For maximum security, you can configure it to DENY:

plug :put_x_frame_options_header, value: "DENY"

For more granular control, Phoenix allows you to configure clickjacking protection per controller:

defmodule MyAppWeb.AdminController do
  use MyAppWeb, :controller
  
  # Protect all actions in this controller
  plug :put_x_frame_options_header, value: "DENY" when action in [:admin_panel, :settings]
  
  def admin_panel(conn, _params) do
    render(conn, "admin_panel.html")
  end
  
  def settings(conn, _params) do
    render(conn, "settings.html")
  end
end

For Phoenix LiveView applications, clickjacking protection works the same way since LiveView mounts are controller actions:

defmodule MyAppWeb.DashboardLive do
  use MyAppWeb, :live_view
  
  # Protect this LiveView from clickjacking
  plug :put_x_frame_options_header, value: "DENY" when action == :mount
  
  @impl true
  def mount(_params, _session, socket) do
    {:ok, assign(socket, message: "Welcome to your dashboard")}
  end
end

For comprehensive protection, combine X-Frame-Options with Content-Security-Policy headers:

plug :put_secure_browser_headers, 
  x_frame_options: "DENY",
  content_security_policy: "frame-ancestors 'none'"

This dual-layer approach ensures protection even in browsers that may handle one header differently than another. middleBrick's scanner will verify that these headers are properly configured across all your Phoenix endpoints.

Frequently Asked Questions

Does Phoenix LiveView require different clickjacking protection than regular Phoenix controllers?
No, Phoenix LiveView uses the same controller-based protection mechanisms. Since LiveView mounts are controller actions, you can apply the same :put_x_frame_options_header plug to LiveView modules. The protection works identically whether you're serving static HTML or LiveView's real-time interfaces.
Can I selectively allow clickjacking on some Phoenix pages but not others?
Yes, Phoenix's plug system allows granular control. You can set X-Frame-Options: SAMEORIGIN for pages that should only be embeddable on your own domain, or use DENY for sensitive pages. The :put_x_frame_options_header plug accepts a value option that you can configure per controller or even per action using conditional plugs.