mirror of
https://github.com/dani-garcia/vaultwarden.git
synced 2026-07-16 08:54:01 -05:00
[GH-ISSUE #7345] User enumeration possibility #35634
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Originally created by @endreszabo on GitHub (Jun 17, 2026).
Original GitHub issue: https://github.com/dani-garcia/vaultwarden/issues/7345
We have this hard coded duration range in the password hint code:
d6a3d539ed/src/api/core/accounts.rs (L1200-L1205)Now, the problem with this is that in my setup it is clearly visible which users exists and which do not, just based on the response time of the query.
I see a possible mitigation to this by letting the users control the range via configuration values so that users can specify ranges that closely match their otherwise usual mail sending delays.
Thank you for your attention to this matter.
@BlackDex commented on GitHub (Jun 17, 2026):
This is not really the place to report these kind of issues, but it's to late for that now.
Adjusting the timing is also not really useful, as an attacker might be able to ddos the mail server and then still see the difference.
@endreszabo commented on GitHub (Jun 18, 2026):
Thanks, noted.
That is not generally true, to put it mildly.
Anyway, I propose to return this script with Ok after a fixed amount of time regardless of existence of users AND regardless of the outcome of the mail sending procedure. I'm saying this because right now e-mail handling errors are simply passed to the user, like so:
I believe this is the worst possible scenario. This not only confirms (with zero delay) that the user exists, but also shows that there are email sending issues server side, also hinting the attacker that users would not be notified for any of their account-related events. By using fixed delay timer tell the attacker that all their requests are handled the same way and there's no extra server-side processing, no matter of their input.
With that said I also miss the use of any CSRF token or alike.
@BlackDex commented on GitHub (Jun 18, 2026):
We can't use CSRF tokens or alike as that isn't something supported by the Bitwarden clients, so that is not possible.
Regarding this specific password hints, i think that this can be solved in a totally different way.
First, put a rate limit on it, second instead of sending the email in-line we could spawn that function inside it's own thread and just do not care about the response from the mail function, someone just needs to request the hint again. Or we start using an MPSC flow which can trigger this for some specific flows if needed. Not sure which option would be the best one right now and which is the fastest. Another option could be to always write to the database in a special table which then another function in the background checks every x seconds or so and sends those hints, with this option there might still be a database timing issue, but that will probably be much less noticeable then smtp.
@endreszabo commented on GitHub (Jun 18, 2026):
Yeah, totally get that part.
Exactly what I wanted to propose.
MPSC sounds like it might scale better, bth not sure if it worth tbh. If something, up to this day the former process seems to have been just fine for most.
I don't find this kind of queueing efficient, but that's just my personal opinion.
@tessus commented on GitHub (Jun 18, 2026):
I hope that I am not out of line here (stating maybe the obvious), but I implore not to pass any unfiltered (security related) errors to the user.
I don't know how many similar cases are present in the code. Maybe someone with an AI subscription can find out and compile a list.
@maximilize commented on GitHub (Jun 23, 2026):
Audit of similar mail-path enumeration/timing leaks, per @tessus's request. All refs against
main(d6a3d53).The
password-hintrandomized-sleep mitigation (src/api/core/accounts.rs:1197-1204) exists in exactly one other place —register/send-verification-email(src/api/identity.rs:1062-1073). Everywhere else the "user exists → send mail inline" pattern runs without the sleep and/or leaks the SMTP error, so the same enumeration is reproducible elsewhere:src/api/core/accounts.rs:1109-1127(/accounts/delete-recover) — mail is sent inline only when the user exists; the error is correctly swallowed (error!+Ok(())), but there is no compensating sleep, so the existing-vs-missing timing delta is identical to the original report. Highest-value match.src/api/core/accounts.rs:1312-1319(/devices/knowndevice) — unauthenticated; returnstrue/falsedirectly fromfind_by_mail(...).is_some()+ device match, so atrueconfirms the account exists.src/api/core/public.rs:127-138(org LDAP/ldap_import) —err!("Error sending invite: {e:?}")leaks the raw SMTP error to the caller, same class as the original report (authenticated org endpoint, so lower severity).For contrast, the register paths (
accounts.rs:254-292) andtwo_factor/email.rs:65-66already use a single shared message, so those are response-shape safe.On the fix direction discussed above: the inline
mail::send_*().await?is the common root cause for both the timing delta and the error leak. Decoupling send from the request (spawn / MPSC, as proposed) fixes timing and error-exposure for all of these at once, whereas per-endpoint sleeps don't. None of theaccounts.rsendpoints are rate-limited today (check_limit_loginis only wired intoidentity.rs:359), so the rate-limit half would be net-new forpassword-hint/delete-recover.One question so the scope is clear: should the decoupled-mail + rate-limit fix target all of the above in one pass, or just
password-hintfirst with the rest tracked separately?(I also spotted a couple of non-mail, login-path timing/response oracles; since those aren't in this thread yet, I'll send them to the security contact in SECURITY.md rather than detail them here.)