🔒 fix(security): Sanitize Azure provider error responses to prevent API key exposure (#9583)

This commit is contained in:
Arvin Xu
2025-10-06 06:24:27 +02:00
committed by GitHub
parent c0974ea955
commit af59bfe013
4 changed files with 178 additions and 2 deletions
@@ -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;
}