Fix bin/publish: use correct .env path for rspade_system Fix bin/publish script: prevent grep exit code 1 from terminating script 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
180 lines
5.8 KiB
PHP
Executable File
180 lines
5.8 KiB
PHP
Executable File
<?php
|
|
|
|
namespace App\RSpade\CodeQuality\Rules\PHP;
|
|
|
|
use App\RSpade\CodeQuality\Rules\CodeQualityRule_Abstract;
|
|
use App\RSpade\CodeQuality\Support\FileSanitizer;
|
|
|
|
class ProcOpenStreamTruncation_CodeQualityRule extends CodeQualityRule_Abstract
|
|
{
|
|
public function get_id(): string
|
|
{
|
|
return 'PHP-PROC-01';
|
|
}
|
|
|
|
public function get_name(): string
|
|
{
|
|
return 'proc_open() Stream Truncation Check';
|
|
}
|
|
|
|
public function get_description(): string
|
|
{
|
|
return 'Detects improper stream reading from proc_open() pipes - causes silent 8KB truncation';
|
|
}
|
|
|
|
public function get_file_patterns(): array
|
|
{
|
|
return ['*.php'];
|
|
}
|
|
|
|
public function get_default_severity(): string
|
|
{
|
|
return 'critical';
|
|
}
|
|
|
|
/**
|
|
* Check PHP file for proc_open() with improper stream reading
|
|
*
|
|
* WHITELIST APPROACH: If proc_open() is used with fread(), the code MUST use:
|
|
* while (!feof($pipes[1])) { ... }
|
|
*
|
|
* This is the ONLY correct pattern for reading proc_open() pipes without truncation.
|
|
* Any other pattern (stream_get_contents, checking feof() after reads, etc.) causes
|
|
* silent 8192-byte truncation bugs.
|
|
*/
|
|
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;
|
|
}
|
|
|
|
// 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_code = $sanitized_data['content'];
|
|
|
|
// Check if function contains proc_open()
|
|
if (!preg_match('/\bproc_open\s*\(/i', $sanitized_code)) {
|
|
return; // No proc_open usage, skip this file
|
|
}
|
|
|
|
// Check if function reads from pipes using fread()
|
|
if (!preg_match('/\bfread\s*\(\s*\$pipes\[/i', $sanitized_code)) {
|
|
return; // Not reading from pipes with fread, skip
|
|
}
|
|
|
|
// WHITELIST CHECK: Must have while (!feof($pipes[...])) pattern
|
|
if (!preg_match('/while\s*\(\s*!\s*feof\s*\(\s*\$pipes\[/i', $sanitized_code)) {
|
|
// VIOLATION: Using fread() on proc_open() pipes without mandatory while (!feof()) pattern
|
|
|
|
// Find the line number where proc_open appears
|
|
$sanitized_lines = $sanitized_data['lines'];
|
|
foreach ($sanitized_lines as $line_num => $sanitized_line) {
|
|
if (preg_match('/\bproc_open\s*\(/i', $sanitized_line)) {
|
|
$line_number = $line_num + 1;
|
|
$original_line = $original_lines[$line_num] ?? $sanitized_line;
|
|
|
|
$this->add_violation(
|
|
$file_path,
|
|
$line_number,
|
|
$this->get_violation_message(),
|
|
trim($original_line),
|
|
$this->get_resolution_message(),
|
|
'critical'
|
|
);
|
|
|
|
return; // Only report first occurrence
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private function get_violation_message(): string
|
|
{
|
|
return "🚨 CRITICAL: proc_open() pipes must be read using while (!feof(\$pipes[...])) pattern
|
|
|
|
When using proc_open() with fread(), you MUST use this specific loop pattern:
|
|
while (!feof(\$pipes[1])) { ... }
|
|
|
|
ANY other pattern causes silent 8192-byte truncation:
|
|
- stream_get_contents() - truncates at 8KB
|
|
- Checking feof() AFTER empty reads - race condition truncation
|
|
- Custom loop conditions - unpredictable behavior
|
|
|
|
Real-world incident from production environment:
|
|
- File: JqhtmlWebpackCompiler.php
|
|
- Symptom: Compiled jqhtml output truncated at exactly 8,217 bytes (8192 + 25)
|
|
- Root cause: feof() checked AFTER empty read instead of as loop condition
|
|
- Impact: JavaScript syntax errors from mid-statement truncation
|
|
|
|
The while (!feof()) pattern is the ONLY battle-tested, safe approach from PHP manual
|
|
and Stack Overflow consensus. No exceptions, no alternatives.";
|
|
}
|
|
|
|
private function get_resolution_message(): string
|
|
{
|
|
return "REQUIRED ACTION - Use while (!feof(\$pipes[...])) as the loop condition:
|
|
|
|
MANDATORY PATTERN (the ONLY correct way):
|
|
\$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]);
|
|
|
|
// Set blocking mode to ensure complete reads
|
|
stream_set_blocking(\$pipes[1], true);
|
|
stream_set_blocking(\$pipes[2], true);
|
|
|
|
// Read stdout until EOF
|
|
\$output_str = '';
|
|
while (!feof(\$pipes[1])) {
|
|
\$chunk = fread(\$pipes[1], 8192);
|
|
if (\$chunk !== false) {
|
|
\$output_str .= \$chunk;
|
|
}
|
|
}
|
|
|
|
// Read stderr until EOF
|
|
\$error_str = '';
|
|
while (!feof(\$pipes[2])) {
|
|
\$chunk = fread(\$pipes[2], 8192);
|
|
if (\$chunk !== false) {
|
|
\$error_str .= \$chunk;
|
|
}
|
|
}
|
|
|
|
fclose(\$pipes[1]);
|
|
fclose(\$pipes[2]);
|
|
|
|
\$exit_code = proc_close(\$process);
|
|
|
|
WHY THIS WORKS:
|
|
- feof() as loop condition prevents ALL truncation bugs
|
|
- No race conditions from checking feof() after empty reads
|
|
- No buffer limits from stream_get_contents()
|
|
- Standard PHP idiom from manual and Stack Overflow
|
|
- Battle-tested across the codebase
|
|
|
|
ALTERNATIVE: Use \\exec_safe() helper
|
|
If executing shell commands, \\exec_safe() has this pattern built-in.";
|
|
}
|
|
}
|