mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-03-16 20:56:35 +01:00
🫷 fix: Validate User-Provided Base URL in Endpoint Init (#12248)
* 🛡️ fix: Block SSRF via user-provided baseURL in endpoint initialization User-provided baseURL values (when endpoint is configured with `user_provided`) were passed through to the OpenAI SDK without validation. Combined with `directEndpoint`, this allowed arbitrary server-side requests to internal/metadata URLs. Adds `validateEndpointURL` that checks against known SSRF targets and DNS-resolves hostnames to block private IPs. Applied in both custom and OpenAI endpoint initialization paths. * 🧪 test: Add validateEndpointURL SSRF tests Covers unparseable URLs, localhost, private IPs, link-local/metadata, internal Docker/K8s hostnames, DNS resolution to private IPs, and legitimate public URLs. * 🛡️ fix: Add protocol enforcement and import order fix - Reject non-HTTP/HTTPS schemes (ftp://, file://, data:, etc.) in validateEndpointURL before SSRF hostname checks - Document DNS rebinding limitation and fail-open semantics in JSDoc - Fix import order in custom/initialize.ts per project conventions * 🧪 test: Expand SSRF validation coverage and add initializer integration tests Unit tests for validateEndpointURL: - Non-HTTP/HTTPS schemes (ftp, file, data) - IPv6 loopback, link-local, and unique-local addresses - .local and .internal TLD hostnames - DNS fail-open path (lookup failure allows request) Integration tests for initializeCustom and initializeOpenAI: - Guard fires when userProvidesURL is true - Guard skipped when URL is system-defined or falsy - SSRF rejection propagates and prevents getOpenAIConfig call * 🐛 fix: Correct broken env restore in OpenAI initialize spec process.env was captured by reference, not by value, making the restore closure a no-op. Snapshot individual env keys before mutation so they can be properly restored after each test. * 🛡️ fix: Throw structured ErrorTypes for SSRF base URL validation Replace plain-string Error throws in validateEndpointURL with JSON-structured errors using type 'invalid_base_url' (matching new ErrorTypes.INVALID_BASE_URL enum value). This ensures the client-side Error component can look up a localized message instead of falling through to the raw-text default. Changes across workspaces: - data-provider: add INVALID_BASE_URL to ErrorTypes enum - packages/api: throwInvalidBaseURL helper emits structured JSON - client: add errorMessages entry and localization key - tests: add structured JSON format assertion * 🧹 refactor: Use ErrorTypes enum key in Error.tsx for consistency Replace bare string literal 'invalid_base_url' with computed property [ErrorTypes.INVALID_BASE_URL] to match every other entry in the errorMessages map.
This commit is contained in:
parent
f9927f0168
commit
f7ab5e645a
9 changed files with 446 additions and 1 deletions
|
|
@ -12,6 +12,7 @@ import {
|
|||
isPrivateIP,
|
||||
isSSRFTarget,
|
||||
resolveHostnameSSRF,
|
||||
validateEndpointURL,
|
||||
} from './domain';
|
||||
|
||||
const mockedLookup = lookup as jest.MockedFunction<typeof lookup>;
|
||||
|
|
@ -1209,3 +1210,135 @@ describe('isMCPDomainAllowed', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateEndpointURL', () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should throw for unparseable URLs', async () => {
|
||||
await expect(validateEndpointURL('not-a-url', 'test-ep')).rejects.toThrow(
|
||||
'Invalid base URL for test-ep',
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw for localhost URLs', async () => {
|
||||
await expect(validateEndpointURL('http://localhost:8080/v1', 'test-ep')).rejects.toThrow(
|
||||
'targets a restricted address',
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw for private IP URLs', async () => {
|
||||
await expect(validateEndpointURL('http://192.168.1.1/v1', 'test-ep')).rejects.toThrow(
|
||||
'targets a restricted address',
|
||||
);
|
||||
await expect(validateEndpointURL('http://10.0.0.1/v1', 'test-ep')).rejects.toThrow(
|
||||
'targets a restricted address',
|
||||
);
|
||||
await expect(validateEndpointURL('http://172.16.0.1/v1', 'test-ep')).rejects.toThrow(
|
||||
'targets a restricted address',
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw for link-local / metadata IP', async () => {
|
||||
await expect(
|
||||
validateEndpointURL('http://169.254.169.254/latest/meta-data/', 'test-ep'),
|
||||
).rejects.toThrow('targets a restricted address');
|
||||
});
|
||||
|
||||
it('should throw for loopback IP', async () => {
|
||||
await expect(validateEndpointURL('http://127.0.0.1:11434/v1', 'test-ep')).rejects.toThrow(
|
||||
'targets a restricted address',
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw for internal Docker/Kubernetes hostnames', async () => {
|
||||
await expect(validateEndpointURL('http://redis:6379/', 'test-ep')).rejects.toThrow(
|
||||
'targets a restricted address',
|
||||
);
|
||||
await expect(validateEndpointURL('http://mongodb:27017/', 'test-ep')).rejects.toThrow(
|
||||
'targets a restricted address',
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw when hostname DNS-resolves to a private IP', async () => {
|
||||
mockedLookup.mockResolvedValueOnce([{ address: '10.0.0.5', family: 4 }] as never);
|
||||
await expect(validateEndpointURL('https://evil.example.com/v1', 'test-ep')).rejects.toThrow(
|
||||
'resolves to a restricted address',
|
||||
);
|
||||
});
|
||||
|
||||
it('should allow public URLs', async () => {
|
||||
mockedLookup.mockResolvedValueOnce([{ address: '104.18.7.192', family: 4 }] as never);
|
||||
await expect(
|
||||
validateEndpointURL('https://api.openai.com/v1', 'test-ep'),
|
||||
).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it('should allow public URLs that resolve to public IPs', async () => {
|
||||
mockedLookup.mockResolvedValueOnce([{ address: '8.8.8.8', family: 4 }] as never);
|
||||
await expect(
|
||||
validateEndpointURL('https://api.example.com/v1/chat', 'test-ep'),
|
||||
).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it('should throw for non-HTTP/HTTPS schemes', async () => {
|
||||
await expect(validateEndpointURL('ftp://example.com/v1', 'test-ep')).rejects.toThrow(
|
||||
'only HTTP and HTTPS are permitted',
|
||||
);
|
||||
await expect(validateEndpointURL('file:///etc/passwd', 'test-ep')).rejects.toThrow(
|
||||
'only HTTP and HTTPS are permitted',
|
||||
);
|
||||
await expect(validateEndpointURL('data:text/plain,hello', 'test-ep')).rejects.toThrow(
|
||||
'only HTTP and HTTPS are permitted',
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw for IPv6 loopback URL', async () => {
|
||||
await expect(validateEndpointURL('http://[::1]:8080/v1', 'test-ep')).rejects.toThrow(
|
||||
'targets a restricted address',
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw for IPv6 link-local URL', async () => {
|
||||
await expect(validateEndpointURL('http://[fe80::1]/v1', 'test-ep')).rejects.toThrow(
|
||||
'targets a restricted address',
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw for IPv6 unique-local URL', async () => {
|
||||
await expect(validateEndpointURL('http://[fc00::1]/v1', 'test-ep')).rejects.toThrow(
|
||||
'targets a restricted address',
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw for .local TLD hostname', async () => {
|
||||
await expect(validateEndpointURL('http://myservice.local/v1', 'test-ep')).rejects.toThrow(
|
||||
'targets a restricted address',
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw for .internal TLD hostname', async () => {
|
||||
await expect(validateEndpointURL('http://api.internal/v1', 'test-ep')).rejects.toThrow(
|
||||
'targets a restricted address',
|
||||
);
|
||||
});
|
||||
|
||||
it('should pass when DNS lookup fails (fail-open)', async () => {
|
||||
mockedLookup.mockRejectedValueOnce(new Error('ENOTFOUND'));
|
||||
await expect(
|
||||
validateEndpointURL('https://nonexistent.example.com/v1', 'test-ep'),
|
||||
).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it('should throw structured JSON with type invalid_base_url', async () => {
|
||||
const error = await validateEndpointURL('http://169.254.169.254/latest/', 'my-ep').catch(
|
||||
(err: Error) => err,
|
||||
);
|
||||
expect(error).toBeInstanceOf(Error);
|
||||
const parsed = JSON.parse((error as Error).message);
|
||||
expect(parsed.type).toBe('invalid_base_url');
|
||||
expect(parsed.message).toContain('my-ep');
|
||||
expect(parsed.message).toContain('targets a restricted address');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -499,3 +499,45 @@ export async function isMCPDomainAllowed(
|
|||
// Use MCP_PROTOCOLS (HTTP/HTTPS/WS/WSS) for MCP server validation
|
||||
return isDomainAllowedCore(domain, allowedDomains, MCP_PROTOCOLS);
|
||||
}
|
||||
|
||||
/** Matches ErrorTypes.INVALID_BASE_URL — string literal avoids build-time dependency on data-provider */
|
||||
const INVALID_BASE_URL_TYPE = 'invalid_base_url';
|
||||
|
||||
function throwInvalidBaseURL(message: string): never {
|
||||
throw new Error(JSON.stringify({ type: INVALID_BASE_URL_TYPE, message }));
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates that a user-provided endpoint URL does not target private/internal addresses.
|
||||
* Throws if the URL is unparseable, uses a non-HTTP(S) scheme, targets a known SSRF hostname,
|
||||
* or DNS-resolves to a private IP.
|
||||
*
|
||||
* @note DNS rebinding: validation performs a single DNS lookup. An adversary controlling
|
||||
* DNS with TTL=0 could respond with a public IP at validation time and a private IP
|
||||
* at request time. This is an accepted limitation of point-in-time DNS checks.
|
||||
* @note Fail-open on DNS errors: a resolution failure here implies a failure at request
|
||||
* time as well, matching {@link resolveHostnameSSRF} semantics.
|
||||
*/
|
||||
export async function validateEndpointURL(url: string, endpoint: string): Promise<void> {
|
||||
let hostname: string;
|
||||
let protocol: string;
|
||||
try {
|
||||
const parsed = new URL(url);
|
||||
hostname = parsed.hostname;
|
||||
protocol = parsed.protocol;
|
||||
} catch {
|
||||
throwInvalidBaseURL(`Invalid base URL for ${endpoint}: unable to parse URL.`);
|
||||
}
|
||||
|
||||
if (protocol !== 'http:' && protocol !== 'https:') {
|
||||
throwInvalidBaseURL(`Invalid base URL for ${endpoint}: only HTTP and HTTPS are permitted.`);
|
||||
}
|
||||
|
||||
if (isSSRFTarget(hostname)) {
|
||||
throwInvalidBaseURL(`Base URL for ${endpoint} targets a restricted address.`);
|
||||
}
|
||||
|
||||
if (await resolveHostnameSSRF(hostname)) {
|
||||
throwInvalidBaseURL(`Base URL for ${endpoint} resolves to a restricted address.`);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
119
packages/api/src/endpoints/custom/initialize.spec.ts
Normal file
119
packages/api/src/endpoints/custom/initialize.spec.ts
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
import { AuthType } from 'librechat-data-provider';
|
||||
import type { BaseInitializeParams } from '~/types';
|
||||
|
||||
const mockValidateEndpointURL = jest.fn();
|
||||
jest.mock('~/auth', () => ({
|
||||
validateEndpointURL: (...args: unknown[]) => mockValidateEndpointURL(...args),
|
||||
}));
|
||||
|
||||
const mockGetOpenAIConfig = jest.fn().mockReturnValue({
|
||||
llmConfig: { model: 'test-model' },
|
||||
configOptions: {},
|
||||
});
|
||||
jest.mock('~/endpoints/openai/config', () => ({
|
||||
getOpenAIConfig: (...args: unknown[]) => mockGetOpenAIConfig(...args),
|
||||
}));
|
||||
|
||||
jest.mock('~/endpoints/models', () => ({
|
||||
fetchModels: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('~/cache', () => ({
|
||||
standardCache: jest.fn(() => ({ get: jest.fn().mockResolvedValue(null) })),
|
||||
}));
|
||||
|
||||
jest.mock('~/utils', () => ({
|
||||
isUserProvided: (val: string) => val === 'user_provided',
|
||||
checkUserKeyExpiry: jest.fn(),
|
||||
}));
|
||||
|
||||
const mockGetCustomEndpointConfig = jest.fn();
|
||||
jest.mock('~/app/config', () => ({
|
||||
getCustomEndpointConfig: (...args: unknown[]) => mockGetCustomEndpointConfig(...args),
|
||||
}));
|
||||
|
||||
import { initializeCustom } from './initialize';
|
||||
|
||||
function createParams(overrides: {
|
||||
apiKey?: string;
|
||||
baseURL?: string;
|
||||
userBaseURL?: string;
|
||||
userApiKey?: string;
|
||||
expiresAt?: string;
|
||||
}): BaseInitializeParams {
|
||||
const { apiKey = 'sk-test-key', baseURL = 'https://api.example.com/v1' } = overrides;
|
||||
|
||||
mockGetCustomEndpointConfig.mockReturnValue({
|
||||
apiKey,
|
||||
baseURL,
|
||||
models: {},
|
||||
});
|
||||
|
||||
const db = {
|
||||
getUserKeyValues: jest.fn().mockResolvedValue({
|
||||
apiKey: overrides.userApiKey ?? 'sk-user-key',
|
||||
baseURL: overrides.userBaseURL ?? 'https://user-api.example.com/v1',
|
||||
}),
|
||||
} as unknown as BaseInitializeParams['db'];
|
||||
|
||||
return {
|
||||
req: {
|
||||
user: { id: 'user-1' },
|
||||
body: { key: overrides.expiresAt ?? '2099-01-01' },
|
||||
config: {},
|
||||
} as unknown as BaseInitializeParams['req'],
|
||||
endpoint: 'test-custom',
|
||||
model_parameters: { model: 'gpt-4' },
|
||||
db,
|
||||
};
|
||||
}
|
||||
|
||||
describe('initializeCustom – SSRF guard wiring', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should call validateEndpointURL when baseURL is user_provided', async () => {
|
||||
const params = createParams({
|
||||
apiKey: 'sk-test-key',
|
||||
baseURL: AuthType.USER_PROVIDED,
|
||||
userBaseURL: 'https://user-api.example.com/v1',
|
||||
expiresAt: '2099-01-01',
|
||||
});
|
||||
|
||||
await initializeCustom(params);
|
||||
|
||||
expect(mockValidateEndpointURL).toHaveBeenCalledTimes(1);
|
||||
expect(mockValidateEndpointURL).toHaveBeenCalledWith(
|
||||
'https://user-api.example.com/v1',
|
||||
'test-custom',
|
||||
);
|
||||
});
|
||||
|
||||
it('should NOT call validateEndpointURL when baseURL is system-defined', async () => {
|
||||
const params = createParams({
|
||||
apiKey: 'sk-test-key',
|
||||
baseURL: 'https://api.provider.com/v1',
|
||||
});
|
||||
|
||||
await initializeCustom(params);
|
||||
|
||||
expect(mockValidateEndpointURL).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should propagate SSRF rejection from validateEndpointURL', async () => {
|
||||
mockValidateEndpointURL.mockRejectedValueOnce(
|
||||
new Error('Base URL for test-custom targets a restricted address.'),
|
||||
);
|
||||
|
||||
const params = createParams({
|
||||
apiKey: 'sk-test-key',
|
||||
baseURL: AuthType.USER_PROVIDED,
|
||||
userBaseURL: 'http://169.254.169.254/latest/meta-data/',
|
||||
expiresAt: '2099-01-01',
|
||||
});
|
||||
|
||||
await expect(initializeCustom(params)).rejects.toThrow('targets a restricted address');
|
||||
expect(mockGetOpenAIConfig).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
|
@ -9,9 +9,10 @@ import type { TEndpoint } from 'librechat-data-provider';
|
|||
import type { AppConfig } from '@librechat/data-schemas';
|
||||
import type { BaseInitializeParams, InitializeResultBase, EndpointTokenConfig } from '~/types';
|
||||
import { getOpenAIConfig } from '~/endpoints/openai/config';
|
||||
import { isUserProvided, checkUserKeyExpiry } from '~/utils';
|
||||
import { getCustomEndpointConfig } from '~/app/config';
|
||||
import { fetchModels } from '~/endpoints/models';
|
||||
import { isUserProvided, checkUserKeyExpiry } from '~/utils';
|
||||
import { validateEndpointURL } from '~/auth';
|
||||
import { standardCache } from '~/cache';
|
||||
|
||||
const { PROXY } = process.env;
|
||||
|
|
@ -123,6 +124,10 @@ export async function initializeCustom({
|
|||
throw new Error(`${endpoint} Base URL not provided.`);
|
||||
}
|
||||
|
||||
if (userProvidesURL) {
|
||||
await validateEndpointURL(baseURL, endpoint);
|
||||
}
|
||||
|
||||
let endpointTokenConfig: EndpointTokenConfig | undefined;
|
||||
|
||||
const userId = req.user?.id ?? '';
|
||||
|
|
|
|||
135
packages/api/src/endpoints/openai/initialize.spec.ts
Normal file
135
packages/api/src/endpoints/openai/initialize.spec.ts
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
import { AuthType, EModelEndpoint } from 'librechat-data-provider';
|
||||
import type { BaseInitializeParams } from '~/types';
|
||||
|
||||
const mockValidateEndpointURL = jest.fn();
|
||||
jest.mock('~/auth', () => ({
|
||||
validateEndpointURL: (...args: unknown[]) => mockValidateEndpointURL(...args),
|
||||
}));
|
||||
|
||||
const mockGetOpenAIConfig = jest.fn().mockReturnValue({
|
||||
llmConfig: { model: 'gpt-4' },
|
||||
configOptions: {},
|
||||
});
|
||||
jest.mock('./config', () => ({
|
||||
getOpenAIConfig: (...args: unknown[]) => mockGetOpenAIConfig(...args),
|
||||
}));
|
||||
|
||||
jest.mock('~/utils', () => ({
|
||||
getAzureCredentials: jest.fn(),
|
||||
resolveHeaders: jest.fn(() => ({})),
|
||||
isUserProvided: (val: string) => val === 'user_provided',
|
||||
checkUserKeyExpiry: jest.fn(),
|
||||
}));
|
||||
|
||||
import { initializeOpenAI } from './initialize';
|
||||
|
||||
function createParams(env: Record<string, string | undefined>): BaseInitializeParams {
|
||||
const savedEnv: Record<string, string | undefined> = {};
|
||||
for (const key of Object.keys(env)) {
|
||||
savedEnv[key] = process.env[key];
|
||||
}
|
||||
Object.assign(process.env, env);
|
||||
|
||||
const db = {
|
||||
getUserKeyValues: jest.fn().mockResolvedValue({
|
||||
apiKey: 'sk-user-key',
|
||||
baseURL: 'https://user-proxy.example.com/v1',
|
||||
}),
|
||||
} as unknown as BaseInitializeParams['db'];
|
||||
|
||||
const params: BaseInitializeParams = {
|
||||
req: {
|
||||
user: { id: 'user-1' },
|
||||
body: { key: '2099-01-01' },
|
||||
config: { endpoints: {} },
|
||||
} as unknown as BaseInitializeParams['req'],
|
||||
endpoint: EModelEndpoint.openAI,
|
||||
model_parameters: { model: 'gpt-4' },
|
||||
db,
|
||||
};
|
||||
|
||||
const restore = () => {
|
||||
for (const key of Object.keys(env)) {
|
||||
if (savedEnv[key] === undefined) {
|
||||
delete process.env[key];
|
||||
} else {
|
||||
process.env[key] = savedEnv[key];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return Object.assign(params, { _restore: restore });
|
||||
}
|
||||
|
||||
describe('initializeOpenAI – SSRF guard wiring', () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should call validateEndpointURL when OPENAI_REVERSE_PROXY is user_provided', async () => {
|
||||
const params = createParams({
|
||||
OPENAI_API_KEY: 'sk-test',
|
||||
OPENAI_REVERSE_PROXY: AuthType.USER_PROVIDED,
|
||||
});
|
||||
|
||||
try {
|
||||
await initializeOpenAI(params);
|
||||
} finally {
|
||||
(params as unknown as { _restore: () => void })._restore();
|
||||
}
|
||||
|
||||
expect(mockValidateEndpointURL).toHaveBeenCalledTimes(1);
|
||||
expect(mockValidateEndpointURL).toHaveBeenCalledWith(
|
||||
'https://user-proxy.example.com/v1',
|
||||
EModelEndpoint.openAI,
|
||||
);
|
||||
});
|
||||
|
||||
it('should NOT call validateEndpointURL when OPENAI_REVERSE_PROXY is a system URL', async () => {
|
||||
const params = createParams({
|
||||
OPENAI_API_KEY: 'sk-test',
|
||||
OPENAI_REVERSE_PROXY: 'https://api.openai.com/v1',
|
||||
});
|
||||
|
||||
try {
|
||||
await initializeOpenAI(params);
|
||||
} finally {
|
||||
(params as unknown as { _restore: () => void })._restore();
|
||||
}
|
||||
|
||||
expect(mockValidateEndpointURL).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should NOT call validateEndpointURL when baseURL is falsy', async () => {
|
||||
const params = createParams({
|
||||
OPENAI_API_KEY: 'sk-test',
|
||||
});
|
||||
|
||||
try {
|
||||
await initializeOpenAI(params);
|
||||
} finally {
|
||||
(params as unknown as { _restore: () => void })._restore();
|
||||
}
|
||||
|
||||
expect(mockValidateEndpointURL).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should propagate SSRF rejection from validateEndpointURL', async () => {
|
||||
mockValidateEndpointURL.mockRejectedValueOnce(
|
||||
new Error('Base URL for openAI targets a restricted address.'),
|
||||
);
|
||||
|
||||
const params = createParams({
|
||||
OPENAI_API_KEY: 'sk-test',
|
||||
OPENAI_REVERSE_PROXY: AuthType.USER_PROVIDED,
|
||||
});
|
||||
|
||||
try {
|
||||
await expect(initializeOpenAI(params)).rejects.toThrow('targets a restricted address');
|
||||
} finally {
|
||||
(params as unknown as { _restore: () => void })._restore();
|
||||
}
|
||||
|
||||
expect(mockGetOpenAIConfig).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
|
@ -6,6 +6,7 @@ import type {
|
|||
UserKeyValues,
|
||||
} from '~/types';
|
||||
import { getAzureCredentials, resolveHeaders, isUserProvided, checkUserKeyExpiry } from '~/utils';
|
||||
import { validateEndpointURL } from '~/auth';
|
||||
import { getOpenAIConfig } from './config';
|
||||
|
||||
/**
|
||||
|
|
@ -55,6 +56,10 @@ export async function initializeOpenAI({
|
|||
? userValues?.baseURL
|
||||
: baseURLOptions[endpoint as keyof typeof baseURLOptions];
|
||||
|
||||
if (userProvidesURL && baseURL) {
|
||||
await validateEndpointURL(baseURL, endpoint);
|
||||
}
|
||||
|
||||
const clientOptions: OpenAIConfigOptions = {
|
||||
proxy: PROXY ?? undefined,
|
||||
reverseProxyUrl: baseURL || undefined,
|
||||
|
|
|
|||
|
|
@ -1560,6 +1560,10 @@ export enum ErrorTypes {
|
|||
* No Base URL Provided.
|
||||
*/
|
||||
NO_BASE_URL = 'no_base_url',
|
||||
/**
|
||||
* Base URL targets a restricted or invalid address (SSRF protection).
|
||||
*/
|
||||
INVALID_BASE_URL = 'invalid_base_url',
|
||||
/**
|
||||
* Moderation error
|
||||
*/
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue