GuardAPI Logo
GuardAPI
Automated Security Protocol

How to fix Insecure Webhooks
in Phoenix

Executive Summary

Webhooks are essentially unauthenticated POST endpoints. Without cryptographic verification, an attacker can spoof payloads to bypass payment gates, escalate privileges, or trigger internal workflows. In Phoenix, the primary hurdle is that Plug.Parsers consumes the request body before your controller sees it, making signature verification impossible unless you capture the raw body during the parsing phase.

The Vulnerable Pattern

VULNERABLE CODE
defmodule MyAppWeb.WebhookController do
  use MyAppWeb, :controller

VULNERABLE: Implicitly trusts the JSON params

def handle_stripe(conn, %{“type” => “checkout.session.completed”, “data” => data}) do user_id = data[“object”][“client_reference_id”] MyApp.Accounts.upgrade_to_premium(user_id) send_resp(conn, 200, "") end end

The Secure Implementation

The fix requires three steps: First, implement a custom body reader to store the raw binary payload in `conn.assigns` because standard parsers 'consume' the stream. Second, retrieve the provider's signature from headers (e.g., X-Hub-Signature or Stripe-Signature). Third, perform an HMAC SHA256 calculation using your shared secret and the raw body. Critically, use `Plug.Crypto.secure_compare/2` to compare the computed hash against the provided signature to prevent timing attacks that could leak the secret byte-by-byte.

SECURE CODE
# 1. Add a CacheBodyReader in your lib/my_app_web/endpoint.ex
defmodule MyAppWeb.CacheBodyReader do
  def read_body(conn, opts) do
    {:ok, body, conn} = Plug.Conn.read_body(conn, opts)
    conn = Plug.Conn.assign(conn, :raw_body, body)
    {:ok, body, conn}
  end
end

2. Update your Endpoint to use it

plug Plug.Parsers, parsers: [:json], body_reader: {MyAppWeb.CacheBodyReader, :read_body, []}

3. Verify in the Controller

defmodule MyAppWeb.WebhookController do use MyAppWeb, :controller

def handle_stripe(conn, _params) do payload = conn.assigns[:raw_body] signature = get_req_header(conn, “stripe-signature”) |> List.first() secret = Application.get_env(:my_app, :webhook_secret)

case verify_signature(payload, signature, secret) do
  :ok -> 
    # Process logic here
    send_resp(conn, 200, "ok")
  {:error, _} -> 
    send_resp(conn, 401, "unauthorized")
end

end

defp verify_signature(payload, signature, secret) do # Use Stripe’s SDK or manual HMAC check # Example manual check: expected = :crypto.mac(:hmac, :sha256, secret, payload) |> Base.encode16(case: :lower) if Plug.Crypto.secure_compare(expected, signature), do: :ok, else: {:error, :invalid} end end

System Alert • ID: 6818
Target: Phoenix API
Potential Vulnerability

Your Phoenix API might be exposed to Insecure Webhooks

74% of Phoenix apps fail this check. Hackers use automated scanners to find this specific flaw. Check your codebase before they do.

RUN FREE SECURITY DIAGNOSTIC
GuardLabs Engine: ONLINE

Free Tier • No Credit Card • Instant Report

Verified by Ghost Labs Security Team

This content is continuously validated by our automated security engine and reviewed by our research team. Ghost Labs analyzes over 500+ vulnerability patterns across 40+ frameworks to provide up-to-date remediation strategies.