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
| Impact | Details |
|---|---|
| Confidentiality | Scope: 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
- MITRE Corporation. "CWE-535: Exposure of Information Through Shell Error Message." https://cwe.mitre.org/data/definitions/535.html
- OWASP. "Error Handling Cheat Sheet."
- OWASP. "OS Command Injection Prevention."