← Todos los advisories
high

Online Brute Force of Bitwarden Send Email-OTP Access Gates

Advisory ID
HKV-ADV-2026-046
Tracking ID
HKV-2026-046
Fabricante
Bitwarden
Publicado

Summary

Bitwarden Sends can be gated so that only a recipient of an emailed 6-digit one-time code may open the shared link. On self-hosted installations that code is the only barrier between a link holder and the Send's plaintext content — and the redemption path applies no attempt counter, no per-attempt delay, and, because the generic IP rate limiter is only registered for cloud deployments, no rate limiting at all. The 5-minute code window can additionally be renewed on demand by the attacker. A holder of a leaked link can therefore exhaust the 1,000,000-code keyspace online in minutes and decrypt the Send with the key carried in the link itself.

Affected Products

Affected Scope

Product
Bitwarden Server — self-hosted deployments.
Component
Identity service — anonymous `send_access` extension grant; email-OTP access gate for Sends.
Affected function
`SendEmailOtpRequestValidator.ValidateAsync` (OTP redemption path of the `send_access` grant); supporting: `OtpTokenProvider.ValidateTokenAsync` (code generation, validation, cache deletion) and the Identity `Startup` rate-limiter registration.
Affected versions
Verified against `ghcr.io/bitwarden/lite:2026.9.0` and source HEAD `8b5fea2e0` (2026-09-21). All releases since the `send_access` email-OTP gate was introduced (2024–2025) are presumed affected. No fixed release exists as of 2026-11-06.
Safe version
None — unpatched upstream as of publication. Bitwarden Cloud (SaaS) behaves as practically mitigated by its per-IP rate limiter (~50 guesses per 5-minute window), but this is a deployment property, not a code fix.
CWE
CWE-307: Improper Restriction of Excessive Authentication Attempts (primary); CWE-645 (secondary, per the internal audit's classification).

Technical Summary

Bitwarden Sends can be gated so that only a recipient of an emailed 6-digit one-time code may open the shared link. On self-hosted installations that code is the only barrier between a link holder and the Send's plaintext content — and the redemption path applies no attempt counter, no per-attempt delay, and, because the generic IP rate limiter is only registered for cloud deployments, no rate limiting at all. The 5-minute code window can additionally be renewed on demand by the attacker. A holder of a leaked link can therefore exhaust the 1,000,000-code keyspace online in minutes and decrypt the Send with the key carried in the link itself.

Prerequisites and Permissions

No account, authentication, mailbox access, or user interaction is required. The attacker needs exactly three things: (1) the Send's access URL, which yields send_id and the sendKey decryption key — the link is normally distributed as a bearer secret; (2) one email address on the Send's recipient allow list, such as the attacker's own address as a former recipient; (3) network reachability to the self-hosted instance. Enabling email-OTP gating is a sender-side premium feature; the attacker plays no part in that configuration.

Affected Locations

Reproducible Proof

Run only against a Bitwarden instance you operate, in an isolated lab, at modest request rates, and stop at the first accepted code.

  1. From the Send link …/#send/{accessId}/{sendKey}, note both fragments.
  2. Trigger code generation — POST /identity/connect/token, form-encoded, without otp:

`` curl -X POST https://SELFHOST/identity/connect/token \ -d "grant_type=send_access&client_id=send&scope=api.send.access&send_id=ACCESSID&email=RECIPIENT" ``

The response is a 400 of type email_and_otp_required, and the recipient receives the emailed code.

  1. Redeem guesses by repeating the request with &otp=NNNNNN added. Wrong guesses return 400 immediately — no delay, no failure counter, no 429 on a stock self-hosted install. On 200, the body carries the access_token.
  2. Fetch the Send with that token — POST /api/sends/access/ with Authorization: Bearer … — and decrypt the returned type-2 EncString (2.{iv}|{ct}|{mac}, AES-256-CBC + HMAC-SHA256) with the 64-byte sendKey from the link fragment.

Measured on a stock, unmodified ghcr.io/bitwarden/lite:2026.9.0 instance: a sweep reached the correct code (601006) after 603,684 guesses in 234 seconds (≈2,580 guesses/s, zero 429 responses, one OTP window), retrieved the Send (HTTP 200), and decrypted it to a known plaintext marker — full chain confirmed. At that rate the full 10⁶ keyspace takes ≈6.5 minutes; because a request without otp regenerates the code and resets the 5-minute window, the sweep is not deadline-bound even at lower rates.

Vulnerable Code

csharpsource excerpt
class="tok-comment">// src/Identity/IdentityServer/RequestValidators/SendAccess/SendEmailOtpRequestValidator.cs
var otpResult = await otpTokenProvider.ValidateTokenAsync(
    requestOtp, SendAccessConstants.OtpToken.TokenProviderName,
    SendAccessConstants.OtpToken.Purpose, uniqueIdentifierForTokenCache);
if (!otpResult)
{
    return BuildErrorResult();   class="tok-comment">// no delay, no counter, no lockout
}

class="tok-comment">// src/Identity/Startup.cs:190-195 — the only generic throttle is not installed on self-host:
if (!globalSettings.SelfHosted)
{
    class="tok-comment">// Rate limiting
    app.UseMiddleware<CustomIpRateLimitMiddleware>();
}

Severity

high

CVSS

7.5

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N

EPSS

0.00

Technical Analysis

A Bitwarden Send is an encrypted text or file blob shared via https://{vault}/#/send/{accessId}/{sendKey}. The sendKey URL fragment is the decryption key (64 bytes: AES-256 key + HMAC key), so anyone holding the link can decrypt the content once they can fetch it. For Sends with AuthType = Email, the server additionally requires an emailed 6-digit OTP before issuing an anonymous send_access token: a token request without otp generates and emails the code; the same request with otp returns the access token; the token then retrieves the ciphertext from POST /api/sends/access/. The OTP exists precisely for the threat model of a leaked link — and that is the adversary who defeats it.

Three controls that normally prevent online OTP guessing are all absent on this path. First, no attempt counter: the OTP validator deletes the cached code only on success and never records failures (OtpTokenProvider). Second, no per-attempt delay: every login 2FA failure in the same service pays Task.Delay(2000) plus a notification email (BaseRequestValidator.cs:504,536), and a failure counter was even scaffolded (user.FailedLoginCount) but is never read for lockout anywhere — dead code; the Send OTP validator returns an error immediately instead. Third, no rate limiting: CustomIpRateLimitMiddleware is registered only when !globalSettings.SelfHosted (src/Identity/Startup.cs:190-195), so a stock self-hosted install has no limiter at all. On top of this, the 5-minute validity window is renewable at will: any token request without otp regenerates the code, re-emails it, and resets the TTL, so the sweep faces no deadline even at modest rates.

We confirmed the full chain live against a stock, unmodified ghcr.io/bitwarden/lite:2026.9.0 instance: a sweep reached the correct code (601006) after 603,684 guesses in 234 seconds — ≈2,580 guesses/s sustained from a single machine, zero HTTP 429 responses, unthrottled latency, within a single OTP window. The send_access token retrieved the Send (HTTP 200) and it decrypted correctly with the sendKey from the link. At that rate the entire 10⁶ keyspace takes ≈6.5 minutes. The scope is one Send: the token is an anonymous grant bound to exactly one send_id and carries no user or vault access (Bitwarden is zero-knowledge; no key material transits this flow). Content disclosure is conditional on holding the link, since only the sendKey fragment decrypts it — in the leaked-link threat model the disclosure is total.

Disclosure outcome: the finding was reported to Bitwarden through the designated security channel on 24th of September, 2026. Bitwarden's response required the reporter to submit the report through Bitwarden's HackerOne program. Bitwarden's HackerOne response policy lists "rate limiting or brute-force issues" among its out-of-scope bug classes, so a submission would be closed as out of scope and the reporter chose not to file a report destined for that outcome. As of 2026-11-06 none of the missing controls has changed upstream, no patch or vendor advisory exists, and the realistic outlook is that this issue will not be addressed; this advisory is published for the self-hosted operators who own the residual risk.

Impact

Remediation

Vendor-side (recommended upstream, but not expected to ship — see Technical Analysis): a per-(sendId, email) failure counter with lockout after N failures; a constant failure delay mirroring the login-2FA Task.Delay(2000); registration of the rate limiter on self-hosted deployments (remove the !SelfHosted gate in src/Identity/Startup.cs); caps on OTP regeneration per (Send, email, IP); telemetry and alerting on send_access failure bursts per send_id.

Operator mitigations to apply now (no server patching required):

Vendor advisory: none. Bitwarden directed the report to its HackerOne program, where "rate limiting or brute-force issues" are out of scope; no vendor advisory will be issued.

Timeline

References

Credits

Reported by David Haintz, Haikhavan.