404 GUARD BLOG

Quick Patch for switching off php session progress update.

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

				
					<?php
/**
 * Patch cPanel / CloudLinux php.ini files. Run with a PHP 7.4+ CLI.
 * No arguments displays help. Does not restart services.
 * Status is file-based, not a check of running PHP or additional INI overrides.
 */
if (PHP_SAPI !== 'cli') { die("CLI only. Run this file using PHP 7.4 or newer from SSH.\n"); }
if (PHP_VERSION_ID < 70400) {
    fwrite(STDERR, "This tool requires PHP CLI 7.4 or newer.\n");
    exit(2);
}

function usage()
{
    echo "Usage:\n";
    echo "  php patch-upload-progress.php --status\n";
    echo "  php patch-upload-progress.php --upload_progress Off\n";
    echo "  php patch-upload-progress.php --upload_progress On\n\n";
    echo "No arguments or --help: show these commands.\n";
    echo "Patched = exactly one active global disabled entry and no commented duplicates.\n";
    echo "Unpatched = missing, enabled, unknown, or conflicting settings.\n";
    echo "Backups: php.ini.backup.d-m-y, using the server's local date.\n";
    echo "Existing backups are never overwritten; numbered suffixes (1), (2), (3) are added.\n";
    echo "Files only: additional INI files, pool settings and running services are not checked.\n";
    echo "Reload/restart the applicable PHP services after patching.\n";
    echo "Exit codes: 0 = completed, 1 = file errors, 2 = invalid command.\n";
}

function inspect_ini($text)
{
    $lines = preg_split('/\r\n|\n|\r/', $text);
    $settings = array(); $near = null; $section = null; $scoped = false;
    foreach ($lines as $i => $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.

See whether 404 Guard helps your website.

Start with 500 protected 404 hits, check the important pages and compare the results.