/** * Parameter Sanitizer * Constitutional Principle III: Local Execution Security * Remove dangerous characters and validate inputs before CLI execution */ import { logger } from '../utils/logger.js'; /** * Characters that should be removed or escaped for security * Note: Brackets [], parentheses (), and braces {} are safe because values are quoted and passed as array args * They're essential for Obsidian markdown (wikilinks [[link]], tasks - [ ] Task, templates {{...}}, etc.) * Note: Single & is safe in quoted args (filenames like "Research & Development.md") * Note: Backticks are safe because formatParam escapes them as \` inside double-quoted strings, * preventing shell command substitution while preserving Markdown code fences (``` ```) * Note: < and > are safe inside double-quoted strings — shell redirection only applies at the * command level, not inside quotes. Stripping them breaks Mermaid arrows (->>, -->) and HTML. * We only block: ; | $ (command separators, pipes, variable substitution) * Command injection patterns (&&, ||, etc.) are handled separately */ const DANGEROUS_CHARS = /[;|$]/g; const COMMAND_INJECTION_PATTERNS = [ /\$\(/g, // Command substitution $(...) /\|\|/g, // OR operator /&&/g, // AND operator /;/g, // Command separator ]; /** * Sanitize a single string parameter */ export function sanitizeString(input: string): string { if (typeof input !== 'string') { logger.warn('sanitizeString received non-string input', { type: typeof input }); return String(input); } // Remove null bytes let sanitized = input.replace(/\0/g, ''); // Check for command injection patterns for (const pattern of COMMAND_INJECTION_PATTERNS) { if (pattern.test(sanitized)) { logger.warn('Potential command injection detected', { pattern: pattern.toString(), }); sanitized = sanitized.replace(pattern, ''); } } // Remove dangerous characters sanitized = sanitized.replace(DANGEROUS_CHARS, ''); // Trim whitespace sanitized = sanitized.trim(); return sanitized; } /** * Sanitize a file path */ export function sanitizePath(path: string): string { if (typeof path !== 'string') { return ''; } // Remove null bytes let sanitized = path.replace(/\0/g, ''); // Remove path traversal attempts sanitized = sanitized.replace(/\.\./g, ''); // Remove leading/trailing slashes (relative paths only) sanitized = sanitized.replace(/^\/+|\/+$/g, ''); // Remove dangerous characters but allow path separators // Note: Brackets, parentheses, braces, and single & are safe in paths (quoted args) // Note: < and > are safe inside double-quoted strings (not shell redirects) sanitized = sanitized.replace(/[;|`$]/g, ''); return sanitized; } /** * Sanitize a tag (must start with #) */ export function sanitizeTag(tag: string): string { if (typeof tag !== 'string') { return '#'; } let sanitized = tag.trim(); // Ensure tag starts with # if (!sanitized.startsWith('#')) { sanitized = '#' + sanitized; } // Allow only alphanumeric, hyphens, underscores, and forward slashes sanitized = sanitized.replace(/[^#a-zA-Z0-9_/-]/g, ''); // Remove empty tags if (sanitized === '#') { return ''; } return sanitized; } /** * Sanitize object parameters recursively */ export function sanitizeParameters(params: Record): Record { const sanitized: Record = {}; for (const [key, value] of Object.entries(params)) { if (typeof value === 'string') { // Special handling for different parameter types if (key === 'tag' || key.startsWith('tag')) { sanitized[key] = sanitizeTag(value); } else if (key === 'path' || key.endsWith('Path') || key.endsWith('path')) { sanitized[key] = sanitizePath(value); } else { sanitized[key] = sanitizeString(value); } } else if (typeof value === 'object' && value !== null && !Array.isArray(value)) { // Recursively sanitize nested objects sanitized[key] = sanitizeParameters(value as Record); } else if (Array.isArray(value)) { // Sanitize array elements sanitized[key] = value.map((item) => typeof item === 'string' ? sanitizeString(item) : item ); } else { // Keep other types as-is (numbers, booleans, null) sanitized[key] = value; } } return sanitized; } /** * Validate that parameters don't contain shell metacharacters */ export function containsDangerousCharacters(input: string): boolean { if (typeof input !== 'string') { return false; } // Check for dangerous characters if (DANGEROUS_CHARS.test(input)) { return true; } // Check for command injection patterns for (const pattern of COMMAND_INJECTION_PATTERNS) { if (pattern.test(input)) { return true; } } return false; }