mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-20 00:02:19 +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:
@@ -337,6 +337,20 @@ mod zoom {
|
||||
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(
|
||||
HmacAlgorithm::Sha256,
|
||||
&signature_config_data.secret_key,
|
||||
@@ -1540,6 +1554,52 @@ mod tests {
|
||||
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 ---
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user