Clickjacking in Phoenix with Hmac Signatures
Clickjacking in Phoenix with Hmac Signatures — how this specific combination creates or exposes the vulnerability
Clickjacking is a client-side injection attack where an attacker tricks a user into clicking or interacting with a hidden or disguised UI element inside an embedded frame. When Phoenix applications embed HTTP responses inside <iframe> or <frame> elements without protection, and also use Hmac Signatures only for request authentication (e.g., to validate webhook or API calls), the two mechanisms can interact in risky ways.
Hmac Signatures in Phoenix are typically computed over selected parts of a request — such as method, path, timestamp, and a shared secret — and passed in a header (e.g., X-API-Signature). If the server uses these signatures to authorize sensitive actions (state-changing POSTs, webhook deliveries, or admin links) but does not enforce strict framing protections, an attacker can construct a malicious page that loads the target endpoint in a hidden frame and relies on the user’s authenticated session to send a signed request. Because the signature is tied to the request parameters and not to the framing context, the server may still validate the Hmac and perform the action, believing it is a legitimate, signed request.
For example, consider a Phoenix endpoint that accepts a payment action via GET with an Hmac-signed token in a query parameter. If this endpoint is also rendered in an iframe by a third-party site, an attacker could trick a logged-in user’s browser into submitting the signed URL inside a form or image request. The server verifies the Hmac, sees valid parameters, and proceeds with the transaction. The vulnerability here is not in Hmac itself, but in the absence of anti-clickjacking controls (such as X-Frame-Options or Content-Security-Policy frame-ancestors) combined with state-changing operations that rely solely on signed URLs without requiring a same-origin context or a per-request nonce tied to the session.
Another scenario involves webhook endpoints that verify an Hmac signature to ensure the payload originates from a trusted source. If the webhook response or confirmation page is inadvertently framed by a malicious site, and the UI reflects sensitive information or provides actionable controls without additional CSRF or frame-busting protections, an attacker might leverage social engineering to induce user interaction inside the frame. Because the Hmac validated the webhook origin, the server may render state-modifying UI components or links that are embedded and activated via user click within the attacker’s page, effectively combining UI redress with trusted webhook validation.
To assess this combination during a scan, middleBrick tests unauthenticated attack surfaces across the 12 checks, including Input Validation and SSRF, and flags missing anti-clickjacking headers alongside endpoints that use Hmac Signatures for sensitive actions. This helps identify where signature-based trust is insufficient without complementary framing protections.
Hmac Signatures-Specific Remediation in Phoenix — concrete code fixes
Remediation focuses on two layers: preventing framing of sensitive endpoints and ensuring Hmac usage includes context that cannot be forged by an attacker in a frame. Below are concrete, working examples in Phoenix/Elixir.
1. Prevent framing with HTTP headers
Ensure sensitive responses include anti-clickjacking headers. In your endpoint or a pipeline, set:
defmodule MyAppWeb.Plugs do
import Plug.Conn
def put_frame_options(conn, opts \\ [to: "DENY"]) do
put_resp_header(conn, "x-frame-options", "DENY")
end
def put_csp_frame_ancestors(conn, opts \\ [sources: ["'none'"]]) do
val = Enum.join(opts[:sources], " ")
put_resp_header(conn, "content-security-policy", "frame-ancestors #{val}")
end
end
Use these plugs in your router or pipeline to protect endpoints that perform state changes or render authenticated content.
2. Include nonce or origin in Hmac computation
When generating and verifying Hmac Signatures, include the request origin or a per-request nonce so that a signed payload cannot be reused from an embedded context. Example signature generation and verification:
defmodule MyAppWeb.Hmac do
@secret System.get_env("HMAC_SECRET")
def generate(params) when is_map(params) do
# Include origin and a timestamp to bind the signature to context
payload = Jason.encode!(%{
params: params,
origin: params["origin"],
ts: System.system_time(:second)
})
:crypto.mac(:hmac, :sha256, @secret, payload)
|> Base.encode16(case: :lower)
end
def verify(params, received_sig) do
expected = generate(params)
# Use constant-time compare to avoid timing attacks
:crypto.verify(:hmac, :sha256, @secret, Jason.encode!(params), received_sig)
|> match?({1, _})
end
end
# In a controller or webhook handler:
defmodule MyAppWeb.OrderController do
use MyAppWeb, :controller
def create(conn, %{"amount" => amount, "origin" => origin, "signature" => sig} = params) do
# Ensure the origin matches the request origin to prevent framing from other origins
case MyAppWeb.Hmac.verify(%{"amount" => amount, "origin" => origin}, sig) do
true ->
# Additional check: require the origin header to match the request's origin
if valid_origin?(origin, conn) do
# Process order safely
json(conn, %{status: "ok"})
else
send_resp(conn, 403, "Invalid origin")
end
false ->
send_resp(conn, 401, "Invalid signature")
end
end
defp valid_origin?(origin, conn) do
request_origin = Plug.Conn.get_req_header(conn, "origin") |> List.first()
# Basic same-origin check; tailor to your deployment
origin == request_origin
end
end
This approach binds the Hmac to an explicit origin, making it difficult for an attacker to forge a signed request inside a frame unless they can control the origin header (which browsers do not allow to be set cross-origin).
3. Combine with per-request nonce and strict referrer checks
For additional assurance, include a server-generated nonce in signed requests and validate it on the server. Also enforce strict Referrer-Policy to limit referrer leakage.
defmodule MyAppWeb.Plugs do
import Plug.Conn
def put_nonce_header(conn) do
nonce = :crypto.strong_rand_bytes(16) |> Base.encode16()
conn
|> put_resp_header("x-content-type-options", "nosniff")
|> put_resp_header("referrer-policy", "strict-origin-when-cross-origin")
|> assign(:request_nonce, nonce)
end
end
# Include the nonce in forms and signed requests:
#
# And verify it server-side in your Hmac verification step.