Ban proc_open() and exec() entirely - replace with shell_exec() and file redirection
🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -19,7 +19,7 @@ class ExecUsage_CodeQualityRule extends CodeQualityRule_Abstract
|
||||
|
||||
public function get_description(): string
|
||||
{
|
||||
return 'Prohibits exec() function due to silent output truncation - requires proc_open() or shell_exec()';
|
||||
return 'Bans exec() function entirely due to unfixable output truncation - use shell_exec() instead';
|
||||
}
|
||||
|
||||
public function get_file_patterns(): array
|
||||
@@ -43,7 +43,7 @@ class ExecUsage_CodeQualityRule extends CodeQualityRule_Abstract
|
||||
* - Error messages are incomplete
|
||||
* - No error/exception is thrown - the truncation is SILENT
|
||||
*
|
||||
* Requires proc_open() (for return code validation) or shell_exec() (simple cases).
|
||||
* exec() is completely banned - use shell_exec() instead.
|
||||
*/
|
||||
public function check(string $file_path, string $contents, array $metadata = []): void
|
||||
{
|
||||
@@ -82,111 +82,54 @@ class ExecUsage_CodeQualityRule extends CodeQualityRule_Abstract
|
||||
if (preg_match('/\bexec\s*\(/i', $sanitized_line)) {
|
||||
$original_line = $original_lines[$line_num] ?? $sanitized_line;
|
||||
|
||||
$violation_message = "🚨 CRITICAL: exec() function detected - causes SILENT OUTPUT TRUNCATION
|
||||
$violation_message = "🚨 CRITICAL: exec() is BANNED - use shell_exec() instead
|
||||
|
||||
exec() has a fundamental flaw: it reads command output LINE-BY-LINE into an array, which:
|
||||
exec() has an unfixable flaw: it reads command output LINE-BY-LINE into an array, which:
|
||||
- Hits memory/buffer limits on large outputs (>1MB typical)
|
||||
- Silently truncates output without throwing errors or exceptions
|
||||
- Causes catastrophic failures in compilation, bundling, and error reporting
|
||||
- Makes debugging impossible (you see partial output with no indication of truncation)
|
||||
- Makes debugging impossible (partial output with no indication of truncation)
|
||||
|
||||
Real-world example from this codebase:
|
||||
- jqhtml compilation of 220-line template was truncated at row 4 (mid-line)
|
||||
- jqhtml compilation truncated at row 4 (mid-line) - output was 4KB instead of 35KB
|
||||
- No error thrown, no indication of failure
|
||||
- Took hours to diagnose because the truncation was SILENT
|
||||
- Fixed by replacing exec() with proc_open() - output jumped from 4KB to 35KB
|
||||
|
||||
This is why exec() is BANNED across the entire application.";
|
||||
exec() is completely banned with NO EXCEPTIONS. Use shell_exec() instead.";
|
||||
|
||||
$resolution = "REQUIRED ACTION - Choose based on your needs:
|
||||
$resolution = "REQUIRED ACTION - Replace exec() with shell_exec():
|
||||
|
||||
QUICKEST FIX (Drop-in replacement - no refactoring needed):
|
||||
Use \exec_safe() - RSpade framework helper with identical signature to exec():
|
||||
|
||||
\exec_safe(\$command, \$output, \$return_var);
|
||||
|
||||
Simply replace exec() with \exec_safe(). That's it. No other code changes needed.
|
||||
Uses proc_open() internally to handle unlimited output without truncation.
|
||||
|
||||
Example:
|
||||
// Before:
|
||||
exec('git status 2>&1', \$output, \$code);
|
||||
|
||||
// After:
|
||||
\exec_safe('git status 2>&1', \$output, \$code);
|
||||
|
||||
Benefits:
|
||||
- Zero refactoring - maintains exact same signature as exec()
|
||||
- All existing code continues to work identically
|
||||
- No silent truncation (uses proc_open() internally)
|
||||
- Framework helper function available everywhere
|
||||
|
||||
FOR ADVANCED USERS (Need full control):
|
||||
Use proc_open() which streams unlimited output without size limits:
|
||||
|
||||
\$descriptors = [
|
||||
0 => ['pipe', 'r'], // stdin
|
||||
1 => ['pipe', 'w'], // stdout
|
||||
2 => ['pipe', 'w'] // stderr
|
||||
];
|
||||
|
||||
\$process = proc_open(\$command, \$descriptors, \$pipes);
|
||||
|
||||
if (!is_resource(\$process)) {
|
||||
throw new \\RuntimeException(\"Failed to execute command\");
|
||||
}
|
||||
|
||||
fclose(\$pipes[0]); // Close stdin
|
||||
|
||||
\$output_str = stream_get_contents(\$pipes[1]); // Read all stdout
|
||||
\$error_str = stream_get_contents(\$pipes[2]); // Read all stderr
|
||||
|
||||
fclose(\$pipes[1]);
|
||||
fclose(\$pipes[2]);
|
||||
|
||||
\$return_code = proc_close(\$process);
|
||||
|
||||
// Combine stderr with stdout if command failed
|
||||
if (\$return_code !== 0 && !empty(\$error_str)) {
|
||||
\$output_str = \$error_str . \"\\n\" . \$output_str;
|
||||
}
|
||||
|
||||
if (\$return_code !== 0) {
|
||||
throw new \\RuntimeException(\"Command failed: {\$output_str}\");
|
||||
}
|
||||
|
||||
Benefits:
|
||||
- Streams unlimited output (no size limits)
|
||||
- Separate stdout/stderr streams
|
||||
- Proper return code validation
|
||||
- No silent truncation
|
||||
|
||||
FOR SIMPLE CASES (Don't need return code):
|
||||
Use shell_exec() for commands where you only need output:
|
||||
|
||||
\$output = shell_exec(\$command);
|
||||
BASIC USAGE (don't need return code):
|
||||
\$output = shell_exec(\$command . ' 2>&1');
|
||||
|
||||
if (\$output === null) {
|
||||
throw new \\RuntimeException(\"Command failed\");
|
||||
throw new \\RuntimeException('Command failed');
|
||||
}
|
||||
|
||||
Benefits:
|
||||
- Simple one-line replacement
|
||||
- Returns entire output as string (no line-by-line buffering)
|
||||
- No silent truncation
|
||||
- Drawback: Cannot get return code (assumes success if output is not null)
|
||||
ADVANCED USAGE (need return code):
|
||||
Use the echo \$? trick to capture exit code:
|
||||
|
||||
RATIONALE:
|
||||
exec() was designed for simple command execution in the early PHP days. Modern PHP
|
||||
applications with large compilation outputs, bundling systems, and complex toolchains
|
||||
need proper stream handling. Both proc_open() and shell_exec() handle
|
||||
unlimited output correctly - exec() does not.
|
||||
\$full_command = \"(\$command) 2>&1; echo \$?\";
|
||||
\$result = shell_exec(\$full_command);
|
||||
|
||||
NEVER use exec() for:
|
||||
- Compilation outputs (esbuild, webpack, babel, jqhtml)
|
||||
- Bundler commands
|
||||
- Any command with potentially large output (>100 lines)
|
||||
- Commands where you need to see complete error messages";
|
||||
// Last line is the exit code
|
||||
\$lines = explode(\"\\n\", trim(\$result));
|
||||
\$return_code = (int)array_pop(\$lines);
|
||||
\$output = implode(\"\\n\", \$lines);
|
||||
|
||||
if (\$return_code !== 0) {
|
||||
throw new \\RuntimeException(\"Command failed: \$output\");
|
||||
}
|
||||
|
||||
WHY THIS WORKS:
|
||||
- shell_exec() returns ALL output as a string (no line-by-line buffering)
|
||||
- No size limits, no truncation, no pipe buffer issues
|
||||
- Simple and reliable
|
||||
|
||||
IMPORTANT NOTES:
|
||||
- Do NOT use proc_open() - it's also banned (see PHP-PROC-01)
|
||||
- Do NOT try to use exec() with file redirection - just use shell_exec()
|
||||
- shell_exec() is the ONLY approved way to execute shell commands";
|
||||
|
||||
$this->add_violation(
|
||||
$file_path,
|
||||
|
||||
Reference in New Issue
Block a user