mitigate sidechannel timing attack for basic auth

Information on whether a user with a given handle exists or not could be
collected by checking the difference between response times of
auth-required endpoints with and without a real handle being passed into
Basic auth. This is because the time-expensive password hash would only
be computed for users that exist, lengthening the response time. In
local testing, this was a difference of 8ms vs. 35-60ms.

A hash is now computed even if a user with the requested handle doesn't
exist, mitigating the issue and leaving only negligible differences
inbetween all response times, from which no information can be obtained.
This commit is contained in:
2026-02-24 14:49:30 +01:00
parent f6feec2469
commit 5a92740785
2 changed files with 19 additions and 3 deletions

View File

@@ -15,8 +15,8 @@ use crate::{
users::{ users::{
User, User,
auth::{ auth::{
AuthError, COOKIE_NAME, TokenSize, UserAuthRequired, UserAuthenticate, AuthError, COOKIE_NAME, TokenSize, UserAuthDummyData, UserAuthRequired,
UserPasswordHashing, UserAuthenticate, UserPasswordHashing,
}, },
sessions::Session, sessions::Session,
}, },
@@ -81,6 +81,15 @@ impl UserPasswordHashing for User {
} }
} }
// TODO: generate these at startup using predefined Argon2 params if
// these ever change from ::Default - the PHC must have the same factors as real hashes.
impl UserAuthDummyData for User {
/// This PHC generated for b"password"
const DUMMY_PASSWORD_PHC: &str = "$argon2id$v=19$m=19456,t=2,p=1$PXcTKpFhLRB70fVF35XYDQ$QOW2IxdPUvqD38+ScqX5SgO+jwweaMO9DUGqmkTeofQ";
/// Different than the input password of the PHC
const DUMMY_PASSWORD: &str = "different_password";
}
impl From<argon2::password_hash::Error> for AuthError { impl From<argon2::password_hash::Error> for AuthError {
fn from(err: argon2::password_hash::Error) -> Self { fn from(err: argon2::password_hash::Error) -> Self {
AuthError::PassHashError(err) AuthError::PassHashError(err)
@@ -175,7 +184,10 @@ fn authenticate_basic(credentials: &str) -> Result<Option<User>, AuthError> {
true => Ok(Some(User::get_by_id(id)?)), true => Ok(Some(User::get_by_id(id)?)),
false => Err(AuthError::InvalidCredentials), false => Err(AuthError::InvalidCredentials),
}, },
_ => Err(AuthError::InvalidCredentials), _ => {
let _ = User::match_hash_password(User::DUMMY_PASSWORD, User::DUMMY_PASSWORD_PHC)?;
Err(AuthError::InvalidCredentials)
}
} }
} }

View File

@@ -20,6 +20,10 @@ pub trait UserPasswordHashing {
/// Returns whether the password matches the hash /// Returns whether the password matches the hash
fn match_hash_password(passw: &str, hash: &str) -> Result<bool, argon2::password_hash::Error>; fn match_hash_password(passw: &str, hash: &str) -> Result<bool, argon2::password_hash::Error>;
} }
pub trait UserAuthDummyData {
const DUMMY_PASSWORD_PHC: &str;
const DUMMY_PASSWORD: &str;
}
#[derive(thiserror::Error, Debug)] #[derive(thiserror::Error, Debug)]
pub enum AuthError { pub enum AuthError {