Webhook Abuse in Actix with Jwt Tokens
Webhook Abuse in Actix with Jwt Tokens — how this specific combination creates or exposes the vulnerability
Webhook abuse in Actix applications that rely on JWT tokens occurs when an attacker causes the server to make unauthorized or excessive outbound HTTP requests, typically by manipulating inputs that determine the webhook target or the token used for outbound authentication. Because JWTs often carry identity and authorization claims, leaking or replaying them to malicious endpoints can expose internal services and enable further compromise.
In an Actix-based service, webhook configurations may be derived from user input such as request parameters, headers, or JSON payloads. If the application deserializes untrusted data into webhook URLs or into the metadata used to select a JWT for client authentication, an attacker can supply a malicious URL pointing to a server they control. When the Actix handler triggers an outbound call, the request may include sensitive JWTs in headers (e.g., Authorization: Bearer <token>). This can lead to information disclosure, privilege escalation, or data exfiltration if the token has elevated scopes.
Another vector involves replaying captured JWTs. If an attacker observes a valid JWT from an Actix endpoint that is used to call external webhooks, they can replay that token to the same or different webhook endpoints, potentially performing actions under the original service identity. This is particularly dangerous when token lifetimes are long or when the JWT is used across multiple integrations without additional binding to the webhook context.
The combination of dynamic webhook target resolution and JWT usage amplifies risks such as Server-Side Request Forgery (SSRF) and token leakage. For example, an attacker may supply a URL like http://169.254.169.254/latest/meta-data/iam/security-credentials/ and trick the Actix service into issuing a request that includes a JWT, thereby exposing credentials through the webhook flow. Even when JWTs are validated for inbound requests, the same discipline may not apply to outbound webhook calls, creating a security gap that can be discovered by scans such as those performed by middleBrick, which tests unauthenticated attack surfaces across multiple security checks including Authentication and Data Exposure.
Real-world patterns include missing validation of the webhook URL scheme and host, lack of outbound TLS enforcement, and failure to scope JWTs to specific webhook destinations. Without strict allowlists and token binding, Actix services can inadvertently route sensitive requests to attacker-controlled infrastructure, leading to breaches that span authentication bypass, information disclosure, and lateral movement.
Jwt Tokens-Specific Remediation in Actix — concrete code fixes
Remediation focuses on strict validation of webhook targets, binding JWTs to intended recipients, and ensuring that tokens are not unnecessarily exposed. Implement allowlists for webhook URLs, enforce HTTPS, and avoid including sensitive JWTs in logs or error messages. Use Actix middleware to sanitize inputs and apply least-privilege tokens scoped to the webhook operation.
Example: Validated webhook target with scoped JWT
use actix_web::{web, HttpResponse, Responder};
use jsonwebtoken::{encode, Algorithm, DecodingKey, EncodingKey, Header, Validation};
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
struct Claims {
sub: String,
scope: String,
exp: usize,
}
async fn build_webhook_request(
target_url: String,
token_scope: String,
) -> Result {
// Strict allowlist: only https endpoints under your domain
if !target_url.starts_with("https://hooks.yourdomain.com/") {
return Ok(HttpResponse::BadRequest().body("Invalid webhook target"));
}
// Build a scoped JWT for the webhook call
let claims = Claims {
sub: "actix-service".to_string(),
scope: token_scope,
exp: (chrono::Utc::now() + chrono::Duration::minutes(5)).timestamp() as usize,
};
let token = encode(
&Header::new(Algorithm::HS256),
&claims,
&EncodingKey::from_secret("your-secret".as_ref()),
)?;
let client = reqwest::Client::new();
let res = client
.post(&target_url)
.bearer_auth(token)
.json(&serde_json::json!({"event": "order.created"}))
.send()
.await?;
Ok(HttpResponse::Ok().body(format!("Webhook sent: {}", res.status())))
}
async fn webhook_handler(body: web::Json) -> impl Responder {
// Example input derived safely from validated context, not raw user input
let target = "https://hooks.yourdomain.com/order".to_string();
let scope = "webhook:order:create".to_string();
match build_webhook_request(target, scope).await {
Ok(resp) => resp,
Err(e) => HttpResponse::InternalServerError().body(e.to_string()),
}
}
Example: JWT validation and secure outbound call
use actix_web::{middleware, web, App, HttpResponse, HttpServer};
use reqwest::Client;
async fn call_webhook_with_bound_token(url: &str, jwt: &str) -> Result<(), reqwest::Error> {
let client = Client::new();
client
.post(url)
.bearer_auth(jwt)
.header("X-Webhook-Scope", "limited")
.body("{}")
.send()
.await?
.error_for_status()?;
Ok(())
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
HttpServer::new(|| {
App::new()
.wrap(middleware::Logger::default())
.route("/trigger", web::post().to(|web::Json(payload): web::Json| async move {
// Validate payload and extract webhook target from a controlled mapping
let target = match payload.get("hook").and_then(|v| v.as_str()) {
Some("order") => "https://hooks.yourdomain.com/order",
Some("notify") => "https://hooks.yourdomain.com/notify",
_ => return HttpResponse::BadRequest().finish(),
};
// Retrieve JWT from a secure source (e.g., environment, vault), not user input
let jwt = std::env::var("WEBHOOK_JWT").unwrap_or_default();
if jwt.is_empty() {
return HttpResponse::InternalServerError().body("Missing webhook token");
}
actix_web::rt::spawn(async move {
if let Err(e) = call_webhook_with_bound_token(target, &jwt).await {
eprintln!("Webhook failed: {:?}", e);
}
});
HttpResponse::Accepted().finish()
}))
})
.bind("127.0.0.1:8080")?
.run()
.await
}
Operational practices
- Validate webhook URLs against an allowlist of hostnames and enforce HTTPS.
- Use short-lived, scoped JWTs dedicated to webhook calls, avoiding broad scopes present in user tokens.
- Do not log full JWTs or include them in error responses; mask tokens in observability outputs.
- Consider binding tokens to the webhook destination via audience (aud) claims where supported by the identity provider.
- Monitor outbound calls for anomalies and integrate with a security scanning solution like middleBrick to detect misconfigurations early.