Webhook Spoofing in Axum
How Webhook Spoofing Manifests in Axum
In Axum, webhook spoofing typically occurs when an endpoint accepts incoming webhook requests (e.g., from GitHub, Stripe, or Slack) without properly verifying the request's authenticity. Attackers can forge requests by guessing or stealing shared secrets, or by exploiting missing signature validation. For example, an Axum route that processes GitHub webhooks might rely solely on the X-Hub-Signature-256 header but fail to compute and compare the HMAC-SHA256 digest correctly, allowing an attacker to spoof events.
Consider a common Axum handler that extracts the signature and compares it using a simple equality check:
use axum::extract::{State, Header};
use axum::http::StatusCode;
use axum::response::IntoResponse;
use serde::Deserialize;
#[derive(Deserialize)]
struct Payload {
action: String,
}
async fn github_webhook_handler(
State(state): State,
Header(header): Header<"x-hub-signature-256">,
axum::Json(payload): axum::Json,
) -> impl IntoResponse {
let expected = format!("sha256={}", hex::encode(hmac_sha256(
&state.webhook_secret,
&payload_bytes,
)));
if expected != header {
return Err((StatusCode::UNAUTHORIZED, "Invalid signature"));
}
// Process payload...
Ok(StatusCode::ACCEPTED)
}
This code is vulnerable if payload_bytes is not the raw request body — for instance, if Axum's Json extractor has already consumed and parsed the body, altering its bytes. An attacker could exploit this by sending a request with a valid-looking signature but a modified body, bypassing validation due to a time-of-check/time-of-use (TOCTOU) issue or incorrect body retrieval. Such flaws map to OWASP API Security Top 10:2023 API1:2023 Broken Object Level Authorization (BOLA) when combined with improper input validation, as the server may act on spoofed events as if they were legitimate.
Axum-Specific Detection
Detecting webhook spoofing in Axum requires analyzing both the route definition and how the request body and signatures are handled. middleBrick identifies this issue during its unauthenticated black-box scan by sending crafted webhook payloads with manipulated signatures and bodies to detect whether the endpoint properly validates authenticity. It checks for missing or weak signature verification, replay vulnerability (lack of timestamp/nonce checks), and improper body hashing.
For example, middleBrick will send a request to a suspected webhook endpoint with:
- A valid-looking
X-Hub-Signature-256header but a tampered JSON body - A missing signature header
- A signature using an outdated algorithm (e.g., SHA1)
- A replayed payload with an old timestamp
If the endpoint processes any of these without rejecting them, middleBrick flags it as a potential webhook spoofing vulnerability under the "Input Validation" and "Authentication" checks, providing a severity rating based on the potential impact (e.g., unauthorized code deployment, financial fraud). The scan also cross-references any provided OpenAPI spec to confirm whether the webhook endpoint is documented and whether security requirements (like apiKey or http signature schemes) are defined and enforced.
This detection is particularly valuable because Axum’s type-safe extractors can sometimes mask body access issues — middleBrick’s runtime behavior analysis catches logic flaws that static analysis might miss, such as when the raw body is not available due to prior extraction by another middleware or extractor.
Axum-Specific Remediation
To fix webhook spoofing in Axum, ensure that the raw request body is accessed before any parsing extractors (like Json) and that the signature is verified using a constant-time comparison to prevent timing attacks. Use Axum’s bytes::Bytes or String extractor to capture the body early, then compute the HMAC digest.
Here’s a corrected Axum handler for GitHub webhooks:
use axum::extract::{State, Header, Bytes};
use axum::http::StatusCode;
use axum::response::IntoResponse;
use axum::Json;
use constant_time_eq::constant_time_eq;
use hex;
use hmac::{Hmac, Mac};
use sha2::Sha256;
type HmacSha256 = Hmac;
async fn github_webhook_handler(
State(state): State,
Header(header): Header<"x-hub-signature-256">,
Header(event): Header<"x-github-event">,
body: Bytes, // Capture raw body BEFORE Json extractor
Json(payload): Json, // Now safe to parse
) -> impl IntoResponse {
// Verify signature using raw body
let mut mac = HmacSha256::new_from_slice(state.webhook_secret.as_bytes())
.expect("HMAC can take key of any size");
mac.update(&body);
let result = mac.finalize();
let expected_bytes = result.into_bytes();
// Parse expected signature from header (format: "sha256=...")
let Some(sig_hex) = header.strip_prefix("sha256=") else {
return Err((StatusCode::BAD_REQUEST, "Missing sha256 prefix"));
};
let Ok(sig_bytes) = hex::decode(sig_hex) else {
return Err((StatusCode::BAD_REQUEST, "Invalid hex in signature"));
};
// Constant-time comparison to prevent timing attacks
if !constant_time_eq(&expected_bytes, &sig_bytes) {
return Err((StatusCode::UNAUTHORIZED, "Invalid signature"));
}
// Optional: Verify timestamp (X-Hub-Signature-256 doesn't include it,
// but GitHub also sends X-Hub-Signature; for timestamp, use X-Github-Delivery + X-Hub-Timestamp if using webhook configs)
// For GitHub, rely on signature; for Stripe, check timestamp.
// Process event...
Ok((StatusCode::ACCEPTED, Json({"status": "processed"})))
}
Key fixes:
Bytesextractor captures the raw body beforeJsonparsing- HMAC-SHA256 is computed over the exact bytes received
- Signature comparison uses
constant_time_eqto avoid timing leaks - Header parsing validates the
sha256=prefix and hex format - The handler now resists both spoofing and replay attacks (if combined with timestamp checks where applicable)
For Stripe or Slack webhooks, adjust the header names and signature schemes accordingly (e.g., Stripe uses t and v1 timestamps). Always refer to the provider’s documentation for the exact verification method. middleBrick’s Pro plan can continuously monitor such endpoints to ensure fixes remain effective over time.