# SCAN: LibreChat date: 2026-02-12 | program: LibreChat | repo: https://github.com/danny-avila/LibreChat | bounty: $500-$1500 ## summary raw_findings: ~10 (trufflehog) + manual review | real: 2 | high_conf: 1 | med_conf: 1 | low_conf: 0 | false_pos: ~10 Tools: manual code review (semgrep blocked by sandbox), trufflehog (all false positives - test fixtures/examples) ### Security posture notes - express-mongo-sanitize globally applied (line 102, api/server/index.js) - TOCTOU-safe SSRF protection via DNS-level blocking at connect time (packages/api/src/auth/agent.ts) - Path traversal guards with `path.relative()` checks in file operations (api/server/services/Files/Local/crud.js) - Zod validation on auth forms (api/strategies/validators.js) - Proper JWT verification on refresh tokens, session validation - Permission system with ACL for agents, files, MCP servers --- ## findings ### F1: IDOR via userId Override in User Key Update Route severity: high | confidence: high | type: IDOR / Broken Access Control | cwe: CWE-639 file: /Users/sebas/Code/bug-bounty/data/repos/LibreChat/api/server/routes/keys.js:8 | tool: manual review ```javascript router.put('/', requireJwtAuth, async (req, res) => { await updateUserKey({ userId: req.user.id, ...req.body }); res.status(201).send(); }); ``` analysis: The spread operator `...req.body` is applied AFTER `userId: req.user.id`, allowing an attacker to override the `userId` field by including it in the request body. In JavaScript, `{ userId: "realUser", ...{ userId: "victimUser" } }` results in `{ userId: "victimUser" }`. The `updateUserKey` function (packages/data-schemas/src/methods/key.ts:99-125) performs a `findOneAndUpdate` with `upsert: true` using the attacker-controlled `userId`, allowing write access to any user's API key store. While `express-mongo-sanitize` prevents NoSQL operator injection, a plain string userId override is NOT blocked. attack_vector: ```http PUT /api/keys HTTP/1.1 Authorization: Bearer Content-Type: application/json { "name": "openAI", "value": "attacker-controlled-key", "userId": "" } ``` impact: Attacker can overwrite any user's stored API keys (OpenAI, Azure, etc.), causing: 1. Denial of service (victim's API calls fail with wrong key) 2. Potential token harvesting if attacker sets their own key and monitors usage 3. Write access to arbitrary user's key store recommendation: REPORT --- ### F2: Password Reset Token Leaked in HTTP Response When Email Not Configured severity: medium | confidence: medium | type: Information Disclosure / Account Takeover | cwe: CWE-640 file: /Users/sebas/Code/bug-bounty/data/repos/LibreChat/api/server/services/AuthService.js:305-309 | tool: manual review ```javascript if (emailEnabled) { // ... sends email } else { logger.info( `[requestPasswordReset] Link issued. [Email: ${email}] [ID: ${user._id}] [IP: ${req.ip}]`, ); return { link }; // <-- Token leaked in HTTP response } ``` analysis: When email is not configured (no SMTP/Mailgun env vars), the `requestPasswordReset` function returns the full password reset link (including token and userId) directly in the HTTP response body. The `link` variable contains `${domains.client}/reset-password?token=${resetToken}&userId=${user._id}`. An attacker who knows a target's email can request a password reset and receive the token in the response, then use it to reset the victim's password. This affects self-hosted instances without email configured, which is common. The endpoint is rate-limited (`resetPasswordLimiter`) but not IP-banned. attack_vector: ```http POST /api/auth/requestPasswordReset HTTP/1.1 Content-Type: application/json {"email": "victim@example.com"} Response: {"link": "https://chat.example.com/reset-password?token=abc123...&userId=507f1f77..."} ``` impact: Full account takeover on instances without email configuration. Attacker can reset any user's password including admin accounts. recommendation: INVESTIGATE - This may be intentional "development mode" behavior. Check if huntr considers self-hosted default configs in scope. High duplicate risk as this pattern is documented/known. --- ## skipped | file:line | rule/pattern | reason | |-----------|-------------|--------| | api/server/routes/messages.js:52 | cursor value in MongoDB query | express-mongo-sanitize strips $ operators; cursor wrapped in {$gt/$lt} already | | api/server/routes/messages.js:43-47 | conversationId/messageId from query in MongoDB | mongoSanitize active + user field always included in filter | | api/server/routes/convos.js:34 | search param in getConvosByCursor | Passed to MeiliSearch, not raw MongoDB; user filter applied | | api/server/routes/share.js:58 | search param decoded and passed to getSharedLinks | Passed to MeiliSearch with user filter | | api/server/routes/files/files.js:109 | $in operator with user-controlled file_ids | file_ids validated by UUID regex; user ownership checked | | api/server/routes/mcp.js:309 | Unauthenticated OAuth status endpoint | Only returns status/boolean fields, no tokens; flowIds are random | | api/server/controllers/TwoFactorController.js:142 | regenerateBackupCodes no 2FA verification | Requires valid JWT auth; lower severity - attacker needs existing session | | api/server/index.js:174-176 | lang cookie in HTML response | Double quotes escaped; value in lang attribute; no XSS vector | | trufflehog: all findings | GCP/URI/Postgres/Box/Fastly/FixerIO | All in test files, example configs, bun.lock hashes - no real secrets | | api/server/services/Files/Local/crud.js | Path traversal in file operations | Protected by path.relative() checks on lines 249, 341, 359 | | api/server/middleware/validateImageRequest.js | Image path traversal | Protected by regex pattern matching and null byte check | | api/server/routes/files/multer.js | Filename sanitization | Uses sanitizeFilename() from @librechat/api |