This is a valet who can swap someone else’s car key tag, not steal the whole garage
CVE-2026-31942 is an authenticated IDOR/mass-assignment bug in LibreChat’s PUT /api/keys endpoint. In affected builds, the route passes userId: req.user.id and then spreads ...req.body, so an attacker-supplied userId overrides the authenticated user and lets any logged-in user overwrite another user’s stored provider key data. The GitHub advisory lists affected versions as <= 0.7.6, with a fix in 0.8.3-rc1 published on 2026-06-02.
Vendor HIGH is technically defensible in a lab because this is cross-user integrity failure over the network with low privileges. In real enterprise deployments it lands lower: the attacker must already have an account, must know or obtain a victim userId, and only gets leverage where LibreChat is actually using per-user stored API keys; this is not pre-auth, not host compromise, and not a clean data-exfil primitive on its own.
4 steps from start to impact.
Land a normal LibreChat user session
curl, or Burp Suite is enough because the vulnerable route sits behind standard JWT auth. This immediately makes the bug post-initial-access rather than perimeter-breaking.- Valid LibreChat user account
- Reachability to the application over the network
- Requires prior access through SSO/local auth/invite flow
- MFA, IdP hygiene, and tenant onboarding controls can stop this before the vuln matters
Obtain a victim userId
- Knowledge of a valid victim
userId
- No native user enumeration is described in the advisory
- If user IDs are not exposed elsewhere, this becomes the biggest operational hurdle
Overwrite the victim’s stored provider key with curl or Burp Repeater
PUT /api/keys with a body such as { "userId": "<victim>", "name": "openAI", "value": "attacker-key" }. Because the route spreads req.body after setting the server-side user ID, JavaScript object precedence lets the attacker-controlled userId win and the backend updates the victim record.- Authenticated session
- Valid victim
userId - Target is running vulnerable code path
- Only impacts stored key records, not broader account ownership
- If the deployment uses server-side shared provider credentials instead of user-stored keys, exploit value drops hard
PUT /api/keys volume, but generic web vul scanners usually do not model this overwrite.Wait for downstream misuse or disruption
- Victim continues using the affected provider
- LibreChat deployment stores and uses per-user provider keys
- No immediate shell, tenant admin, or database takeover
- Prompt visibility depends on how the external AI provider logs requests and what the victim actually uses
The supporting signals.
| In-the-wild status | No public exploitation evidence found in the sources reviewed as of 2026-06-03. This is an inference from the absence of KEV listing and lack of indexed campaign reporting. |
|---|---|
| KEV status | Not listed in CISA KEV as of 2026-06-03. |
| Public PoC | Yes, effectively trivial. The vendor advisory includes a complete attack scenario and the request shape is simple enough to reproduce with curl or Burp; no exploit framework is needed. |
| EPSS | No public EPSS value was found in indexed sources on 2026-06-03. Treat that as unavailable, not as proof of low risk. |
| Vendor CVSS | CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:L = authenticated network attack, easy to execute once inside, strong integrity impact, limited availability impact. |
| Affected versions | GitHub advisory says <= 0.7.6. |
| Fixed version | Patched in 0.8.3-rc1 per the GitHub advisory. |
| Exposure reality | LibreChat is popular open source software with ~38k GitHub stars and easy Docker deployment, but this bug is only reachable by authenticated users and only matters where user-stored API keys are in play. |
| Scanning / internet exposure | No product-specific GreyNoise/Censys/Shodan evidence was found in the reviewed sources for this CVE. Even if a LibreChat login page is internet-exposed, the vuln remains post-auth business logic. |
| Disclosure / credit | Disclosed 2026-06-02 via GitHub Security Advisory GHSA-5jcj-rh68-cgj7. The advisory page reviewed does not list a public researcher credit. |
noisgate verdict.
The decisive friction is attacker position: this bug requires an already-authenticated LibreChat user and a valid victim userId, which makes it a post-initial-access abuse path rather than a perimeter event. The blast radius is further narrowed because the exploit modifies stored provider-key state; it does not directly hand over host control, tenant admin, or guaranteed message disclosure.
Why this verdict
- Down from vendor HIGH: requires authenticated remote access, which implies the attacker already cleared your identity layer and is not exploiting the internet-facing edge cold.
- Further down: the attacker also needs a victim
userId; the advisory shows overwrite logic, not a built-in enumeration primitive, so the exploit chain has a real dependency. - Impact is narrower than raw CVSS suggests: this tampers with stored provider keys and can disrupt or potentially observe future provider traffic, but it is not server RCE, database dump, or tenant-admin takeover.
- Population is narrower: only deployments that let users store their own provider/API keys get the full impact. Environments using centrally managed server-side keys reduce the practical value of the bug.
- Still not low: this is cross-user authorization failure in a multi-user app, and once the prerequisites are met the overwrite itself is easy and reliable.
Why not higher?
This is not pre-auth and not broadly wormable. It does not provide direct code execution, direct read access to all chats, or immediate control of the LibreChat host; exploitation value depends on a second condition that many shops will not meet, namely active use of per-user stored provider keys.
Why not lower?
The authorization failure is real, cross-user, and easy to execute once prerequisites are satisfied. In shared internal AI portals, letting one ordinary user silently replace another user’s API key is a serious integrity break with plausible downstream confidentiality consequences.
What to do — in priority order.
- Disable or avoid user-stored provider keys where possible — If your deployment can use centrally managed service credentials instead of
user_providedkeys, do that; it removes most of the exploit value because there is no per-user key record worth hijacking. For a MEDIUM verdict there is no mitigation SLA — go straight to the 365-day remediation window, but this is worth doing earlier if your tenant model is shared. - Audit and alert on
PUT /api/keys— Log request metadata, actor, target record, provider name, and old/new key events so cross-user key churn becomes visible. There is no mitigation SLA for this severity, but deploy the telemetry on your normal change cadence so you are not blind while you wait for remediation. - Reduce low-privilege account sprawl — This vuln only exists after login, so tightening invites, dormant accounts, SSO groups, and MFA materially lowers exposure. Again, no mitigation SLA — go straight to the 365-day remediation window unless this is an externally accessible multi-user portal.
- Review where user IDs are exposed — The exploit is much easier if
userIdvalues leak through logs, exports, browser responses, or admin tooling. Minimize that exposure and sanitize support artifacts to preserve the main friction point that keeps this bug from being a higher-priority fire.
- A WAF alone will not reliably stop this; the malicious request looks like a normal authenticated API call and the flaw is object-level authorization logic.
- Network segmentation does little if users already reach the LibreChat app; the abuse happens over the legitimate application path.
- Rotating provider keys globally is not a fix; the attacker can simply overwrite the victim record again until the application logic is corrected.
Crowdsourced verification payload.
Run this on the LibreChat host, inside the LibreChat container, or in CI against a checked-out source tree. Invoke it with python3 check_cve_2026_31942.py /app (Docker default example) or python3 check_cve_2026_31942.py /path/to/LibreChat; it only needs read access to the application files.
#!/usr/bin/env python3
# CVE-2026-31942 verification helper for LibreChat
# Exit codes:
# 0 = PATCHED
# 1 = VULNERABLE
# 2 = UNKNOWN / unable to determine
import json
import os
import re
import sys
from pathlib import Path
VULN_ROUTE = Path('api/server/routes/keys.js')
PACKAGE_JSON = Path('package.json')
def parse_semver(v):
# Supports forms like 0.7.6, v0.7.6, 0.8.3-rc1
v = v.strip()
if v.startswith('v'):
v = v[1:]
m = re.match(r'^(\d+)\.(\d+)\.(\d+)(?:-([A-Za-z]+)(\d+)?)?$', v)
if not m:
return None
major, minor, patch, pre_label, pre_num = m.groups()
major, minor, patch = int(major), int(minor), int(patch)
if pre_label is None:
pre_rank = 1 # stable > prerelease
pre_label = ''
pre_num = 0
else:
pre_rank = 0
pre_num = int(pre_num) if pre_num else 0
return (major, minor, patch, pre_rank, pre_label, pre_num)
def cmp_ver(a, b):
return (a > b) - (a < b)
def read_version(root):
pkg = root / PACKAGE_JSON
if not pkg.exists():
return None
try:
data = json.loads(pkg.read_text(encoding='utf-8'))
return data.get('version')
except Exception:
return None
def inspect_route(root):
route = root / VULN_ROUTE
if not route.exists():
return None
try:
text = route.read_text(encoding='utf-8', errors='ignore')
except Exception:
return None
# Vulnerable pattern from advisory
if 'await updateUserKey({ userId: req.user.id, ...req.body });' in text:
return 'vulnerable'
# Likely fixed pattern from advisory
fixed_markers = [
'const { name, value, expiresAt } = req.body;',
'await updateUserKey({ userId: req.user.id, name, value, expiresAt });'
]
if all(marker in text for marker in fixed_markers):
return 'patched'
# Heuristic: if req.body is spread into updateUserKey after userId, flag as vulnerable
heuristic = re.search(r'updateUserKey\s*\(\s*\{[^}]*userId\s*:\s*req\.user\.id[^}]*\.\.\.req\.body[^}]*\}\s*\)', text, re.S)
if heuristic:
return 'vulnerable'
return 'unknown'
def main():
if len(sys.argv) != 2:
print('UNKNOWN - usage: python3 check_cve_2026_31942.py /path/to/LibreChat')
sys.exit(2)
root = Path(sys.argv[1]).resolve()
if not root.exists() or not root.is_dir():
print(f'UNKNOWN - path not found or not a directory: {root}')
sys.exit(2)
route_status = inspect_route(root)
version = read_version(root)
if route_status == 'vulnerable':
print(f'VULNERABLE - code pattern matches CVE-2026-31942 in {root / VULN_ROUTE}')
sys.exit(1)
if route_status == 'patched':
print(f'PATCHED - fixed code pattern found in {root / VULN_ROUTE}')
sys.exit(0)
if version:
parsed = parse_semver(version)
threshold = parse_semver('0.7.6')
if parsed and threshold:
if cmp_ver(parsed, threshold) <= 0:
print(f'VULNERABLE - package.json version {version} is <= 0.7.6 and code pattern was inconclusive')
sys.exit(1)
else:
print(f'PATCHED - package.json version {version} is > 0.7.6 and code pattern was inconclusive')
sys.exit(0)
print('UNKNOWN - could not confirm vulnerable route pattern or parse LibreChat version')
sys.exit(2)
if __name__ == '__main__':
main()
If you remember one thing.
Sources
- GitHub Security Advisory GHSA-5jcj-rh68-cgj7
- LibreChat security advisory index
- LibreChat docs: AI Endpoints (`user_provided` keys)
- LibreChat docs: Custom Endpoints
- LibreChat docs: Agents API and user-generated API keys
- LibreChat quick start / deployment options
- CISA Known Exploited Vulnerabilities Catalog
- FIRST EPSS FAQ
What defenders are saying.
Crowdsourced verification outputs.
Results submitted by users who ran the verification payload against their environment.