Prevent user enumeration via password hints

When `show_password_hint` is enabled but mail is not configured, the previous
implementation returned a differentiable response for non-existent email
addresses.

Even if mail is enabled, there is a timing side channel since mail is sent
synchronously. Add a randomized sleep to mitigate this somewhat.
This commit is contained in:
Jeremy Lin 2021-07-10 01:21:27 -07:00
parent 8ee5d51bd4
commit 88bea44dd8

View File

@ -576,24 +576,45 @@ struct PasswordHintData {
#[post("/accounts/password-hint", data = "<data>")] #[post("/accounts/password-hint", data = "<data>")]
fn password_hint(data: JsonUpcase<PasswordHintData>, conn: DbConn) -> EmptyResult { fn password_hint(data: JsonUpcase<PasswordHintData>, conn: DbConn) -> EmptyResult {
if !CONFIG.mail_enabled() && !CONFIG.show_password_hint() {
err!("This server is not configured to provide password hints.");
}
const NO_HINT: &str = "Sorry, you have no password hint...";
let data: PasswordHintData = data.into_inner().data; let data: PasswordHintData = data.into_inner().data;
let email = &data.Email;
let hint = match User::find_by_mail(&data.Email, &conn) { match User::find_by_mail(email, &conn) {
Some(user) => user.password_hint, None => {
None => return Ok(()), // To prevent user enumeration, act as if the user exists.
};
if CONFIG.mail_enabled() { if CONFIG.mail_enabled() {
mail::send_password_hint(&data.Email, hint)?; // There is still a timing side channel here in that the code
} else if CONFIG.show_password_hint() { // paths that send mail take noticeably longer than ones that
if let Some(hint) = hint { // don't. Add a randomized sleep to mitigate this somewhat.
err!(format!("Your password hint is: {}", &hint)); use rand::{thread_rng, Rng};
} else { let mut rng = thread_rng();
err!("Sorry, you have no password hint..."); let base = 1000;
} let delta: i32 = 100;
} let sleep_ms = (base + rng.gen_range(-delta..=delta)) as u64;
std::thread::sleep(std::time::Duration::from_millis(sleep_ms));
Ok(()) Ok(())
} else {
err!(NO_HINT);
}
}
Some(user) => {
let hint: Option<String> = user.password_hint;
if CONFIG.mail_enabled() {
mail::send_password_hint(email, hint)?;
Ok(())
} else if let Some(hint) = hint {
err!(format!("Your password hint is: {}", hint));
} else {
err!(NO_HINT);
}
}
}
} }
#[derive(Deserialize)] #[derive(Deserialize)]