fix(audio): add SSRF protection, MIME type fix, and tests for audio-transcribe tool

- Add URL validation blocking localhost, private IPs, and non-http protocols
- Use response Content-Type header instead of hardcoded audio/wav for URL downloads
- Add 25 tests covering validation, SSRF, config errors, transcription paths, and error handling

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
William Valentin
2026-02-11 21:57:45 -08:00
parent a8a2c59313
commit 0b44adbaea
2 changed files with 325 additions and 1 deletions
+34 -1
View File
@@ -25,6 +25,31 @@ const PROVIDER_ENDPOINTS: Record<string, string> = {
llamacpp: 'http://localhost:8080/v1/audio/transcriptions',
};
function validateUrl(url: string): { valid: boolean; error?: string } {
let parsed: URL;
try {
parsed = new URL(url);
} catch {
return { valid: false, error: `Invalid URL: ${url}` };
}
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
return { valid: false, error: `Only http/https URLs are allowed, got ${parsed.protocol}` };
}
const hostname = parsed.hostname;
if (hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1' || hostname === '0.0.0.0') {
return { valid: false, error: 'URLs pointing to localhost are not allowed' };
}
// Block private/internal IP ranges
if (/^(10\.|172\.(1[6-9]|2\d|3[01])\.|192\.168\.|169\.254\.)/.test(hostname)) {
return { valid: false, error: 'URLs pointing to private/internal networks are not allowed' };
}
return { valid: true };
}
function validateInput(args: AudioTranscribeArgs): { valid: boolean; error?: string } {
const hasData = args.data !== undefined && args.data !== '';
const hasUrl = args.url !== undefined && args.url !== '';
@@ -45,6 +70,13 @@ function validateInput(args: AudioTranscribeArgs): { valid: boolean; error?: str
return { valid: false, error: `Unsupported MIME type: ${args.mime_type}. Supported: ${Array.from(SUPPORTED_MIME_TYPES).join(', ')}` };
}
if (hasUrl) {
const urlValidation = validateUrl(args.url!);
if (!urlValidation.valid) {
return urlValidation;
}
}
return { valid: true };
}
@@ -136,7 +168,8 @@ export function createAudioTranscribeTool(audioConfig?: AudioTranscriptionConfig
const urlExt = args.url.split('.').pop()?.split('?')[0] || 'bin';
filename = `audio.${urlExt}`;
audioBlob = new Blob([arrayBuffer], { type: 'audio/wav' });
const contentType = response.headers.get('content-type') ?? 'audio/wav';
audioBlob = new Blob([arrayBuffer], { type: contentType });
}
const endpoint = audioConfig.endpoint;