mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-12 08:05:44 +00:00
fix(triggers): prevent Zoom challenge handler from being used as a signing oracle (#9413)
The Zoom URL-validation challenge handler in `handle_challenge_request`
would HMAC-sign any arbitrary `plainToken` and return the result. Since
Zoom webhook verification checks `HMAC-SHA256(secret, "v0:{ts}:{body}")`,
an attacker could craft a `plainToken` in that format to obtain a valid
signature for a forged body, bypassing authentication on a later request.
Unlike the Twitch handler, the Zoom handler verifies no signature on the
challenge request (Zoom's protocol does not include one). Reject any
`plainToken` containing `:` or longer than 128 chars: legitimate Zoom
validation tokens are short random hex strings that never contain colons,
while the exploit requires the colon-bearing `v0:{ts}:{body}` format.
Fixes WIN-2008
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
9e6559a6f6
commit
ab2a15b2a8
@@ -337,6 +337,20 @@ mod zoom {
|
|||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Prevent this challenge endpoint from being used as a signing oracle.
|
||||||
|
// Legitimate Zoom validation tokens are short random hex strings that
|
||||||
|
// never contain colons. The exploit requires crafting a plainToken in the
|
||||||
|
// `v0:{timestamp}:{body}` webhook-signing format (always containing colons)
|
||||||
|
// to obtain a valid signature for an arbitrary body. Reject any token that
|
||||||
|
// does not look like a legitimate Zoom validation token.
|
||||||
|
if zoom_request_body.payload.plain_token.contains(':')
|
||||||
|
|| zoom_request_body.payload.plain_token.len() > 128
|
||||||
|
{
|
||||||
|
return Err(AuthenticationError::InvalidChallengeResponse(
|
||||||
|
"Zoom: invalid plainToken format".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
let hmac_signature = calculate_hmac_signature(
|
let hmac_signature = calculate_hmac_signature(
|
||||||
HmacAlgorithm::Sha256,
|
HmacAlgorithm::Sha256,
|
||||||
&signature_config_data.secret_key,
|
&signature_config_data.secret_key,
|
||||||
@@ -1540,6 +1554,52 @@ mod tests {
|
|||||||
assert!(response.is_none());
|
assert!(response.is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_zoom_challenge_normal_token_succeeds() {
|
||||||
|
// A legitimate Zoom validation token is a short random alphanumeric string.
|
||||||
|
let payload = r#"{"event":"endpoint.url_validation","event_ts":1234567890,"payload":{"plainToken":"qgg8vlvZRS6UYooatFL8Aw"}}"#;
|
||||||
|
|
||||||
|
let handler = WebhookType::Zoom.get_webhook_handler().unwrap();
|
||||||
|
let config_data = SignatureConfigData { secret_key: "zoom_secret" };
|
||||||
|
let response = handler
|
||||||
|
.handle_challenge_request(&HeaderMap::new(), &config_data, payload)
|
||||||
|
.unwrap();
|
||||||
|
assert!(response.is_some());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_zoom_challenge_token_with_colons_rejected() {
|
||||||
|
// Exploit attempt: a plainToken crafted in the `v0:{ts}:{body}` signing format
|
||||||
|
// would let an attacker obtain a valid webhook signature for an arbitrary body.
|
||||||
|
let payload = r#"{"event":"endpoint.url_validation","event_ts":1234567890,"payload":{"plainToken":"v0:1234567890:{\"forged\":\"body\"}"}}"#;
|
||||||
|
|
||||||
|
let handler = WebhookType::Zoom.get_webhook_handler().unwrap();
|
||||||
|
let config_data = SignatureConfigData { secret_key: "zoom_secret" };
|
||||||
|
let result = handler.handle_challenge_request(&HeaderMap::new(), &config_data, payload);
|
||||||
|
assert!(matches!(
|
||||||
|
result,
|
||||||
|
Err(AuthenticationError::InvalidChallengeResponse(_))
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_zoom_challenge_token_too_long_rejected() {
|
||||||
|
// A plainToken exceeding 128 chars cannot be a legitimate Zoom validation token.
|
||||||
|
let long_token = "a".repeat(129);
|
||||||
|
let payload = format!(
|
||||||
|
r#"{{"event":"endpoint.url_validation","event_ts":1234567890,"payload":{{"plainToken":"{}"}}}}"#,
|
||||||
|
long_token
|
||||||
|
);
|
||||||
|
|
||||||
|
let handler = WebhookType::Zoom.get_webhook_handler().unwrap();
|
||||||
|
let config_data = SignatureConfigData { secret_key: "zoom_secret" };
|
||||||
|
let result = handler.handle_challenge_request(&HeaderMap::new(), &config_data, &payload);
|
||||||
|
assert!(matches!(
|
||||||
|
result,
|
||||||
|
Err(AuthenticationError::InvalidChallengeResponse(_))
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
// --- Custom webhook end-to-end ---
|
// --- Custom webhook end-to-end ---
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
Reference in New Issue
Block a user