mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-07-11 16:53:45 +00:00
When an admin configures a custom endpoint with `baseURL: 'user_provided'`, each end user supplies their own base URL via the UI. Without validation, a user can point that URL at internal services (cloud metadata endpoints, intranet APIs, etc.), causing the backend to issue requests to those addresses on every model-listing fetch. Add `interface.blockPrivateUserBaseURL` (default `false` to preserve current behavior). When enabled, the `/api/keys` PUT handler parses the encrypted user-key payload and rejects values whose `baseURL` fails an http(s) protocol check or resolves to a private, loopback, or link-local IP. Operators running self-hosted LLMs on private IPs can leave the flag off; multi-tenant deployments should turn it on. This is save-time validation; runtime fetch sites should additionally adopt SSRF-safe agents (defense in depth).
43 lines
1.2 KiB
JavaScript
43 lines
1.2 KiB
JavaScript
const dns = require('node:dns').promises;
|
|
const { isPrivateIP } = require('@librechat/api');
|
|
|
|
const ALLOWED_PROTOCOLS = new Set(['http:', 'https:']);
|
|
|
|
/**
|
|
* Validates that a user-supplied baseURL is safe to fetch from.
|
|
*
|
|
* Throws when the URL is malformed, uses a non-http(s) protocol, or
|
|
* resolves (now) to a private/loopback/link-local address. The DNS
|
|
* check is best-effort and does not protect against rebinding at
|
|
* fetch time — runtime call sites should still use SSRF-safe agents.
|
|
*
|
|
* @param {string} rawBaseURL
|
|
* @returns {Promise<void>}
|
|
*/
|
|
async function validateUserBaseURL(rawBaseURL) {
|
|
let parsed;
|
|
try {
|
|
parsed = new URL(rawBaseURL);
|
|
} catch {
|
|
throw new Error('baseURL is not a valid URL');
|
|
}
|
|
|
|
if (!ALLOWED_PROTOCOLS.has(parsed.protocol)) {
|
|
throw new Error(`baseURL must use http or https, got "${parsed.protocol}"`);
|
|
}
|
|
|
|
let addresses;
|
|
try {
|
|
addresses = await dns.lookup(parsed.hostname, { all: true });
|
|
} catch {
|
|
throw new Error('baseURL hostname could not be resolved');
|
|
}
|
|
|
|
for (const { address } of addresses) {
|
|
if (isPrivateIP(address)) {
|
|
throw new Error('baseURL resolves to a private or reserved IP address');
|
|
}
|
|
}
|
|
}
|
|
|
|
module.exports = { validateUserBaseURL };
|