Webhook Abuse in Axum with Api Keys
Webhook Abuse in Axum with Api Keys — how this specific combination creates or exposes the vulnerability
Webhook abuse in Axum when protected only by static API keys arises because the webhook endpoint is effectively public and the key is often transmitted as a query parameter or a static header. Axum does not provide built-in webhook signature validation; if you rely solely on an API key that is static and predictable, an attacker who discovers the key can forge requests that appear legitimate.
Consider a typical Axum webhook handler that expects an x-api-key header:
use axum::{routing::post, Router};
use axum::http::HeaderMap;
use axum::extract::State;
use std::sync::Arc;
struct AppState {
webhook_key: String,
}
async fn webhook_handler(
State(state): State<Arc<AppState>>,
headers: HeaderMap,
) -> Result<(), (axum::http::StatusCode, String)> {
let provided = headers.get("x-api-key")
.and_then(|v| v.to_str().ok())
.ok_or_else(|| (axum::http::StatusCode::UNAUTHORIZED, "Missing key".to_string()))?;
if provided != state.webhook_key {
return Err((axum::http::StatusCode::UNAUTHORIZED, "Invalid key".to_string()));
}
// process webhook payload
Ok(())
}
#[tokio::main]
async fn main() {
let state = Arc::new(AppState { webhook_key: "static-secret-key".to_string() });
let app = Router::new()
.route("/webhook", post(webhook_handler))
.with_state(state);
// axum::Server::bind(&"0.0.0.0:3000".parse().unwrap()).serve(app.into_make_service()).await.unwrap();
}
If the API key is static and exposed in logs, error messages, or client-side code, an attacker can replay captured requests or send crafted payloads to the webhook endpoint. This becomes webhook abuse because the endpoint can be triggered arbitrarily to cause unintended actions such as creating resources, sending notifications, or invoking downstream services. Because the check is only a static equality test, there is no per-request secret or rotating element, making replay straightforward.
Additionally, if the webhook consumer does not verify the origin of the request beyond the API key, there is no protection against SSRF or IP spoofing within a trusted network. MiddleBrick scans detect such patterns by correlating static authentication usage with webhook definitions in the OpenAPI spec and runtime behavior, highlighting the absence of HMAC signatures or replay protections.
Api Keys-Specific Remediation in Axum — concrete code fixes
To remediate webhook abuse when using API keys in Axum, move from static, shared keys to per-request or per-delivery secrets and add replay resistance. One approach is to use HMAC signatures where the sender provides a signature header derived from the payload and a shared secret, and the server recomputes and verifies the signature.
Example of a safer webhook setup using HMAC-SHA256 in Axum:
use axum::{routing::post, Router, http::HeaderMap};
use axum::extract::State;
use hmac::{Hmac, Mac};
use sha2::Sha256;
use std::sync::Arc;
type HmacSha256 = Hmac<Sha256>;
struct WebhookState {
signing_secret: String,
}
async fn webhook_handler(
State(state): State<Arc<WebhookState>>,
headers: HeaderMap,
body: String,
) -> Result<(), (axum::http::StatusCode, String)> {
let signature = headers.get("x-hub-signature-256")
.and_then(|v| v.to_str().ok())
.ok_or_else(|| (axum::http::StatusCode::BAD_REQUEST, "Missing signature".to_string()))?;
// signature format: "sha256=hex"
let sig_hex = signature.strip_prefix("sha256=")
.ok_or_else(|| (axum::http::StatusCode::BAD_REQUEST, "Invalid signature format".to_string()))?;
let mut mac = HmacSha256::new_from_slice(state.signing_secret.as_bytes())
.map_err(|_| (axum::http::StatusCode::INTERNAL_SERVER_ERROR, "HMAC init error".to_string()))?;
mac.update(body.as_bytes());
let computed = mac.finalize();
let computed_bytes = computed.into_bytes();
let computed_hex = hex::encode(computed_bytes);
if subtle::ConstantTimeEq::ct_eq(computed_hex.as_bytes(), sig_hex.as_bytes()).into() {
// process webhook payload
Ok(())
} else {
Err((axum::http::StatusCode::UNAUTHORIZED, "Invalid signature".to_string()))
}
}
#[tokio::main]
async fn main() {
let state = Arc::new(WebhookState { signing_secret: "super-secret-change-me".to_string() });
let app = Router::new()
.route("/webhook", post(webhook_handler))
.with_state(state);
// axum::Server::bind(&"0.0.0.0:3000".parse().unwrap()).serve(app.into_make_service()).await.unwrap();
}
In this pattern, the shared secret is used to create an HMAC over the request body. The sender must also compute the same HMAC and include it in the x-hub-signature-256 header. This prevents replay because each request can be made unique (e.g., including a timestamp or nonce in the body), and the server can optionally enforce freshness. Additionally, rotate the signing secret periodically and avoid logging it to reduce exposure.
For simpler cases where HMAC is not feasible, at minimum ensure API keys are transmitted over TLS, are long and random, rotated frequently, and are not embedded in client-side code or logs. MiddleBrick’s scans will highlight whether your webhook endpoints show static key usage and can guide you toward implementing per-request tokens or signature schemes.