Webhook Abuse in Axum with Jwt Tokens
Webhook Abuse in Axum with Jwt Tokens — how this specific combination creates or exposes the vulnerability
Webhook abuse in an Axum service that relies on JWT tokens occurs when an attacker triggers downstream HTTP callbacks without authorization, often because token validation is incomplete or applied inconsistently. In Axum, a common pattern is to issue a JWT after initial authentication and then use that token to authorize outbound webhook calls to third‑party services. If the service constructs webhook requests by copying claims from the original token (e.g., subject or scopes) without revalidating permissions for the webhook context, it may over‑authorize the call. For example, a token issued for read‑only user data might be reused to invoke a privileged webhook that modifies resources, because the service trusts token scopes that were valid for the API endpoint but not for the external callback.
Another vector is token replay across services. An attacker who intercepts a JWT (e.g., via logs or insecure storage) can replay it to a webhook endpoint that lacks nonce or idempotency checks. Axum handlers that deserialize tokens using a shared secret but do not bind the token to the webhook request context (such as a unique event ID or timestamp) may process the same malicious payload multiple times. Additionally, if the application embeds the webhook URL inside the token payload (e.g., as a custom claim) and trusts that value when making outbound requests, an attacker who can tamper with or influence the token can redirect webhooks to malicious endpoints, enabling data exfiltration or server‑side request forgery against internal services.
These issues map to common API weaknesses such as Broken Object Level Authorization (BOLA) when token scopes are misaligned with webhook actions, and unsafe consumption of external endpoints. Because webhooks often carry high trust (triggered by events rather than interactive users), the impact can include unauthorized data modification or unintended external calls. MiddleBrick scans detect patterns where JWT scopes are broad and webhook destinations are not independently verified, surfacing the risk before an attacker can exploit the chain of misaligned authorization and callback handling.
Jwt Tokens-Specific Remediation in Axum — concrete code fixes
Remediation centers on strict token validation, scope minimization, and binding tokens to the webhook context. In Axum, validate the JWT immediately upon entry using a verified key set and enforce audience and issuer claims explicitly. Do not propagate raw token claims to webhook construction; instead, derive minimal required permissions from revalidated session or application state. Below are concrete code examples that demonstrate a secure pattern.
use axum::{routing::post, Router};
use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation, TokenData};
use serde::{Deserialize, Serialize};
use std::net::SocketAddr;
#[derive(Debug, Serialize, Deserialize)]
struct Claims {
sub: String,
scopes: Vec,
exp: usize,
// Do not embed webhook-specific targets in this claim
}
async fn validate_token(token: &str) -> Result, jsonwebtoken::errors::Error> {
let mut validation = Validation::new(Algorithm::HS256);
validation.validate_exp = true;
validation.validate_nbf = true;
validation.set_audience(&["my-api.example.com"]);
validation.set_issuer(&["auth.example.com"]);
let key = DecodingKey::from_secret("YOUR_STRONG_SECRET".as_ref());
decode::(token, &key, &validation)
}
// Minimal scope check for webhook initiation
async fn can_initiate_webhook(claims: &Claims, required_scope: &str) -> bool {
claims.scopes.iter().any(|s| s == required_scope)
}
async fn webhook_handler(
axum::extract::Json(payload): axum::extract::Json,
axum::extract::headers::Authorization auth,
) -> Result {
let token = auth.token();
let data = validate_token(token).await.map_err(|e| (axum::http::StatusCode::UNAUTHORIZED, e.to_string()))?;
// Enforce scope at the handler level; do not reuse the token for outbound decisions
if !can_initiate_webhook(&data.claims, "webhook:send").await {
return Err((axum::http::StatusCode::FORBIDDEN, "Insufficient scope".into()));
}
// Derive webhook target from application config or a verified mapping, NOT from token claims
let webhook_url = "https://partner.example.com/callback";
// Example: send minimal, verified payload
let client = reqwest::Client::new();
let res = client.post(webhook_url)
.bearer_auth(token) // optional: forward token only if partner expects it and is validated
.json(&payload)
.send()
.await
.map_err(|e| (axum::http::StatusCode::BAD_GATEWAY, e.to_string()))?;
if res.status().is_success() {
Ok("Accepted")
} else {
Err((axum::http::StatusCode::BAD_GATEWAY, "Webhook failed".into()))
}
}
fn app() -> Router {
Router::new().route("/trigger", post(webhook_handler))
}
#[tokio::main]
async fn main() {
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
axum::Server::from_tcp(listener.into_std().unwrap())
.unwrap()
.serve(app().into_make_service())
.await
.unwrap();
}
Key practices illustrated:
- Validate exp, nbf, audience, and issuer on every request; do not rely on token contents alone for authorization decisions.
- Check granular scopes at the handler and deny by default; avoid broad scopes like "*" for webhook initiation.
- Do not embed or trust webhook URLs inside JWT claims; resolve targets from server‑side configuration or verified mappings.
- Use short‑lived tokens and consider per‑event nonces or idempotency keys when the webhook is invoked to reduce replay impact.
By combining strict JWT validation in Axum with context‑bound authorization checks, you reduce the likelihood of webhook abuse while maintaining a clear separation between API authentication and downstream callback integrity.