LibreChat/api/server/utils/validateBaseURL.js
Danny Avila 630cb59e3b 🛡️ fix: Optionally Block Private IPs On User-Provided baseURL
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).
2026-05-03 13:51:13 -04:00

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 };