{"record":{"id":"a3835db2212c1645","repo":"windmill-labs/windmill","slug":"signature-mismatch-a3835d","errorCode":null,"errorMessage":"signature mismatch","messagePattern":"signature mismatch","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"backend/windmill-oauth/src/lib.rs","lineNumber":1124,"sourceCode":"pub struct SlackVerifier {\n    mac: HmacSha256,\n}\n\nimpl SlackVerifier {\n    pub fn new<S: AsRef<[u8]>>(secret: S) -> anyhow::Result<SlackVerifier> {\n        HmacSha256::new_from_slice(secret.as_ref())\n            .map(|mac| SlackVerifier { mac })\n            .map_err(|_| anyhow::anyhow!(\"invalid secret\"))\n    }\n\n    pub fn verify(&self, ts: &str, body: &str, exp_sig: &str) -> anyhow::Result<()> {\n        let basestring = format!(\"v0:{}:{}\", ts, body);\n        let mut mac = self.mac.clone();\n\n        mac.update(basestring.as_bytes());\n        let sig = format!(\"v0={}\", hex::encode(mac.finalize().into_bytes()));\n        if sig != exp_sig {\n            Err(anyhow::anyhow!(\"signature mismatch\"))?;\n        }\n        Ok(())\n    }\n}\n\n/// Fetch user info from OAuth provider\npub async fn http_get_user_info<T: DeserializeOwned>(\n    http_client: &reqwest::Client,\n    url: &str,\n    token: &str,\n) -> error::Result<T> {\n    let res = http_client\n        .get(url)\n        .bearer_auth(token)\n        .send()\n        .await\n        .map_err(to_anyhow)\n        .map_err(|e| error::Error::InternalErr(format!(\"failed to fetch user info: {}\", e)))?;","sourceCodeStart":1106,"sourceCodeEnd":1142,"githubUrl":"https://github.com/windmill-labs/windmill/blob/e474e8803ce2ff5c2df09a58dab51d45f5c922ca/backend/windmill-oauth/src/lib.rs#L1106-L1142","documentation":"SlackVerifier::verify recomputes the Slack 'v0' HMAC-SHA256 signature over \"v0:{timestamp}:{body}\" using the signing secret and compares it to the X-Slack-Signature header. Any mismatch means the request was not signed with the expected secret or the payload changed in transit.","triggerScenarios":"verify(ts, body, expected_sig) called with a timestamp/body that differs from what Slack signed, or an expected signature computed with a different signing secret.","commonSituations":"A proxy or framework re-serializing/reordering the JSON body before verification; verifying against the wrong Slack app's signing secret (multiple Slack apps / workspaces); replayed or modified webhooks; comparing against a truncated or URL-decoded signature.","solutions":["Verify the exact raw request body bytes are passed to verify — never a re-serialized or pretty-printed JSON","Confirm the signing secret matches the Slack app that sent the webhook (reset/re-copy from Slack app settings)","Ensure the timestamp header (X-Slack-Request-Timestamp) is forwarded unmodified","Check no reverse proxy (nginx, Cloudflare) is rewriting the body (e.g. gzip decompression with re-encoding, WAF modification)"],"exampleFix":"// before (body re-serialized by handler framework)\nlet body = serde_json::to_string(&payload)?; // alters key order/spacing\nverify(&ts, &body, &sig)?;\n// after\nlet body = raw_body_bytes; // exact bytes Slack sent\nverify(&ts, &body, &sig)?;","handlingStrategy":"try-catch","validationCode":"// Before verifying, ensure you have raw bytes and a plausible v0 signature\nif !exp_sig.starts_with(\"v0=\") { anyhow::bail!(\"not a Slack v0 signature\"); }\nif ts.parse::<i64>().map(|t| now - t > 300).unwrap_or(true) { anyhow::bail!(\"stale or bad timestamp\"); }","typeGuard":"fn is_v0_signature(s: &str) -> bool { s.starts_with(\"v0=\") && s[3..].len() == 64 && s[3..].bytes().all(|b| b.is_ascii_hexdigit()) }","tryCatchPattern":"if let Err(e) = verifier.verify(&ts, &raw_body, &sig) {\n    if e.to_string() == \"signature mismatch\" {\n        // reject the webhook: wrong secret or mutated body\n        return HttpResponse::Unauthorized().finish();\n    }\n    return HttpResponse::BadRequest().finish();\n}","preventionTips":["Always hash the exact raw request body bytes, never a re-serialized struct","Reject requests older than ~5 minutes (Slack recommends timestamp replay protection)","Keep one signing secret per Slack app and route webhooks accordingly","Check for proxies/WAFs that might rewrite request bodies"],"tags":["slack","hmac","webhook","signature-verification","security"],"backgroundTag":"hmac-signature-mismatch","analyzedSha":"e474e8803ce2ff5c2df09a58dab51d45f5c922ca","analyzedAt":"2026-09-03T12:38:19.024Z","contentChangedAt":"2026-09-03T12:38:19.024Z","schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}