Skip to content

Instantly share code, notes, and snippets.

@saleebm
Created August 20, 2025 06:51
Show Gist options
  • Select an option

  • Save saleebm/6fe61a6f2b7f8974e9e62fbf807f14fc to your computer and use it in GitHub Desktop.

Select an option

Save saleebm/6fe61a6f2b7f8974e9e62fbf807f14fc to your computer and use it in GitHub Desktop.
Input sanitizer for cli tools
/**
* Input Sanitizer for CLI Command Execution
*
* This module provides sanitization for user inputs before they are passed
* to CLI commands, preventing command injection attacks.
*/
/**
* Dangerous patterns that should be blocked in inputs
*/
const DANGEROUS_PATTERNS = [
/[;&|`$]/g, // Command separators and substitution
/\.\.\//g, // Directory traversal
/^-/, // Options that could be interpreted as flags
/<|>|>>/g, // Redirection operators
/\n|\r/g, // Newlines that could inject commands
/\$\{.*\}/g, // Template literals
/\$\(.*\)/g, // Command substitution
];
/**
* List of allowed characters for different input types
*/
const ALLOWED_CHARS = {
path: /^[a-zA-Z0-9\-_./\\: ]+$/,
message: /^[a-zA-Z0-9\-_.,:;!?()\[\]{}"'\s]+$/,
identifier: /^[a-zA-Z0-9\-_]+$/,
};
export interface SanitizationOptions {
/** Type of input being sanitized */
type: 'path' | 'message' | 'identifier' | 'custom';
/** Custom allowed pattern for 'custom' type */
allowedPattern?: RegExp;
/** Maximum length of input */
maxLength?: number;
/** Whether to throw on invalid input or return sanitized */
strict?: boolean;
}
/**
* Sanitize user input for CLI command execution
*
* @param input - The raw user input
* @param options - Sanitization options
* @returns Sanitized input safe for CLI execution
* @throws Error if input is malicious and strict mode is enabled
*/
export function sanitizeInput(
input: string,
options: SanitizationOptions
): string {
// Check for null/undefined
if (!input || typeof input !== 'string') {
if (options.strict) {
throw new Error('Invalid input: must be a non-empty string');
}
return '';
}
// Trim whitespace
let sanitized = input.trim();
// Check length
const maxLength = options.maxLength || 1000;
if (sanitized.length > maxLength) {
if (options.strict) {
throw new Error(`Input exceeds maximum length of ${maxLength} characters`);
}
sanitized = sanitized.substring(0, maxLength);
}
// Check for dangerous patterns
for (const pattern of DANGEROUS_PATTERNS) {
if (pattern.test(sanitized)) {
if (options.strict) {
throw new Error(`Potentially dangerous input detected: ${pattern}`);
}
// Remove dangerous characters
sanitized = sanitized.replace(pattern, '');
}
}
// Apply type-specific validation
if (options.type !== 'custom') {
const allowedPattern = ALLOWED_CHARS[options.type];
if (allowedPattern && !allowedPattern.test(sanitized)) {
if (options.strict) {
throw new Error(`Input contains invalid characters for type: ${options.type}`);
}
// Keep only allowed characters
sanitized = sanitized.split('').filter(char =>
allowedPattern.test(char)
).join('');
}
} else if (options.allowedPattern) {
if (!options.allowedPattern.test(sanitized)) {
if (options.strict) {
throw new Error('Input does not match allowed pattern');
}
// Can't safely filter with custom pattern, return empty
return '';
}
}
return sanitized;
}
/**
* Sanitize a file path for safe file system operations
*
* @param path - The file path to sanitize
* @param basePath - Optional base path to restrict access
* @returns Sanitized path
*/
export function sanitizePath(path: string, basePath?: string): string {
const sanitized = sanitizeInput(path, {
type: 'path',
maxLength: 500,
strict: false
});
// Prevent directory traversal
const normalized = sanitized.replace(/\.\.\//g, '').replace(/\.\.$/g, '');
// If base path is provided, ensure the path stays within it
if (basePath) {
const fullPath = `${basePath}/${normalized}`.replace(/\/+/g, '/');
if (!fullPath.startsWith(basePath)) {
throw new Error('Path traversal attempt detected');
}
return fullPath;
}
return normalized;
}
/**
* Sanitize a command message/prompt
*
* @param message - The message to sanitize
* @returns Sanitized message
*/
export function sanitizeMessage(message: string): string {
return sanitizeInput(message, {
type: 'message',
maxLength: 10000,
strict: false
});
}
/**
* Escape shell arguments for safe execution
*
* @param arg - The argument to escape
* @returns Escaped argument safe for shell execution
*/
export function escapeShellArg(arg: string): string {
// For Windows
if (process.platform === 'win32') {
return `"${arg.replace(/"/g, '""')}"`;
}
// For Unix-like systems
return `'${arg.replace(/'/g, "'\\''")}'`;
}
/**
* Validate and sanitize environment variables
*
* @param env - Environment variables object
* @param allowedKeys - List of allowed environment variable keys
* @returns Sanitized environment variables
*/
export function sanitizeEnv(
env: Record<string, string | undefined>,
allowedKeys: string[]
): Record<string, string | undefined> {
const sanitized: Record<string, string | undefined> = {};
for (const key of allowedKeys) {
if (env[key]) {
// Only allow printable ASCII characters in env values
const value = env[key]!.replace(/[^\x20-\x7E]/g, '');
sanitized[key] = value;
}
}
return sanitized;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment