UNPKG

breathe-api

Version:

Model Context Protocol server for Breathe HR APIs with Swagger/OpenAPI support - also works with custom APIs

156 lines 4.64 kB
import { URL } from 'url'; export function sanitizeUrl(url) { try { const parsed = new URL(url); if (!['http:', 'https:'].includes(parsed.protocol)) { throw new Error(`Invalid protocol: ${parsed.protocol}`); } const hostname = parsed.hostname.toLowerCase(); const blockedHosts = [ 'localhost', '127.0.0.1', '0.0.0.0', '::1', '169.254.169.254', 'metadata.google.internal', ]; if (blockedHosts.includes(hostname)) { throw new Error('Access to local/internal URLs is not allowed'); } if (isPrivateIP(hostname)) { throw new Error('Access to private IP addresses is not allowed'); } return parsed.toString(); } catch (error) { if (error instanceof Error) { throw new Error(`Invalid URL: ${error.message}`); } throw new Error('Invalid URL format'); } } function isPrivateIP(hostname) { const parts = hostname.split('.'); if (parts.length !== 4) return false; const nums = parts.map(p => parseInt(p, 10)); if (nums.some(n => isNaN(n) || n < 0 || n > 255)) return false; return (nums[0] === 10 || (nums[0] === 172 && nums[1] >= 16 && nums[1] <= 31) || (nums[0] === 192 && nums[1] === 168)); } export function sanitizeHeaders(headers) { const sanitized = {}; const blockedHeaders = [ 'host', 'cookie', 'set-cookie', 'x-forwarded-for', 'x-real-ip', 'x-forwarded-host', 'x-forwarded-proto', 'x-frame-options', 'x-content-type-options', ]; for (const [key, value] of Object.entries(headers)) { const lowerKey = key.toLowerCase(); if (blockedHeaders.includes(lowerKey)) { continue; } if (!/^[a-zA-Z0-9-_]+$/.test(key)) { continue; } if (typeof value !== 'string' || /[\x00-\x1F\x7F]/.test(value)) { continue; } if (value.length > 8192) { continue; } sanitized[key] = value; } return sanitized; } export function sanitizeParams(params) { const sanitized = {}; for (const [key, value] of Object.entries(params)) { if (!/^[a-zA-Z0-9-_.[\]]+$/.test(key)) { continue; } const strValue = String(value); if (strValue.length > 2048) { continue; } const cleaned = strValue.replace(/[\x00-\x1F\x7F]/g, ''); sanitized[key] = cleaned; } return sanitized; } export function sanitizeRequestBody(data) { const cloned = JSON.parse(JSON.stringify(data)); return sanitizeObject(cloned); } function sanitizeObject(obj, depth = 0) { if (depth > 10) { throw new Error('Object nesting too deep'); } if (obj === null || obj === undefined) { return obj; } if (typeof obj === 'string') { return obj.replace(/[\x00-\x1F\x7F]/g, '').substring(0, 65536); } if (typeof obj === 'number' || typeof obj === 'boolean') { return obj; } if (Array.isArray(obj)) { if (obj.length > 1000) { throw new Error('Array too large'); } return obj.map(item => sanitizeObject(item, depth + 1)); } if (typeof obj === 'object') { const sanitized = {}; const keys = Object.keys(obj); if (keys.length > 1000) { throw new Error('Object has too many properties'); } for (const key of keys) { if (!/^[a-zA-Z0-9-_.]+$/.test(key) || key.length > 256) { continue; } sanitized[key] = sanitizeObject(obj[key], depth + 1); } return sanitized; } return undefined; } export function sanitizePath(path) { const cleaned = path .replace(/\.\./g, '') .replace(/\/+/g, '/') .replace(/^\//, ''); if (!/^[a-zA-Z0-9-_./]+$/.test(cleaned)) { throw new Error('Invalid path characters'); } return cleaned; } export function sanitizeSqlParam(param) { return param .replace(/'/g, "''") .replace(/;/g, '') .replace(/--/g, '') .replace(/\/\*/g, '') .replace(/\*\//g, ''); } export function validateBreatheApiPath(path) { const validPaths = [ /^\/api\/v\d+\//, /^\/api-docs\//, /^\/swagger/, /^\/openapi/, /^\/api-specs\//, ]; return validPaths.some(pattern => pattern.test(path)); } //# sourceMappingURL=sanitizer.js.map