Files
rspade_system/app/RSpade/CodeQuality/Rules/PHP/ProcOpenStreamTruncation_CodeQualityRule.php
root f6fac6c4bc Fix bin/publish: copy docs.dist from project root
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>
2025-10-21 02:08:33 +00:00

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.";
}
}