CVE-2026-72917 Overview
AnythingLLM contains an authentication bypass vulnerability in its unauthenticated account-recovery flow. The flaw affects versions 1.0.0 through 1.15.0 and resides in server/utils/PasswordRecovery/index.js. The recoverAccount() function deduplicates raw recoveryCodes values before trimming whitespace. An attacker who knows a target username and a single valid recovery code can submit that code twice with different surrounding whitespace to satisfy the two-code check. The same normalized value also matches the same stored bcrypt hash instead of consuming a distinct hash. This enables account takeover, including administrator accounts, through the POST /api/system/recover-account and POST /api/system/reset-password endpoints in multi-user mode.
Critical Impact
An attacker with knowledge of one recovery code and a username can take over any account, including administrator accounts, without prior authentication.
Affected Products
- AnythingLLM versions 1.0.0 through 1.15.0
- Deployments running in multi-user mode
- Self-hosted Mintplex-Labs AnythingLLM instances exposing the recovery API
Discovery Timeline
- 2026-08-10 - CVE-2026-72917 published to NVD
- 2026-08-12 - Last updated in NVD database
Technical Details for CVE-2026-72917
Vulnerability Analysis
AnythingLLM requires users to submit two distinct recovery codes to reset a forgotten password. The two-code requirement is designed to raise the bar for unauthenticated recovery. The original implementation constructed a Set from the raw input array first, then trimmed and validated each element. This ordering allowed "abc-uuid" and " abc-uuid " to appear as two distinct Set entries because Set uniqueness compared the untrimmed strings. Both entries reduced to the same UUID after trimming and both matched the same stored bcrypt hash. The recovery flow then issued a password-reset token, which an attacker submitted to POST /api/system/reset-password to complete account takeover. The weakness is classified as [CWE-180] Incorrect Behavior Order: Validate Before Canonicalize.
Root Cause
The root cause is an ordering error in input canonicalization. Deduplication ran against raw, whitespace-padded strings before trimming and validation. A single valid UUID recovery code could therefore satisfy both slots of the two-code check. A second logic error compounded the flaw: the hash-matching loop did not remove a matched hash from the candidate pool, so the same stored hash could authorize both submitted codes.
Attack Vector
The attack requires network access to the AnythingLLM API and prior knowledge of a target username plus one valid recovery code. An attacker sends POST /api/system/recover-account with a JSON body containing the username and an array such as ["<code>", " <code> "]. The server responds with a password-reset token. The attacker then calls POST /api/system/reset-password with the token and a chosen password. Multi-user mode must be enabled for the recovery endpoints to be reachable.
if (allUserHashes.length < 4)
return { success: false, error: "Invalid recovery codes." };
- // If they tried to send more than two unique codes, we only take the first two
- const uniqueRecoveryCodes = [...new Set(recoveryCodes)]
- .map((code) => code.trim())
- .filter((code) => validate(code)) // we know that any provided code must be a uuid v4.
- .slice(0, 2);
+ const uniqueRecoveryCodes = [
+ ...new Set(
+ recoveryCodes
+ .map((code) => (typeof code === "string" ? code.trim() : ""))
+ .filter((code) => validate(code))
+ ),
+ ].slice(0, 2);
if (uniqueRecoveryCodes.length !== 2)
return { success: false, error: "Invalid recovery codes." };
+ const unmatchedHashes = [...allUserHashes];
const validCodes = uniqueRecoveryCodes.every((code) => {
- let valid = false;
- allUserHashes.forEach((hash) => {
- if (bcrypt.compareSync(code, hash)) valid = true;
- });
- return valid;
+ const index = unmatchedHashes.findIndex((hash) =>
+ bcrypt.compareSync(code, hash)
+ );
+ if (index === -1) return false;
+ unmatchedHashes.splice(index, 1);
Source: GitHub Commit 61766d0. The patch trims and validates recovery codes before deduplication and tracks matched hashes in unmatchedHashes to ensure each code consumes a distinct stored hash.
Detection Methods for CVE-2026-72917
Indicators of Compromise
- Requests to POST /api/system/recover-account containing recoveryCodes array entries that differ only in leading or trailing whitespace.
- Successful POST /api/system/reset-password calls that follow a recovery request within a short time window for privileged accounts.
- Unexpected password changes on administrator accounts followed by new session activity from unfamiliar IP addresses.
Detection Strategies
- Parse application access logs and flag recoverAccount request bodies where trimmed UUIDs across the recoveryCodes array collide.
- Alert on any successful password reset for accounts flagged with the admin role in the AnythingLLM database.
- Correlate reset-password events with subsequent workspace configuration or user management API calls that indicate takeover activity.
Monitoring Recommendations
- Enable verbose logging on the AnythingLLM server and forward events to a centralized log platform for retention and correlation.
- Monitor authentication and recovery endpoints for anomalous request rates, particularly from single source IP addresses probing multiple usernames.
- Track administrator login history and password change audit records for out-of-band modifications.
How to Mitigate CVE-2026-72917
Immediate Actions Required
- Upgrade AnythingLLM to a version above 1.15.0 that includes commit 61766d0.
- Rotate all existing recovery codes for every user, prioritizing administrator accounts.
- Force a password reset for accounts whose recovery codes may have been exposed through prior phishing, shared documents, or backup leaks.
- Restrict network exposure of the AnythingLLM API to trusted networks or place it behind an authenticating reverse proxy.
Patch Information
The fix is published in commit 61766d06b77b903f66dc4afd8dffb3a39012db14 and documented in GHSA-vv8w-wg6r-hq56. The patch trims and validates each recovery code before applying Set deduplication and consumes each stored bcrypt hash exactly once during verification.
Workarounds
- Disable multi-user mode until the patched version is deployed, since the recovery endpoints only operate in that mode.
- Block POST /api/system/recover-account at a reverse proxy or web application firewall when self-service recovery is not required.
- Add a WAF rule that rejects recoverAccount request bodies where trimmed recoveryCodes array entries are not unique.
# Example nginx rule to block the recovery endpoint
location = /api/system/recover-account {
return 403;
}
Disclaimer: This content was generated using AI. While we strive for accuracy, please verify critical information with official sources.

