Files
rspade_system/app/RSpade/CodeQuality/Rules/PHP/ExecUsage_CodeQualityRule.php
root 29c657f7a7 Exclude tests directory from framework publish
Add 100+ automated unit tests from .expect file specifications
Add session system test
Add rsx:constants:regenerate command test
Add rsx:logrotate command test
Add rsx:clean command test
Add rsx:manifest:stats command test
Add model enum system test
Add model mass assignment prevention test
Add rsx:check command test
Add migrate:status command test

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-25 03:59:58 +00:00

146 lines
4.7 KiB
PHP

<?php
namespace App\RSpade\CodeQuality\Rules\PHP;
use App\RSpade\CodeQuality\Rules\CodeQualityRule_Abstract;
use App\RSpade\CodeQuality\Support\FileSanitizer;
class ExecUsage_CodeQualityRule extends CodeQualityRule_Abstract
{
public function get_id(): string
{
return 'PHP-EXEC-01';
}
public function get_name(): string
{
return 'exec() Usage Check';
}
public function get_description(): string
{
return 'Bans exec() function entirely due to unfixable output truncation - use shell_exec() instead';
}
public function get_file_patterns(): array
{
return ['*.php'];
}
public function get_default_severity(): string
{
return 'critical';
}
/**
* Check PHP file for exec() usage
*
* exec() has a critical limitation: it reads command output line-by-line into an array,
* which can cause silent truncation for large outputs or hit memory/buffer limits.
*
* This causes catastrophic failures where:
* - Compilation output gets truncated mid-line
* - Error messages are incomplete
* - No error/exception is thrown - the truncation is SILENT
*
* exec() is completely banned - use shell_exec() instead.
*/
public function check(string $file_path, string $contents, array $metadata = []): void
{
// Skip vendor directories
if (str_contains($file_path, '/vendor/')) {
return;
}
// Skip CodeQuality directory
if (str_contains($file_path, '/CodeQuality/')) {
return;
}
// Skip InspectCommand.php - it documents what the checks do
if (str_contains($file_path, 'InspectCommand.php')) {
return;
}
// Get both original and sanitized content
$original_content = file_get_contents($file_path);
$original_lines = explode("\n", $original_content);
// Get sanitized content with comments and strings removed
$sanitized_data = FileSanitizer::sanitize_php($contents);
$sanitized_lines = $sanitized_data['lines'];
foreach ($sanitized_lines as $line_num => $sanitized_line) {
$line_number = $line_num + 1;
// Skip if the line is empty in sanitized version (was a comment)
if (trim($sanitized_line) === '') {
continue;
}
// Check for exec( usage - word boundary ensures we don't match "execute(" etc.
if (preg_match('/\bexec\s*\(/i', $sanitized_line)) {
$original_line = $original_lines[$line_num] ?? $sanitized_line;
$violation_message = "🚨 CRITICAL: exec() is BANNED - use shell_exec() instead
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 (partial output with no indication of truncation)
Real-world example from this codebase:
- 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
exec() is completely banned with NO EXCEPTIONS. Use shell_exec() instead.";
$resolution = "REQUIRED ACTION - Replace exec() with shell_exec():
BASIC USAGE (don't need return code):
\$output = shell_exec(\$command . ' 2>&1');
if (\$output === null) {
throw new \\RuntimeException('Command failed');
}
ADVANCED USAGE (need return code):
Use the echo \$? trick to capture exit code:
\$full_command = \"(\$command) 2>&1; echo \$?\";
\$result = shell_exec(\$full_command);
// 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,
$line_number,
$violation_message,
trim($original_line),
$resolution,
'critical'
);
}
}
}
}