fix(webdav): constant-time compare on lock-token equality checks
Replace plain `==` on lock tokens with `subtle::ConstantTimeEq` at every token-comparison site on the WebDAV surface. Closes a reported timing side-channel (2026-09-05) in `evaluate_if_header` where an authenticated attacker could theoretically recover another user's active lock token via response-latency measurements on the `If:` header state-token comparison. Practical exploitability is marginal — the signal is tens-of-ns buried under ms-scale network jitter, ~5×10⁸ samples needed per token to average through the noise vs a default lock lifetime of 60 s to 1 h — but the fix is a five-line change with zero measurable perf cost (`subtle` is already transitive via sqlx-postgres → digest, so no new binary weight), and adopting constant-time compare on any token that gates access matches the hygiene rule the rest of the codebase already follows on password and session paths. Sites fixed: * `evaluate_if_header` — first-pass state-token scan and second-pass condition eval in `webdav_handler.rs`. * `WebdavLockService::refresh` — `!= token` mismatch check. * `WebdavLockService::release` — `== token` guard on the by_path invalidation branch. The two `WebdavLockService` sites are already gated by `self.by_token.get(token)?` — the attacker cannot reach the comparison without already presenting a valid token, so their timing surface is nil in practice. Kept constant-time anyway for callsite consistency. Sweep confirmed no other secret-adjacent `==` in production code: password verification goes through Argon2's `verify_password`, session/CSRF/DPoP jti tokens are hashmap-gated, and blob-hash equality compares two server-side values with no attacker- controlled operand. Reported-by: Abdurazzoqov Javohir <abdurazzoqovjavohir700-dev@users.noreply.github.com> Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -19,8 +19,26 @@
|
||||
use std::sync::Arc;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use subtle::ConstantTimeEq;
|
||||
|
||||
use crate::application::adapters::webdav_adapter::{LockInfo, LockScope};
|
||||
|
||||
/// Constant-time equality for lock tokens. Same rationale as
|
||||
/// `webdav_handler::ct_str_eq` — see that helper's doc-comment.
|
||||
///
|
||||
/// The two callsites in this file (`refresh` at :169, `release`
|
||||
/// at :191) are already gated by `self.by_token.get(token)?`, so
|
||||
/// the attacker CANNOT reach these checks without already having
|
||||
/// presented a valid token — the practical timing-attack surface is
|
||||
/// nil. Kept constant-time for defense-in-depth consistency across
|
||||
/// every token comparison in the WebDAV surface, so a future
|
||||
/// auditor doesn't have to re-derive "this one is safe because…"
|
||||
/// for each individual callsite.
|
||||
#[inline]
|
||||
fn ct_str_eq(a: &str, b: &str) -> bool {
|
||||
a.len() == b.len() && a.as_bytes().ct_eq(b.as_bytes()).into()
|
||||
}
|
||||
|
||||
/// Default lock timeout when the client does not specify one (RFC 4918 §10.7).
|
||||
const DEFAULT_LOCK_TIMEOUT_SECS: u64 = 1800; // 30 minutes
|
||||
|
||||
@@ -166,7 +184,7 @@ impl WebDavLockStore {
|
||||
let path = self.by_token.get(token)?;
|
||||
let mut entry = self.by_path.get(&path)?;
|
||||
|
||||
if entry.info.token != token {
|
||||
if !ct_str_eq(&entry.info.token, token) {
|
||||
return None; // token mismatch — lock was replaced
|
||||
}
|
||||
|
||||
@@ -188,7 +206,7 @@ impl WebDavLockStore {
|
||||
if let Some(path) = self.by_token.get(token) {
|
||||
// Only remove from by_path if the token still matches
|
||||
if let Some(entry) = self.by_path.get(&path)
|
||||
&& entry.info.token == token
|
||||
&& ct_str_eq(&entry.info.token, token)
|
||||
{
|
||||
self.by_path.invalidate(&path);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user