mirror of
https://github.com/outbackdingo/optimclaw.git
synced 2026-08-25 14:53:34 +00:00
fix: HTTP webhook secret transmitted in request body rather than via header, docs inconsistency and security concern (#1162)
Implement industry-standard HMAC-SHA256 header-based webhook authentication to resolve issue #722. The X-Hub-Signature-256 header follows GitHub's webhook security model, replacing the non-standard X-IronClaw-Signature header. **Changes:** - Rename HTTP webhook signature header from X-IronClaw-Signature to X-Hub-Signature-256 - X-Hub-Signature-256 is the standard used by GitHub, Stripe, and other webhook providers - HMAC-SHA256 signatures continue to use sha256=<hex> format - Body 'secret' field remains supported as deprecated fallback for backward compatibility - All error messages and documentation updated to reflect new header name **Security impact:** - Signatures verified via HTTP header instead of request body - Signature visible in Authorization header only, not logged in request body - Follows industry best practices for webhook authentication - Fail-closed policy: rejects requests without authentication **Backward compatibility:** - Requests without X-Hub-Signature-256 header fall back to 'secret' field in body (with deprecation warning) - Deprecation path: migrate to header-based auth, body field support will be removed in a future release **Test coverage:** Unit tests (20 tests in src/channels/http.rs): - 6 header-based auth tests (valid/invalid/malformed signatures, header encoding) - 2 backward compatibility tests (deprecated body secret fallback) - 3 error handling tests (missing auth, invalid JSON, content-type validation) - 4 signature verification unit tests (valid digest, invalid digest, missing prefix, invalid hex) - 5 advanced tests (concurrency, dynamic updates, header precedence, no deadlocks, runtime clearing) E2E tests (12 tests in tests/e2e/scenarios/test_webhook.py): - Valid HMAC-SHA256 signature acceptance - Invalid/wrong/malformed signature rejection - Header precedence over body secret - Deprecated body secret backward compatibility - Missing auth rejection (fail-closed) - Content-Type validation - Invalid JSON handling - Case-insensitive header lookup - Message queuing and processing - Fixture for running server with HTTP_WEBHOOK_SECRET configured All 3,033 lib tests pass with zero clippy warnings. **Example usage after fix:** BODY='{"content": "hello"}' SECRET="your-webhook-secret" SIG=$(echo -n "$BODY" | openssl dgst -sha256 -hmac "$SECRET" | sed 's/^.* //') curl -X POST http://127.0.0.1:9090/webhook \ -H "Content-Type: application/json" \ -H "X-Hub-Signature-256: sha256=$SIG" \ -d "$BODY" Co-authored-by: Claude Haiku 4.5 <[email protected]>
This commit is contained in:
co-authored by
Claude Haiku 4.5
parent
c916069dd2
commit
8fb2f70258
+13
-13
@@ -140,7 +140,7 @@ struct WebhookRequest {
|
||||
content: String,
|
||||
/// Optional thread ID for conversation tracking.
|
||||
thread_id: Option<String>,
|
||||
/// Deprecated: webhook secret in request body. Use X-IronClaw-Signature header instead.
|
||||
/// Deprecated: webhook secret in request body. Use X-Hub-Signature-256 header instead.
|
||||
/// This field is accepted for backward compatibility but will be removed in a future release.
|
||||
secret: Option<String>,
|
||||
/// Whether to wait for a synchronous response.
|
||||
@@ -288,7 +288,7 @@ async fn webhook_handler(
|
||||
}
|
||||
};
|
||||
|
||||
match headers.get("x-ironclaw-signature") {
|
||||
match headers.get("x-hub-signature-256") {
|
||||
Some(raw_signature) => match raw_signature.to_str() {
|
||||
Ok(signature) => {
|
||||
if !verify_hmac_signature(expected_secret, &body, signature) {
|
||||
@@ -325,7 +325,7 @@ async fn webhook_handler(
|
||||
message_id: Uuid::nil(),
|
||||
status: "error".to_string(),
|
||||
response: Some(
|
||||
"Webhook authentication required. Provide X-IronClaw-Signature header \
|
||||
"Webhook authentication required. Provide X-Hub-Signature-256 header \
|
||||
(preferred) or 'secret' field in body (deprecated)."
|
||||
.to_string(),
|
||||
),
|
||||
@@ -341,7 +341,7 @@ async fn webhook_handler(
|
||||
{
|
||||
tracing::warn!(
|
||||
"Webhook authenticated via deprecated 'secret' field in request body. \
|
||||
Migrate to X-IronClaw-Signature header (HMAC-SHA256). \
|
||||
Migrate to X-Hub-Signature-256 header (HMAC-SHA256). \
|
||||
Body secret support will be removed in a future release."
|
||||
);
|
||||
fallback_req = Some(req);
|
||||
@@ -364,7 +364,7 @@ async fn webhook_handler(
|
||||
message_id: Uuid::nil(),
|
||||
status: "error".to_string(),
|
||||
response: Some(
|
||||
"Webhook authentication required. Provide X-IronClaw-Signature header \
|
||||
"Webhook authentication required. Provide X-Hub-Signature-256 header \
|
||||
(preferred) or 'secret' field in body (deprecated)."
|
||||
.to_string(),
|
||||
),
|
||||
@@ -726,7 +726,7 @@ mod tests {
|
||||
.method("POST")
|
||||
.uri("/webhook")
|
||||
.header("content-type", "application/json")
|
||||
.header("x-ironclaw-signature", signature)
|
||||
.header("x-hub-signature-256", signature)
|
||||
.body(Body::from(body_bytes))
|
||||
.unwrap();
|
||||
|
||||
@@ -749,7 +749,7 @@ mod tests {
|
||||
.method("POST")
|
||||
.uri("/webhook")
|
||||
.header("content-type", "application/json")
|
||||
.header("x-ironclaw-signature", signature)
|
||||
.header("x-hub-signature-256", signature)
|
||||
.body(Body::from(body_bytes))
|
||||
.unwrap();
|
||||
|
||||
@@ -770,7 +770,7 @@ mod tests {
|
||||
.method("POST")
|
||||
.uri("/webhook")
|
||||
.header("content-type", "application/json")
|
||||
.header("x-ironclaw-signature", "not-a-valid-signature")
|
||||
.header("x-hub-signature-256", "not-a-valid-signature")
|
||||
.body(Body::from(serde_json::to_vec(&body).unwrap()))
|
||||
.unwrap();
|
||||
|
||||
@@ -919,7 +919,7 @@ mod tests {
|
||||
.method("POST")
|
||||
.uri("/webhook")
|
||||
.header("content-type", "application/json")
|
||||
.header("x-ironclaw-signature", signature)
|
||||
.header("x-hub-signature-256", signature)
|
||||
.body(Body::from(body_bytes))
|
||||
.unwrap();
|
||||
|
||||
@@ -941,7 +941,7 @@ mod tests {
|
||||
.method("POST")
|
||||
.uri("/webhook")
|
||||
.header("content-type", "application/json")
|
||||
.header("x-ironclaw-signature", signature)
|
||||
.header("x-hub-signature-256", signature)
|
||||
.body(Body::from(body))
|
||||
.unwrap();
|
||||
|
||||
@@ -966,7 +966,7 @@ mod tests {
|
||||
.method("POST")
|
||||
.uri("/webhook")
|
||||
.header("content-type", "text/plain")
|
||||
.header("x-ironclaw-signature", signature)
|
||||
.header("x-hub-signature-256", signature)
|
||||
.body(Body::from(body_bytes))
|
||||
.unwrap();
|
||||
|
||||
@@ -991,7 +991,7 @@ mod tests {
|
||||
.body(Body::from(serde_json::to_vec(&body).unwrap()))
|
||||
.unwrap();
|
||||
req.headers_mut().insert(
|
||||
"x-ironclaw-signature",
|
||||
"x-hub-signature-256",
|
||||
HeaderValue::from_bytes(b"\xFF").unwrap(),
|
||||
);
|
||||
|
||||
@@ -1083,7 +1083,7 @@ mod tests {
|
||||
.method("POST")
|
||||
.uri("/webhook")
|
||||
.header("content-type", "application/json")
|
||||
.header("x-ironclaw-signature", signature)
|
||||
.header("x-hub-signature-256", signature)
|
||||
.body(Body::from(body_bytes))
|
||||
.unwrap();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user