Exposure of Information Through Shell Error Message

Description

Exposure of Information Through Shell Error Message is a vulnerability where a command shell error message indicates that an unhandled exception exists in the web application code, potentially exposing sensitive system information. When applications invoke shell commands and fail to properly handle errors, the raw shell error messages may be returned to users. These messages can reveal file paths, system configuration, usernames, command syntax, and other internal details that help attackers understand the system architecture and identify potential attack vectors.

Risk

Shell error messages can expose critical system information to attackers. File paths reveal directory structures and installation locations. Command syntax errors show which programs are being invoked and how. Permission errors indicate security configurations. Missing file errors reveal expected system state. Attackers can leverage this information to craft targeted attacks, exploit specific software versions, or understand the execution environment. In many cases, error conditions that trigger these messages can be deliberately induced by malformed input, enabling active reconnaissance.

Solution

Implement comprehensive error handling for all shell command executions. Catch all exceptions and return generic, user-friendly error messages that don't reveal system internals. Log detailed error information server-side for debugging while presenting sanitized messages to users. Use whitelisting for shell command inputs and validate all parameters. Consider using native libraries instead of shell commands when possible. Implement proper exception handling wrappers around command execution code. Test error handling with various failure scenarios to ensure no information leaks.

Common Consequences

ImpactDetails
ConfidentialityScope: Confidentiality

Read Application Data - Shell error messages can expose sensitive application and system information including file paths, usernames, software versions, and configuration details.

Example Code

Vulnerable Code

<?php
// Vulnerable: Shell command with exposed errors
function vulnerableBackup($filename) {
    // Vulnerable: Direct shell execution
    $output = shell_exec("tar czf backup.tar.gz " . $filename);

    // Vulnerable: Shell errors returned directly to user
    if ($output === null) {
        // Error message from shell is displayed
        echo "Error: Command failed";
        echo shell_exec("tar czf backup.tar.gz " . $filename . " 2>&1");
    }

    return $output;
}

// When called with invalid input:
// tar: nonexistent_file: Cannot stat: No such file or directory
// tar: Error is not recoverable: exiting now
// Reveals: file system paths, tar command usage

// Vulnerable: exec() with error output
function vulnerableSearch($query) {
    $cmd = "grep -r '" . $query . "' /var/www/data/";
    exec($cmd, $output, $return_var);

    if ($return_var !== 0) {
        // Vulnerable: Shows shell error to user
        echo "Search failed: ";
        system($cmd . " 2>&1");
        // Reveals: directory structure, grep command syntax
    }

    return $output;
}
?>
# Vulnerable: Python subprocess with exposed errors
import subprocess

def vulnerable_convert(input_file, output_file):
    try:
        # Vulnerable: shell=True allows command injection
        cmd = f"convert {input_file} {output_file}"
        result = subprocess.run(cmd, shell=True, capture_output=True, text=True)

        if result.returncode != 0:
            # Vulnerable: Raw stderr returned to user
            return f"Conversion failed: {result.stderr}"
            # Could reveal:
            # convert: unable to open image '/var/www/uploads/test.jpg': No such file or directory
            # convert: no images defined 'output.png'

    except Exception as e:
        # Vulnerable: Full exception details exposed
        return f"Error: {str(e)}"

def vulnerable_ping(host):
    import os
    # Vulnerable: os.popen with error exposure
    result = os.popen(f"ping -c 1 {host} 2>&1").read()
    return result
    # Could reveal:
    # ping: unknown host malicious-input; cat /etc/passwd
    # sh: syntax error near unexpected token
// Vulnerable: Java Runtime.exec with error exposure
public class VulnerableCommandExecutor {

    public String executeCommand(String filename) {
        try {
            // Vulnerable: User input in command
            String command = "ls -la " + filename;
            Process process = Runtime.getRuntime().exec(command);

            BufferedReader errorReader = new BufferedReader(
                new InputStreamReader(process.getErrorStream())
            );

            StringBuilder errors = new StringBuilder();
            String line;
            while ((line = errorReader.readLine()) != null) {
                errors.append(line).append("\n");
            }

            if (process.waitFor() != 0) {
                // Vulnerable: Shell errors returned to caller
                return "Error executing command: " + errors.toString();
                // Could reveal:
                // ls: cannot access '/etc/shadow': Permission denied
                // ls: cannot access '/nonexistent': No such file or directory
            }

            return "Success";
        } catch (Exception e) {
            // Vulnerable: Exception details exposed
            return "Command execution failed: " + e.getMessage();
        }
    }
}
// Vulnerable: Node.js child_process with error exposure
const { exec, execSync } = require('child_process');

function vulnerableCommand(userInput) {
    return new Promise((resolve, reject) => {
        // Vulnerable: User input in shell command
        exec(`cat ${userInput}`, (error, stdout, stderr) => {
            if (error) {
                // Vulnerable: Shell error returned to user
                resolve({
                    success: false,
                    error: stderr,
                    // Could reveal:
                    // cat: /etc/shadow: Permission denied
                    // cat: /nonexistent: No such file or directory
                    code: error.code
                });
                return;
            }
            resolve({ success: true, data: stdout });
        });
    });
}

function vulnerableSyncCommand(filename) {
    try {
        // Vulnerable: Sync exec with error in exception
        const result = execSync(`file ${filename}`, { encoding: 'utf8' });
        return result;
    } catch (error) {
        // Vulnerable: Full error details exposed
        return `Command failed: ${error.stderr}`;
    }
}

Fixed Code

<?php
// Fixed: Secure command execution with error handling
function secureBackup($filename) {
    // Fixed: Validate filename
    if (!preg_match('/^[a-zA-Z0-9_.-]+$/', $filename)) {
        return ['success' => false, 'error' => 'Invalid filename'];
    }

    // Fixed: Use escapeshellarg
    $safeFilename = escapeshellarg($filename);

    // Fixed: Capture errors internally
    $descriptors = [
        0 => ['pipe', 'r'],
        1 => ['pipe', 'w'],
        2 => ['pipe', 'w']  // stderr
    ];

    $process = proc_open(
        "tar czf backup.tar.gz {$safeFilename}",
        $descriptors,
        $pipes
    );

    if (is_resource($process)) {
        $stdout = stream_get_contents($pipes[1]);
        $stderr = stream_get_contents($pipes[2]);
        $returnCode = proc_close($process);

        if ($returnCode !== 0) {
            // Fixed: Log error internally
            error_log("Backup command failed: " . $stderr);

            // Fixed: Return generic message to user
            return [
                'success' => false,
                'error' => 'Backup operation failed. Please contact support.'
            ];
        }

        return ['success' => true, 'message' => 'Backup created successfully'];
    }

    return ['success' => false, 'error' => 'Unable to start backup process'];
}

// Fixed: Secure search function
function secureSearch($query) {
    // Fixed: Whitelist validation
    if (!preg_match('/^[a-zA-Z0-9\s]+$/', $query)) {
        return ['error' => 'Invalid search query'];
    }

    $safeQuery = escapeshellarg($query);
    $safeDir = '/var/www/data/';

    $cmd = "grep -r {$safeQuery} " . escapeshellarg($safeDir);
    exec($cmd . " 2>/dev/null", $output, $returnCode);

    if ($returnCode !== 0) {
        // Fixed: Generic error, detailed log
        error_log("Search failed with code $returnCode");
        return ['results' => [], 'message' => 'No results found'];
    }

    return ['results' => $output];
}
?>
# Fixed: Python subprocess with secure error handling
import subprocess
import logging
import re

logger = logging.getLogger(__name__)

def secure_convert(input_file, output_file):
    # Fixed: Validate filenames
    if not re.match(r'^[\w\-./]+$', input_file) or \
       not re.match(r'^[\w\-./]+$', output_file):
        return {'success': False, 'error': 'Invalid filename'}

    try:
        # Fixed: Use list arguments, not shell=True
        result = subprocess.run(
            ['convert', input_file, output_file],
            capture_output=True,
            text=True,
            timeout=30
        )

        if result.returncode != 0:
            # Fixed: Log details, return generic message
            logger.error(f"Convert failed: {result.stderr}")
            return {
                'success': False,
                'error': 'Image conversion failed. Please check the file format.'
            }

        return {'success': True, 'message': 'Conversion successful'}

    except subprocess.TimeoutExpired:
        logger.error("Convert command timed out")
        return {'success': False, 'error': 'Operation timed out'}

    except FileNotFoundError:
        logger.error("Convert command not found")
        return {'success': False, 'error': 'Conversion service unavailable'}

    except Exception as e:
        # Fixed: Never expose exception details
        logger.exception("Unexpected error in convert")
        return {'success': False, 'error': 'An unexpected error occurred'}


def secure_ping(host):
    # Fixed: Validate host format
    import ipaddress

    try:
        # Try to parse as IP address
        ipaddress.ip_address(host)
    except ValueError:
        # Validate as hostname
        if not re.match(r'^[a-zA-Z0-9][a-zA-Z0-9\-.]{0,61}[a-zA-Z0-9]$', host):
            return {'success': False, 'error': 'Invalid host format'}

    try:
        result = subprocess.run(
            ['ping', '-c', '1', '-W', '5', host],
            capture_output=True,
            text=True,
            timeout=10
        )

        if result.returncode == 0:
            return {'success': True, 'message': 'Host is reachable'}
        else:
            # Fixed: Generic message only
            return {'success': False, 'error': 'Host unreachable'}

    except Exception as e:
        logger.error(f"Ping failed: {e}")
        return {'success': False, 'error': 'Network check failed'}
// Fixed: Java secure command execution
import java.util.logging.Logger;
import java.util.regex.Pattern;

public class SecureCommandExecutor {
    private static final Logger logger = Logger.getLogger(
        SecureCommandExecutor.class.getName()
    );
    private static final Pattern SAFE_FILENAME = Pattern.compile("^[\\w\\-./]+$");

    public CommandResult executeCommand(String filename) {
        // Fixed: Validate input
        if (!SAFE_FILENAME.matcher(filename).matches()) {
            return new CommandResult(false, "Invalid filename format");
        }

        try {
            // Fixed: Use ProcessBuilder with list arguments
            ProcessBuilder pb = new ProcessBuilder("ls", "-la", filename);
            pb.redirectErrorStream(false);

            Process process = pb.start();

            // Fixed: Read stderr but don't expose
            String stderr = readStream(process.getErrorStream());
            String stdout = readStream(process.getInputStream());

            int exitCode = process.waitFor();

            if (exitCode != 0) {
                // Fixed: Log internally, return generic message
                logger.warning("Command failed: " + stderr);
                return new CommandResult(false, "File operation failed");
            }

            return new CommandResult(true, "Operation completed successfully");

        } catch (IOException e) {
            logger.log(Level.SEVERE, "IO error in command execution", e);
            return new CommandResult(false, "System error occurred");

        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            logger.warning("Command interrupted");
            return new CommandResult(false, "Operation cancelled");
        }
    }

    private String readStream(InputStream stream) throws IOException {
        BufferedReader reader = new BufferedReader(new InputStreamReader(stream));
        StringBuilder sb = new StringBuilder();
        String line;
        while ((line = reader.readLine()) != null) {
            sb.append(line).append("\n");
        }
        return sb.toString();
    }
}
// Fixed: Node.js secure command execution
const { execFile } = require('child_process');
const path = require('path');

function secureCommand(userInput) {
    return new Promise((resolve) => {
        // Fixed: Validate input
        if (!/^[\w\-./]+$/.test(userInput)) {
            resolve({
                success: false,
                error: 'Invalid input format'
            });
            return;
        }

        // Fixed: Use execFile with arguments array
        execFile('cat', [userInput], { timeout: 5000 }, (error, stdout, stderr) => {
            if (error) {
                // Fixed: Log error, return generic message
                console.error('Command error:', stderr);

                resolve({
                    success: false,
                    error: 'Unable to read file'
                });
                return;
            }

            resolve({ success: true, data: stdout });
        });
    });
}

// Fixed: Error handling wrapper
function safeExec(command, args, options = {}) {
    return new Promise((resolve, reject) => {
        execFile(command, args, {
            timeout: options.timeout || 10000,
            maxBuffer: options.maxBuffer || 1024 * 1024
        }, (error, stdout, stderr) => {
            if (error) {
                // Fixed: Structured error without shell details
                console.error(`${command} failed:`, stderr);
                resolve({
                    success: false,
                    code: 'COMMAND_FAILED',
                    message: 'Operation could not be completed'
                });
                return;
            }
            resolve({ success: true, output: stdout });
        });
    });
}

CVE Examples

No specific CVEs are listed in the MITRE database for this CWE. However, the vulnerability pattern is documented in:

  • General error handling best practices
  • Web application security testing guidelines

References

  1. MITRE Corporation. "CWE-535: Exposure of Information Through Shell Error Message." https://cwe.mitre.org/data/definitions/535.html
  2. OWASP. "Error Handling Cheat Sheet."
  3. OWASP. "OS Command Injection Prevention."