From f5fa814fa4bc37ab8dfa9d8f496d14574fc28a81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Helleu?= Date: Sat, 30 May 2026 20:49:55 +0200 Subject: [PATCH] core: fix timing attack on TOTP validation (GHSA-vhv8-g2r9-cwcc) weecrypto_totp_validate compared the generated and client-supplied OTPs with strcmp and broke out of the time-window loop on the first match. Both choices leaked information via response timing: strcmp leaked the expected OTP digit-by-digit (shrinking the brute-force search from ~10^digits to a handful of guesses within the 30-second window), and the early break leaked which window offset matched. Compare in constant time with string_memcmp_constant_time and always iterate the full window, OR-ing the result into otp_ok without an early exit. This affects both relay protocols (which call totp_validate via the public info hook) and any other caller of the info hook. --- CHANGELOG.md | 1 + src/core/core-crypto.c | 11 +++++++---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dda648651..2f72b2083 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ ### Fixed - relay: fix timing attack on password authentication ([GHSA-vhv8-g2r9-cwcc](https://github.com/weechat/weechat/security/advisories/GHSA-vhv8-g2r9-cwcc)) +- api, relay: fix timing attack on TOTP validation ([GHSA-vhv8-g2r9-cwcc](https://github.com/weechat/weechat/security/advisories/GHSA-vhv8-g2r9-cwcc)) - relay: limit size of decompressed websocket frame with permessage-deflate to prevent memory exhaustion ([GHSA-v2v4-45wm-5cr3](https://github.com/weechat/weechat/security/advisories/GHSA-v2v4-45wm-5cr3)) - relay/weechat: fix empty buffers in client when WeeChat is running on Solaris/illumos - build: fix build on Solaris/illumos (issue #2251) diff --git a/src/core/core-crypto.c b/src/core/core-crypto.c index 8c34d1fb7..a81664014 100644 --- a/src/core/core-crypto.c +++ b/src/core/core-crypto.c @@ -662,15 +662,18 @@ weecrypto_totp_validate (const char *secret_base32, time_t totp_time, otp_ok = 0; + /* + * Compare in constant time and never break early: a non-constant + * compare and an early exit on match would let an observer measure + * how many digits of the expected OTP they got right and which + * time-window offset matched. + */ for (i = moving_factor - window; i <= moving_factor + window; i++) { rc = weecrypto_totp_generate_internal (secret, length_secret, i, digits, str_otp); - if (rc && (strcmp (str_otp, otp) == 0)) - { + if (rc && (string_memcmp_constant_time (str_otp, otp, digits) == 0)) otp_ok = 1; - break; - } } free (secret);