From 1473e726f3d4a3dfba2f07134d4237ebc8e2c1b2 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 13 Aug 2026 02:10:09 +0000 Subject: [PATCH] webhook: compare secret in constant time payload.secret != expected_secret short-circuits on the first differing byte, which leaks timing information about how many leading bytes of a guess are correct. Use subtle::ConstantTimeEq instead. --- Cargo.toml | 1 + src/webhook.rs | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 2bc0ec5..f5459ae 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,7 @@ reqwest = { serde = { version = "1", features = ["derive"] } serde_json = "1" sha2 = "0.10" +subtle = "2" sqlx = { version = "0.8", features = [ diff --git a/src/webhook.rs b/src/webhook.rs index 2223c17..fab543e 100644 --- a/src/webhook.rs +++ b/src/webhook.rs @@ -1,5 +1,6 @@ use axum::{extract::State, http::StatusCode, response::IntoResponse}; use serde::Deserialize; +use subtle::ConstantTimeEq; use crate::AppState; @@ -28,7 +29,9 @@ pub async fn receive( .into_response(); }; - if payload.secret != expected_secret { + let secret_matches = payload.secret.as_bytes().ct_eq(expected_secret.as_bytes()); + + if !bool::from(secret_matches) { tracing::warn!("received webhook with invalid secret"); return (StatusCode::UNAUTHORIZED, "invalid webhook secret").into_response();