mirror of
https://github.com/lobehub/lobe-chat.git
synced 2026-06-16 12:36:07 +00:00
🔒 fix(security): Sanitize Azure provider error responses to prevent API key exposure (#9583)
This commit is contained in:
@@ -20,6 +20,7 @@ import { AgentRuntimeError } from '../../utils/createError';
|
||||
import { debugStream } from '../../utils/debugStream';
|
||||
import { convertImageUrlToFile, convertOpenAIMessages } from '../../utils/openaiHelpers';
|
||||
import { StreamingResponse } from '../../utils/response';
|
||||
import { sanitizeError } from '../../utils/sanitizeError';
|
||||
|
||||
const azureImageLogger = debug('lobe-image:azure');
|
||||
export class LobeAzureOpenAI implements LobeRuntimeAI {
|
||||
@@ -253,9 +254,12 @@ export class LobeAzureOpenAI implements LobeRuntimeAI {
|
||||
? AgentRuntimeErrorType.ProviderBizError
|
||||
: AgentRuntimeErrorType.AgentRuntimeError;
|
||||
|
||||
// Sanitize error to remove sensitive information like API keys from headers
|
||||
const sanitizedError = sanitizeError(error);
|
||||
|
||||
throw AgentRuntimeError.chat({
|
||||
endpoint: this.maskSensitiveUrl(this.baseURL),
|
||||
error,
|
||||
error: sanitizedError,
|
||||
errorType,
|
||||
provider: ModelProvider.Azure,
|
||||
});
|
||||
|
||||
@@ -12,6 +12,7 @@ import { AgentRuntimeErrorType } from '../../types/error';
|
||||
import { AgentRuntimeError } from '../../utils/createError';
|
||||
import { debugStream } from '../../utils/debugStream';
|
||||
import { StreamingResponse } from '../../utils/response';
|
||||
import { sanitizeError } from '../../utils/sanitizeError';
|
||||
|
||||
interface AzureAIParams {
|
||||
apiKey?: string;
|
||||
@@ -112,9 +113,12 @@ export class LobeAzureAI implements LobeRuntimeAI {
|
||||
? AgentRuntimeErrorType.ProviderBizError
|
||||
: AgentRuntimeErrorType.AgentRuntimeError;
|
||||
|
||||
// Sanitize error to remove sensitive information like API keys from headers
|
||||
const sanitizedError = sanitizeError(error);
|
||||
|
||||
throw AgentRuntimeError.chat({
|
||||
endpoint: this.maskSensitiveUrl(this.baseURL),
|
||||
error,
|
||||
error: sanitizedError,
|
||||
errorType,
|
||||
provider: ModelProvider.Azure,
|
||||
});
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { sanitizeError } from './sanitizeError';
|
||||
|
||||
describe('sanitizeError', () => {
|
||||
it('should remove sensitive request headers', () => {
|
||||
const errorWithHeaders = {
|
||||
message: 'API Error',
|
||||
code: 401,
|
||||
request: {
|
||||
headers: {
|
||||
authorization: 'Bearer sk-1234567890',
|
||||
'content-type': 'application/json',
|
||||
'ocp-apim-subscription-key': 'azure-key-123',
|
||||
},
|
||||
url: 'https://api.example.com',
|
||||
},
|
||||
};
|
||||
|
||||
const sanitized = sanitizeError(errorWithHeaders);
|
||||
|
||||
expect(sanitized).toEqual({
|
||||
message: 'API Error',
|
||||
code: 401,
|
||||
});
|
||||
expect(sanitized.request).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should remove sensitive fields at any level', () => {
|
||||
const errorWithNestedSensitive = {
|
||||
message: 'Error',
|
||||
data: {
|
||||
config: {
|
||||
apikey: 'secret-key',
|
||||
headers: {
|
||||
authorization: 'Bearer token',
|
||||
},
|
||||
},
|
||||
response: {
|
||||
status: 401,
|
||||
data: 'Unauthorized',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const sanitized = sanitizeError(errorWithNestedSensitive);
|
||||
|
||||
expect(sanitized).toEqual({
|
||||
message: 'Error',
|
||||
data: {
|
||||
response: {
|
||||
status: 401,
|
||||
data: 'Unauthorized',
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(sanitized.data.config).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should handle primitive values', () => {
|
||||
expect(sanitizeError('string')).toBe('string');
|
||||
expect(sanitizeError(123)).toBe(123);
|
||||
expect(sanitizeError(true)).toBe(true);
|
||||
expect(sanitizeError(null)).toBe(null);
|
||||
expect(sanitizeError(undefined)).toBe(undefined);
|
||||
});
|
||||
|
||||
it('should handle arrays', () => {
|
||||
const errorArray = [
|
||||
{ message: 'Error 1', apikey: 'secret' },
|
||||
{ message: 'Error 2', status: 500 },
|
||||
];
|
||||
|
||||
const sanitized = sanitizeError(errorArray);
|
||||
|
||||
expect(sanitized).toEqual([{ message: 'Error 1' }, { message: 'Error 2', status: 500 }]);
|
||||
});
|
||||
|
||||
it('should be case insensitive for sensitive field detection', () => {
|
||||
const errorWithMixedCase = {
|
||||
message: 'Error',
|
||||
Authorization: 'Bearer token',
|
||||
'API-KEY': 'secret',
|
||||
Headers: { token: 'secret' },
|
||||
};
|
||||
|
||||
const sanitized = sanitizeError(errorWithMixedCase);
|
||||
|
||||
expect(sanitized).toEqual({
|
||||
message: 'Error',
|
||||
});
|
||||
});
|
||||
|
||||
it('should preserve safe nested structures', () => {
|
||||
const errorWithSafeNested = {
|
||||
message: 'Error occurred',
|
||||
status: 401,
|
||||
details: {
|
||||
code: 'UNAUTHORIZED',
|
||||
timestamp: '2024-01-01T00:00:00Z',
|
||||
path: '/api/chat',
|
||||
},
|
||||
};
|
||||
|
||||
const sanitized = sanitizeError(errorWithSafeNested);
|
||||
|
||||
expect(sanitized).toEqual(errorWithSafeNested);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* Sanitizes error objects by removing sensitive information that could expose API keys or other credentials.
|
||||
* This is particularly important for errors from Azure/OpenAI SDKs that may include request headers.
|
||||
*/
|
||||
export function sanitizeError(error: any): any {
|
||||
if (!error || typeof error !== 'object') {
|
||||
return error;
|
||||
}
|
||||
|
||||
// Handle array of errors
|
||||
if (Array.isArray(error)) {
|
||||
return error.map(sanitizeError);
|
||||
}
|
||||
|
||||
// Create a sanitized copy
|
||||
const sanitized: any = {};
|
||||
|
||||
// List of sensitive fields that should be removed or masked
|
||||
const sensitiveFields = [
|
||||
'request',
|
||||
'headers',
|
||||
'authorization',
|
||||
'apikey',
|
||||
'api-key',
|
||||
'ocp-apim-subscription-key',
|
||||
'x-api-key',
|
||||
'bearer',
|
||||
'token',
|
||||
'auth',
|
||||
'credential',
|
||||
'key',
|
||||
'secret',
|
||||
'password',
|
||||
'config',
|
||||
'options',
|
||||
];
|
||||
|
||||
// Copy safe fields and recursively sanitize nested objects
|
||||
for (const key in error) {
|
||||
if (error.hasOwnProperty(key)) {
|
||||
const value = error[key];
|
||||
const lowerKey = key.toLowerCase();
|
||||
|
||||
// Skip sensitive fields entirely
|
||||
if (sensitiveFields.indexOf(lowerKey) !== -1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Recursively sanitize nested objects
|
||||
if (value && typeof value === 'object') {
|
||||
sanitized[key] = sanitizeError(value);
|
||||
} else {
|
||||
sanitized[key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return sanitized;
|
||||
}
|
||||
Reference in New Issue
Block a user