agent-hub-mcp
Version:
Universal AI agent coordination platform based on Model Context Protocol (MCP)
224 lines (178 loc) • 5.55 kB
text/typescript
import { realpathSync } from 'fs';
import { resolve } from 'path';
import { MessagePriority, MessageType } from '~/types';
/**
* Security validation utilities for input sanitization
*/
/**
* Validates context values to prevent injection
*/
export function validateContextValue(value: unknown): any {
if (value === null || value === undefined) {
return value;
}
// Deep sanitize by JSON serialization (automatically handles prototype pollution)
return JSON.parse(JSON.stringify(value));
}
/**
* Validates agent IDs and similar identifiers
*/
export function validateIdentifier(value: unknown, fieldName: string): string {
return validateString(value, fieldName, {
required: true,
maxLength: 100,
pattern: /^[\w-]+$/,
});
}
/**
* Validates message priority
*/
export function validateMessagePriority(value: unknown): MessagePriority | undefined {
if (!value) {
return undefined;
}
if (typeof value !== 'string') {
throw new TypeError('Priority must be a string');
}
const validPriorities = Object.values(MessagePriority);
if (!validPriorities.includes(value as MessagePriority)) {
throw new Error(`Invalid priority: ${value}`);
}
return value as MessagePriority;
}
/**
* Validates message type
*/
export function validateMessageType(value: unknown): MessageType {
if (!value || typeof value !== 'string') {
throw new Error('Invalid message type');
}
const validTypes = Object.values(MessageType);
if (!validTypes.includes(value as MessageType)) {
throw new Error(`Invalid message type: ${value}`);
}
return value as MessageType;
}
/**
* Validates and sanitizes metadata objects
*/
export function validateMetadata(value: unknown): Record<string, any> | undefined {
if (!value) {
return undefined;
}
if (typeof value !== 'object' || Array.isArray(value)) {
throw new TypeError('Metadata must be an object');
}
const metadata = value as Record<string, any>;
// Limit metadata size
const keys = Object.keys(metadata);
if (keys.length > 20) {
throw new Error('Metadata cannot have more than 20 properties');
}
// Sanitize metadata to prevent prototype pollution
const sanitized: Record<string, any> = {};
for (const [key, value_] of Object.entries(metadata)) {
// Skip dangerous keys
if (key === '__proto__' || key === 'constructor' || key === 'prototype') {
continue;
}
// Validate key
if (!/^[\w-]+$/.test(key)) {
throw new Error(`Invalid metadata key: ${key}`);
}
// Deep copy and sanitize value
sanitized[key] = JSON.parse(JSON.stringify(value_));
}
return sanitized;
}
/**
* Validates project paths to prevent directory traversal
*/
export function validateProjectPath(path: string): string {
if (!path || typeof path !== 'string') {
throw new Error('Invalid project path');
}
// Prevent directory traversal
if (path.includes('..') || path.includes('~')) {
throw new Error('Invalid project path: directory traversal detected');
}
// Only allow specific safe directories
const allowedPrefixes = [
'/Users/',
'/home/',
'/var/www/',
'/opt/',
'/workspace/',
'/tmp/',
process.cwd(), // Current working directory
];
// Resolve relative paths to absolute paths for proper validation
const resolvedPath = resolve(path);
// Resolve symlinks to get the real path and prevent directory escape via symlinks
let realPath: string;
try {
realPath = realpathSync(resolvedPath);
} catch {
// If real path resolution fails (broken symlink, permission issues), use resolved path
// but add a warning that this could be a security risk
realPath = resolvedPath;
}
// Validate both the resolved path and the real path to prevent symlink-based escapes
const pathsToValidate = [resolvedPath, realPath];
for (const pathToCheck of pathsToValidate) {
const isAllowed = allowedPrefixes.some(prefix => pathToCheck.startsWith(prefix));
if (!isAllowed) {
throw new Error(`Project path must be in an allowed directory. Resolved to: ${pathToCheck}`);
}
}
return realPath;
}
/**
* Validates string inputs to prevent injection attacks
*/
export function validateString(
value: unknown,
fieldName: string,
options: {
maxLength?: number;
minLength?: number;
pattern?: RegExp;
required?: boolean;
} = {},
): string {
const { maxLength = 1000, minLength = 1, pattern, required = true } = options;
if (value === undefined || value === null) {
if (required) {
throw new Error(`${fieldName} is required`);
}
return '';
}
if (typeof value !== 'string') {
throw new TypeError(`${fieldName} must be a string`);
}
const trimmed = value.trim();
if (required && trimmed.length < minLength) {
throw new Error(`${fieldName} must be at least ${minLength} characters`);
}
if (trimmed.length > maxLength) {
throw new Error(`${fieldName} must not exceed ${maxLength} characters`);
}
if (pattern && !pattern.test(trimmed)) {
throw new Error(`${fieldName} contains invalid characters`);
}
// Check for common injection patterns
const dangerousPatterns = [
/<script/i,
/javascript:/i,
/on\w+\s*=/i, // onclick, onload, etc.
/__proto__/,
/constructor\[/,
/prototype\[/,
];
for (const dangerous of dangerousPatterns) {
if (dangerous.test(trimmed)) {
throw new Error(`${fieldName} contains potentially malicious content`);
}
}
return trimmed;
}