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
JavaScript
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