Quickly patch those session upload progress bugs.
Quickly wrote this code with AI to help secure existing php for the bug. Wanting to extend it to be selective as old PHP prior to 8.2 wont get patched by PHP.org
Warning
If you need a quick script to patch your own cpanel or cloudlinux alt-php to turn the setting off for now use this script at your own risk no warranties. Always backup your php.ini's.
$line) {
if (preg_match('/^\s*\[([^\]]+)\]/', $line, $m)) {
$scoped = preg_match('/^(PATH|HOST)\s*=/i', trim($m[1])) === 1;
if (strcasecmp(trim($m[1]), 'session') === 0 && $section === null) { $section = $i; }
}
if (!$scoped && preg_match('/^\s*session\.[a-zA-Z0-9_.]+\s*=/', $line)) { $near = $i; }
if (preg_match('/^([ \t]*)(;[; \t]*)?(session\.upload_progress\.enabled[ \t]*=[ \t]*)(.*)$/', $line, $m)) {
$settings[] = array('index' => $i, 'prefix' => $m[1] . $m[3], 'raw' => $m[4], 'scoped' => $scoped, 'commented' => $m[2] !== '');
}
}
return array('lines' => $lines, 'settings' => $settings, 'near' => $near, 'section' => $section);
}
function disabled_value($raw)
{
$value = trim(preg_replace('/;.*$/', '', $raw));
$value = strtolower(trim($value, "\"'"));
return in_array($value, array('off', '0', 'false', 'no', 'none', ''), true);
}
function patched($data)
{
if (count($data['settings']) !== 1) { return false; }
$setting = $data['settings'][0];
return !$setting['commented'] && !$setting['scoped'] && disabled_value($setting['raw']);
}
function changed_text($text, $target, &$note)
{
$data = inspect_ini($text); $lines = $data['lines']; $global = false;
$eol = strpos($text, "\r\n") !== false ? "\r\n" : (strpos($text, "\n") !== false ? "\n" : (strpos($text, "\r") !== false ? "\r" : "\n"));
$note = ''; $removed = 0;
foreach ($data['settings'] as $setting) {
// Retain the first global occurrence, even if commented; remove all others.
if (!$global && !$setting['scoped']) {
$comment = '';
if (preg_match('/(\s*;.*)$/', $setting['raw'], $m)) { $comment = $m[1]; }
$lines[$setting['index']] = $setting['prefix'] . $target . $comment;
$global = true;
} else {
unset($lines[$setting['index']]);
$removed++;
}
}
$lines = array_values($lines);
// Recalculate positions after removing duplicate/scoped occurrences.
$data = inspect_ini(implode($eol, $lines));
if (!$global) {
$new = 'session.upload_progress.enabled = ' . $target;
if ($data['near'] !== null) {
array_splice($lines, $data['near'] + 1, 0, array($new));
$note = 'Inserted beside existing session.* settings.';
} elseif ($data['section'] !== null) {
array_splice($lines, $data['section'] + 1, 0, array($new));
$note = 'No existing active session.* settings; inserted in [Session].';
} else {
// Prepend a global section so PATH/HOST sections cannot capture the new setting.
array_splice($lines, 0, 0, array('[Session]', $new, ''));
$note = 'No existing active session.* settings or [Session] section; created [Session].';
}
} else { $note = 'Set one active global entry (uncommented if necessary).'; }
if ($removed) { $note .= ' Removed ' . $removed . ' duplicate/scoped entry(s).'; }
return implode($eol, $lines);
}
function write_all($handle, $text)
{
$length = strlen($text); $offset = 0;
while ($offset < $length) {
$count = @fwrite($handle, substr($text, $offset));
if ($count === false || $count === 0) { return false; }
$offset += $count;
}
return @fflush($handle);
}
function backup_ini($path, $original, $stat)
{
$base = $path . '.backup.' . date('d-m-y');
$number = 0;
while (true) {
$backup = $base . ($number === 0 ? '' : '(' . $number . ')');
clearstatcache(true, $backup);
if (file_exists($backup) || is_link($backup)) {
$number++;
continue;
}
$oldMask = umask(0077);
// Exclusive creation prevents overwriting a backup created concurrently.
$out = @fopen($backup, 'xb');
umask($oldMask);
if ($out !== false) { break; }
clearstatcache(true, $backup);
if (file_exists($backup) || is_link($backup)) {
$number++;
continue;
}
throw new Exception('Cannot create backup: ' . $backup);
}
$ok = write_all($out, $original); fclose($out);
if (!$ok || @file_get_contents($backup) !== $original) {
@unlink($backup); throw new Exception('Backup verification failed; original untouched.');
}
// Backups stay private; original ownership, permissions and inode are retained.
return $backup;
}
function process_ini($path, $target, &$edited)
{
if (!file_exists($path) && !is_link($path)) { return 'No php.ini'; }
if (!is_file($path)) { throw new Exception('Not a regular php.ini file: ' . $path); }
$fp = @fopen($path, $target === null ? 'rb' : 'r+b');
if (!$fp) { throw new Exception('Cannot open: ' . $path); }
try {
if (!@flock($fp, ($target === null ? LOCK_SH : LOCK_EX) | LOCK_NB)) {
throw new Exception('File is locked: ' . $path);
}
$original = stream_get_contents($fp);
if ($original === false) { throw new Exception('Cannot read: ' . $path); }
$data = inspect_ini($original);
$before = patched($data) ? 'Patched' : 'Unpatched';
if ($target === null) {
$detail = count($data['settings']) ? '' : ' (setting missing)';
flock($fp, LOCK_UN); fclose($fp); return $before . $detail;
}
if ($target === 'Off' && patched($data)) {
flock($fp, LOCK_UN); fclose($fp); return 'Patched (unchanged)';
}
$note = ''; $new = changed_text($original, $target, $note);
if ($new === $original) {
flock($fp, LOCK_UN); fclose($fp); return $before . ' (unchanged)';
}
$backup = backup_ini($path, $original, fstat($fp));
// Keep the original inode, permissions, ACLs, ownership and security labels.
rewind($fp);
if (!write_all($fp, $new) || !@ftruncate($fp, strlen($new)) || !@fflush($fp)) {
rewind($fp);
$restored = write_all($fp, $original) && @ftruncate($fp, strlen($original)) && @fflush($fp);
throw new Exception('Write failed. ' . ($restored ? 'Original restored.' : 'RESTORE REQUIRED from ' . $backup));
}
rewind($fp);
if (stream_get_contents($fp) !== $new) {
rewind($fp);
$restored = write_all($fp, $original) && @ftruncate($fp, strlen($original)) && @fflush($fp);
throw new Exception('Verification failed. ' . ($restored ? 'Original restored.' : 'RESTORE REQUIRED from ' . $backup));
}
flock($fp, LOCK_UN); fclose($fp);
$edited[] = $path;
return $before . ' -> ' . (patched(inspect_ini($new)) ? 'Patched' : 'Unpatched') . ' (edited)' . "\n " . $note . "\n Backup: " . $backup;
} catch (Exception $e) {
flock($fp, LOCK_UN); fclose($fp); throw $e;
}
}
function main($args)
{
if (count($args) === 1 || (count($args) === 2 && in_array($args[1], array('--help', '-h'), true))) { usage(); return 0; }
$target = null;
if (count($args) === 2 && $args[1] === '--status') {
// Read-only.
} elseif (count($args) === 3 && $args[1] === '--upload_progress' && in_array(strtolower($args[2]), array('off', 'on'), true)) {
$target = ucfirst(strtolower($args[2]));
} elseif (count($args) === 2 && preg_match('/^--upload_progress=(off|on)$/i', $args[1], $m)) {
$target = ucfirst(strtolower($m[1]));
} else { fwrite(STDERR, "Invalid command.\n\n"); usage(); return 2; }
$edited = array(); $errors = 0; $missing = 0;
echo "session.upload_progress.enabled: " . ($target === null ? 'status' : 'set to ' . $target) . "\n";
echo "File-based check only; services are not restarted.\n\n";
foreach (array('cpanel', 'alt') as $family) {
// Released PHP series only: PHP 6 and unreleased minor versions are excluded.
$versions = array_merge(range(50, 56), range(70, 74), range(80, $family === 'alt' ? 85 : 84));
foreach ($versions as $version) {
$label = ($family === 'alt' ? 'CloudLinux Alt-PHP ' : 'cPanel PHP ') . floor($version / 10) . '.' . ($version % 10);
$path = $family === 'alt' ? '/opt/alt/php' . $version . '/etc/php.ini' : '/opt/cpanel/ea-php' . $version . '/root/etc/php.ini';
try {
$result = process_ini($path, $target, $edited);
if ($result === 'No php.ini') { $missing++; }
echo $label . ' - ' . $result . "\n File: " . $path . "\n";
} catch (Exception $e) { $errors++; echo $label . ' - ERROR: ' . $e->getMessage() . "\n"; }
}
echo "\n";
}
if ($target !== null) {
echo 'Edited files: ' . count($edited) . "\n";
foreach ($edited as $path) { echo ' ' . $path . "\n"; }
}
echo 'Missing php.ini: ' . $missing . '; errors: ' . $errors . "\n";
return $errors ? 1 : 0;
}
// Runtime entry point: always execute when invoked from the command line.
echo "This is a free tool by 404 Guard - https://www.404-guard.com/" . PHP_EOL;
echo " " . PHP_EOL;
$arguments = isset($_SERVER['argv']) ? $_SERVER['argv'] : array(__FILE__);
$exitCode = main($arguments);
exit($exitCode);
/* The code in php.ini is session.upload_progress.enabled and
temporarily swith it to Off if it's on. */
Use at your own risk but this plugin I wrote for myself that will simply say if your PHP is patched or vulnerable to the session.progress_update bug. No warranty. Written with the help of ChatGPT.