GuardAPI Logo
GuardAPI

Fix Insecure Webhooks in Axum

Webhooks are an attack vector often overlooked by developers who treat internal-facing endpoints as implicitly trusted. In Axum, the default behavior of the Json extractor is to deserialize any valid JSON payload, regardless of its origin. Without cryptographic verification—typically via HMAC signatures—an attacker can spoof events, trigger unauthorized state changes, or perform mass-data injection by simply POSTing to your endpoint. To secure this, you must intercept the raw request body, compute the HMAC-SHA256 hash using a shared secret, and perform a constant-time comparison against the provided signature header.

The Vulnerable Pattern

use ax_um::{routing::post, Json, Router};
use serde::Deserialize;

#[derive(Deserialize)] struct PaymentPayload { order_id: String, status: String, }

// VULNERABLE: No signature verification. // Anyone can POST to this endpoint and mark orders as ‘paid’. async fn insecure_webhook(Json(payload): Json) { if payload.status == “paid” { println!(“Processing payment for order: {}”, payload.order_id); } }

pub fn app() -> Router { Router::new().route(“/webhook”, post(insecure_webhook)) }

The Secure Implementation

The fix involves three critical steps. First, use the `Bytes` extractor instead of `Json` to access the raw, unmodified request body; deserializing first can alter the byte sequence and break the hash. Second, use the `hmac` and `sha2` crates to re-calculate the signature using your shared secret. Third, use `mac.verify_slice()`, which implements a constant-time comparison. Never use standard equality operators (`==`) for signature checks, as they are susceptible to timing attacks that allow an attacker to brute-force the signature byte-by-byte.

use axum::{http::{HeaderMap, StatusCode}, body::Bytes, routing::post, Router};
use hmac::{Hmac, Mac};
use sha2::Sha256;
use hex;

type HmacSha256 = Hmac; const WEBHOOK_SECRET: &[u8] = b”your_secure_shared_secret”;

async fn secure_webhook(headers: HeaderMap, body: Bytes) -> StatusCode { // 1. Extract signature header (e.g., X-Hub-Signature-256) let Some(sig_header) = headers.get(“x-signature-256”).and_then(|v| v.to_str().ok()) else { return StatusCode::UNAUTHORIZED; };

// 2. Decode hex signature
let Ok(expected_sig) = hex::decode(sig_header) else {
    return StatusCode::BAD_REQUEST;
};

// 3. Compute HMAC of the raw body
let mut mac = HmacSha256::new_from_slice(WEBHOOK_SECRET).expect("HMAC can take key of any size");
mac.update(&body);

// 4. Constant-time verification to prevent timing attacks
if mac.verify_slice(&expected_sig).is_err() {
    return StatusCode::UNAUTHORIZED;
}

// 5. Only now is it safe to deserialize and process the body
// let payload: PaymentPayload = serde_json::from_slice(&body).unwrap();
StatusCode::OK

}

pub fn app() -> Router { Router::new().route(“/webhook”, post(secure_webhook)) }

System Alert • ID: 4748
Target: Axum API
Potential Vulnerability

Your Axum API might be exposed to Insecure Webhooks

74% of Axum 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.