Files
obsidian-mcp/src/validation/sanitizer.ts
Peter.Morton 466587d1c5 fix: preserve square brackets and escape quotes in note content (v1.1.1)
- Fix square bracket removal: Remove [] from DANGEROUS_CHARS regex
  * Wikilinks ([[link]]) now work correctly
  * Task checkboxes (- [ ] Task) are properly preserved
  * Brackets are safe because values are quoted and passed as array args

- Fix quote truncation: Escape double quotes in formatParam
  * Content like "Bot QM" no longer truncates
  * Internal quotes escaped as \" before wrapping in parameter quotes
  * Prevents shell from misinterpreting quote boundaries

Bump version: 1.0.0 -> 1.1.1

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-10 00:07:13 -05:00

159 lines
4.2 KiB
TypeScript

/**
* 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: Square brackets [] are safe because values are quoted and passed as array args
* They're essential for Obsidian markdown (wikilinks [[link]] and tasks - [ ] Task)
*/
const DANGEROUS_CHARS = /[;&|`$(){}<>]/g;
const COMMAND_INJECTION_PATTERNS = [
/\$\(/g, // Command substitution $(...)
/`[^`]*`/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: Square brackets are safe in paths (quoted args) but removed for consistency
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<string, unknown>): Record<string, unknown> {
const sanitized: Record<string, unknown> = {};
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<string, unknown>);
} 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;
}