ailock
Version:
AI-Proof File Guard - Protect sensitive files from accidental AI modifications
197 lines • 7.56 kB
JavaScript
import { execa, ExecaError } from 'execa';
import { quote } from 'shell-quote';
import path from 'path';
/**
* Secure command executor that prevents injection attacks and provides
* comprehensive security controls for external command execution
*/
export class SecureCommandExecutor {
defaultTimeout = 30000; // 30 seconds
maxOutputSize = 1024 * 1024; // 1MB
allowedCommands;
constructor(allowedCommands = []) {
// Default allowed commands for file operations
const defaultAllowed = [
'chmod', 'chattr', 'chflags', 'attrib', 'icacls',
'xattr', 'getfattr', 'setfattr', 'lsattr',
'git', 'ls', 'stat', 'test', 'find'
];
this.allowedCommands = new Set([...defaultAllowed, ...allowedCommands]);
}
/**
* Executes a command securely with argument validation and output limits
*/
async executeCommand(command, args, options = {}) {
// Validate command is allowed
this.validateCommand(command);
// Validate and sanitize arguments
const sanitizedArgs = this.sanitizeArguments(args);
// Prepare execution options
const execOptions = {
timeout: options.timeout || this.defaultTimeout,
cwd: options.cwd ? path.resolve(options.cwd) : process.cwd(),
env: this.createSecureEnvironment(options.env),
input: options.input,
stdio: 'pipe',
shell: false, // CRITICAL: Never use shell
stripFinalNewline: false,
buffer: true,
maxBuffer: options.maxOutputSize || this.maxOutputSize,
windowsHide: true, // Hide window on Windows
killSignal: 'SIGTERM',
};
try {
const result = await execa(command, sanitizedArgs, execOptions);
return {
stdout: String(result.stdout || ''),
stderr: String(result.stderr || ''),
exitCode: result.exitCode || 0,
command,
args: sanitizedArgs
};
}
catch (error) {
if (error instanceof ExecaError) {
// Handle specific execution errors
if (error.timedOut) {
throw new Error(`Command timed out after ${execOptions.timeout}ms: ${command}`);
}
if (error.signal) {
throw new Error(`Command was killed with signal ${error.signal}: ${command}`);
}
if (error.exitCode !== undefined) {
return {
stdout: String(error.stdout || ''),
stderr: String(error.stderr || ''),
exitCode: error.exitCode,
command,
args: sanitizedArgs
};
}
}
throw new Error(`Command execution failed: ${error}`);
}
}
/**
* Executes a command and returns only success/failure
*/
async executeCommandSilent(command, args, options = {}) {
try {
const result = await this.executeCommand(command, args, options);
return result.exitCode === 0;
}
catch (error) {
return false;
}
}
/**
* Creates a safe shell command string for logging purposes only
* DO NOT USE THIS FOR ACTUAL EXECUTION
*/
createSafeCommandString(command, args) {
return quote([command, ...args]);
}
/**
* Validates that a command is in the allowed list
*/
validateCommand(command) {
if (!command || typeof command !== 'string') {
throw new Error('Invalid command: must be a non-empty string');
}
// Extract base command name without path
const baseCommand = path.basename(command);
if (!this.allowedCommands.has(baseCommand)) {
throw new Error(`Command not allowed: ${baseCommand}`);
}
// Additional validation for command paths
if (command.includes('/') || command.includes('\\')) {
// Allow only commands in system PATH or specific trusted locations
const trustedPaths = [
'/bin', '/usr/bin', '/usr/local/bin',
'/sbin', '/usr/sbin',
'C:\\Windows\\System32', 'C:\\Windows\\SysWOW64'
];
const commandDir = path.dirname(path.resolve(command));
const isTrusted = trustedPaths.some(trusted => commandDir.startsWith(trusted));
if (!isTrusted) {
throw new Error(`Command path not trusted: ${command}`);
}
}
}
/**
* Sanitizes command arguments to prevent injection
*/
sanitizeArguments(args) {
if (!Array.isArray(args)) {
throw new Error('Arguments must be an array');
}
return args.map((arg, index) => {
if (typeof arg !== 'string') {
throw new Error(`Argument ${index} must be a string, got ${typeof arg}`);
}
// Check for dangerous characters that could be used for injection
const dangerousChars = /[;&|`$(){}[\]<>]/;
if (dangerousChars.test(arg)) {
throw new Error(`Dangerous characters in argument ${index}: ${arg}`);
}
// Limit argument length
if (arg.length > 4096) {
throw new Error(`Argument ${index} too long: maximum 4096 characters`);
}
return arg;
});
}
/**
* Creates a secure environment for command execution
*/
createSecureEnvironment(customEnv) {
// Start with minimal environment
const secureEnv = {
PATH: process.env.PATH || '',
HOME: process.env.HOME || '',
USER: process.env.USER || '',
LANG: process.env.LANG || 'C',
};
// Add platform-specific environment variables
if (process.platform === 'win32') {
secureEnv.SYSTEMROOT = process.env.SYSTEMROOT || 'C:\\Windows';
secureEnv.WINDIR = process.env.WINDIR || 'C:\\Windows';
secureEnv.USERPROFILE = process.env.USERPROFILE || '';
}
// Merge custom environment variables (with validation)
if (customEnv) {
for (const [key, value] of Object.entries(customEnv)) {
if (typeof key !== 'string' || typeof value !== 'string') {
throw new Error(`Invalid environment variable: ${key}=${value}`);
}
// Prevent environment variable injection
if (key.includes('=') || key.includes('\0') || value.includes('\0')) {
throw new Error(`Dangerous environment variable: ${key}=${value}`);
}
secureEnv[key] = value;
}
}
return secureEnv;
}
/**
* Adds an allowed command to the whitelist
*/
addAllowedCommand(command) {
if (command && typeof command === 'string') {
this.allowedCommands.add(path.basename(command));
}
}
/**
* Removes a command from the whitelist
*/
removeAllowedCommand(command) {
this.allowedCommands.delete(path.basename(command));
}
/**
* Gets the list of allowed commands
*/
getAllowedCommands() {
return Array.from(this.allowedCommands).sort();
}
}
//# sourceMappingURL=CommandExecutor.js.map