$sanitized_line) { if (preg_match($stream_sources_pattern, $sanitized_line)) { $this->add_violation( $file_path, $line_num + 1, $this->get_missing_blocking_message(), trim($original_lines[$line_num] ?? $sanitized_line), $this->get_missing_blocking_resolution(), 'high' ); return; // Only report first occurrence } } return; } // VIOLATION 2: Dangerous read pattern - breaking on false in fread loop // Pattern: while (!feof($stream)) { $chunk = fread(...); if ($chunk === false) { break; } ... } // This breaks prematurely and truncates data because fread can return false temporarily if (preg_match('/while\s*\([^)]*!feof[^)]*\)[^{]*\{[^}]*fread[^}]*===\s*false[^}]*break/is', $sanitized_code)) { foreach ($sanitized_lines as $line_num => $sanitized_line) { if (preg_match('/if\s*\([^)]*===\s*false[^)]*\)\s*\{[^}]*break/', $sanitized_line)) { $this->add_violation( $file_path, $line_num + 1, $this->get_dangerous_break_message(), trim($original_lines[$line_num] ?? $sanitized_line), $this->get_dangerous_break_resolution(), 'critical' ); return; } } } // VIOLATION 3: Using stream_get_contents() in blocking mode without checking for false // stream_get_contents() has 8192 byte default buffer and will truncate silently if (preg_match('/stream_get_contents\s*\([^)]+\)\s*;/i', $sanitized_code) && preg_match('/stream_set_blocking\s*\([^)]+,\s*true\s*\)/i', $sanitized_code)) { foreach ($sanitized_lines as $line_num => $sanitized_line) { if (preg_match('/stream_get_contents\s*\(/i', $sanitized_line)) { // Check if it's NOT in a proper loop $context_start = max(0, $line_num - 3); $context_end = min(count($sanitized_lines) - 1, $line_num + 3); $context = implode("\n", array_slice($sanitized_lines, $context_start, $context_end - $context_start + 1)); // If not in a while loop that handles chunks, flag it if (!preg_match('/while\s*\([^)]*!feof|while\s*\([^)]*!\$.*_done/', $context)) { $this->add_violation( $file_path, $line_num + 1, $this->get_unsafe_stream_get_contents_message(), trim($original_lines[$line_num] ?? $sanitized_line), $this->get_unsafe_stream_get_contents_resolution(), 'high' ); return; } } } } } private function get_missing_blocking_message(): string { return "Stream operations without explicit blocking mode File uses stream sources (proc_open/fsockopen/popen/stream_socket_client) with read operations but does not call stream_set_blocking(). PHP streams default to non-blocking mode in some contexts, which causes: - Incomplete reads with partial data - Silent data truncation (no errors or warnings) - Race conditions depending on when data arrives - Data integrity issues for command output and file transfers"; } private function get_missing_blocking_resolution(): string { return "REQUIRED ACTION: Add stream_set_blocking(\$stream, true) before reading AND use proper read loop: CORRECT PATTERN: \$process = proc_open(\$command, \$descriptors, \$pipes); fclose(\$pipes[0]); stream_set_blocking(\$pipes[1], true); stream_set_blocking(\$pipes[2], true); // Read stdout until EOF \$output = ''; while (!feof(\$pipes[1])) { \$chunk = fread(\$pipes[1], 8192); if (\$chunk !== false) { \$output .= \$chunk; } } // Read stderr until EOF \$error = ''; while (!feof(\$pipes[2])) { \$chunk = fread(\$pipes[2], 8192); if (\$chunk !== false) { \$error .= \$chunk; } }"; } private function get_dangerous_break_message(): string { return "CRITICAL: Dangerous stream read pattern will truncate data This fread() loop breaks on false, which causes data truncation because fread() can return false TEMPORARILY even when more data is available. Bug impact: - Large outputs silently truncated at ~8KB - No error thrown, appears successful - Production data loss and corruption - Intermittent failures depending on timing"; } private function get_dangerous_break_resolution(): string { return "REQUIRED FIX: Use feof() as the loop condition - do NOT check it inside the loop. WRONG PATTERN (race condition, will truncate): \$done = false; while (!\$done) { \$chunk = fread(\$stream, 8192); if (\$chunk === '') { if (feof(\$stream)) { // ❌ Checked AFTER empty read \$done = true; } } else { \$output .= \$chunk; } } CORRECT PATTERN: \$output = ''; while (!feof(\$stream)) { \$chunk = fread(\$stream, 8192); if (\$chunk !== false) { \$output .= \$chunk; } }"; } private function get_unsafe_stream_get_contents_message(): string { return "Unsafe stream_get_contents() usage may truncate large outputs stream_get_contents() uses 8192 byte default buffer. For large outputs (>8KB), data will be silently truncated. Even with stream_set_blocking(true), stream_get_contents() may not read all data in one call for large outputs."; } private function get_unsafe_stream_get_contents_resolution(): string { return "Use chunked read loop instead of stream_get_contents(): UNSAFE: stream_set_blocking(\$pipe, true); \$output = stream_get_contents(\$pipe); // ❌ May truncate >8KB SAFE: stream_set_blocking(\$pipe, true); \$output = ''; while (!feof(\$pipe)) { \$chunk = fread(\$pipe, 8192); if (\$chunk !== false) { \$output .= \$chunk; } }"; } }