Back to blog

PHP2Shell in 2026: How to Protect Your PHP Server From CVE-2026-109618 and 109619

Hello HaWkers, on October 21, 2026 the PHP project shipped an emergency update: versions 8.5.12, 8.4.27, 8.3.36 and 8.2.35 fix two critical flaws that allow unauthenticated remote code execution. Both were rated 9.2 on CVSS and got the nickname PHP2Shell, because the end goal of the attack is getting a shell on the server.

The scary part: the flaws have existed since PHP 5.4, they hit parts of the engine that practically every request touches (memory management and error handling) and, on Apache with mod_php, the attack happens before a single line of your code runs. It does not matter how clean your Laravel app is.

Do you know right now, without opening a terminal, which PHP version is running in production? If the answer is "I think it is 8.2", this article is for you. By the end you will have a checklist to upgrade, apply mitigations while you cannot upgrade yet, and look for signs that someone already tried to get in.

What PHP2Shell Is and Why It Is So Serious

These are two different vulnerabilities, published together:

  • CVE-2026-109618 (RCE via out-of-memory corruption): when a request hits memory_limit at the wrong moment, PHP can leave internal request data partially initialized. During cleanup, the engine treats that garbage as valid data and corrupts memory. The known vector is a gigantic form field name.
  • CVE-2026-109619 (RCE via stale global state): a fatal error at the wrong point leaves internal state pointing to memory from a previous request that has already been freed. The next request served by the same worker uses that memory: the classic use-after-free. The known vector goes through the session.upload_progress callback.

Neither flaw requires login or user interaction. Attack complexity is rated high, but that brings little comfort, because PHP itself demonstrated remote code execution on Apache with mod_php using the default configuration.

Who is most at risk

The impact depends on how PHP is executed (the so-called SAPI):

Execution mode Risk What is known
Apache + mod_php Highest RCE demonstrated before application code runs
PHP-FPM High RCE confirmed via application code paths
php-cgi, FrankenPHP, LiteSpeed At risk Listed as affected, without the same public demonstration

In short: if your PHP serves HTTP requests, you are in the affected group.

The Timeline: Audits, AI and an Embargo That Leaked

The story of how these flaws surfaced says a lot about open source security in 2026. According to the PHP Foundation blog:

  • 2024: Quarkslab performs the first external audit of PHP in years, organized by OSTIF and funded by the Sovereign Tech Agency. While fixing an issue reported there, a core developer finds the first signs of the out-of-memory bug.
  • May 2026: the foundation creates the Ecosystem Security Team.
  • From July 1, 2026: the team publishes 15 CVEs and processes more than 100 invalid, duplicate or non-reproducible reports.
  • July 30, 2026: Calif starts auditing PHP through OpenAI's Patch the Planet program.
  • August 11, 2026: researcher Ryan A, from Calif, reports an unconditional RCE with the same root cause.
  • August 24 and September 18, 2026: Wiz and OpenAI report other attack vectors.
  • October 20, 2026: the CVE IDs leak online with the wrong release time and score (a 10.0 circulated), PHP publishes the pre-announcement earlier than planned and Calif accidentally publishes a proof-of-concept video.
  • October 21, 2026, 14:00 UTC: coordinated release, moved up from the 22nd.

Notice two things. First, AI tools are in the middle of the process: Alpha-Omega ran Anthropic's Claude Mythos against the PHP codebase, and the team started using OpenAI's Daybreak Red to validate reports. Second, the embargo did not hold. When the patch came out, a lot of people already knew exactly where to look.

Step 1: Find Out Which PHP You Are Running

It sounds obvious, but the PHP in your terminal is not always the same one serving the site. A server can have the CLI on 8.3 and FPM on 8.2. Check both:

# Command-line PHP version
php -v

# Installed PHP-FPM versions (Debian/Ubuntu)
ls /usr/sbin/php-fpm* 2>/dev/null
for bin in /usr/sbin/php-fpm*; do "$bin" -v | head -n 1; done

# Ask the web server itself which PHP responds
# (create the file, hit it and DELETE it right after)
echo '<?php echo PHP_VERSION, " ", PHP_SAPI;' > /var/www/html/versao-tmp.php
curl -s https://seu-dominio.com.br/versao-tmp.php; echo
rm /var/www/html/versao-tmp.php

With the version in hand, compare it against the fix table:

Branch Vulnerable Fixed
PHP 8.5 below 8.5.12 8.5.12
PHP 8.4 below 8.4.27 8.4.27
PHP 8.3 below 8.3.36 8.3.36
PHP 8.2 below 8.2.35 8.2.35

What if you see 8.1, 8.0 or 7.4? Those branches no longer receive updates from the project. The security team published minimal source-code backports for 7.4, 8.0 and 8.1, aimed at people who compile PHP themselves, and distributions and extended support vendors got the patches earlier. Other than that, the path is migrating. Also remember that PHP 8.2 reaches end of life on December 31, 2026, so anyone on it will have to move again soon.

Step 2: Upgrade (and Restart Every Worker)

There is only one complete fix: upgrading. The rest of this article is containment. A script that checks whether the running version is already patched helps a lot in a deploy pipeline or in an audit cron job:

<?php
// checa-php2shell.php - checks whether the running PHP already has the October 2026 fixes
$corrigidas = [
    '8.5' => '8.5.12',
    '8.4' => '8.4.27',
    '8.3' => '8.3.36',
    '8.2' => '8.2.35',
];

$ramo = PHP_MAJOR_VERSION . '.' . PHP_MINOR_VERSION;

if (!isset($corrigidas[$ramo])) {
    // 8.1 or older: no official release, only a backport or migration
    fwrite(STDERR, "PHP {$ramo} está fora de suporte. Migre ou use um backport do seu fornecedor.\n");
    exit(2);
}

if (version_compare(PHP_VERSION, $corrigidas[$ramo], '<')) {
    fwrite(STDERR, "VULNERÁVEL: PHP " . PHP_VERSION . " (precisa de {$corrigidas[$ramo]} ou superior)\n");
    exit(1);
}

echo "OK: PHP " . PHP_VERSION . " já inclui as correções do PHP2Shell\n";

Run it with php checa-php2shell.php on every image or server. A non-zero exit code makes the pipeline fail, which is exactly what you want.

Once the package is upgraded, restart every worker. An FPM that keeps old processes alive is still running the vulnerable binary:

# Debian/Ubuntu with PHP-FPM 8.3 (adjust the branch number)
sudo apt update && sudo apt install --only-upgrade php8.3-fpm php8.3-cli
sudo systemctl restart php8.3-fpm

# Apache with mod_php: PHP lives inside the Apache process
sudo systemctl restart apache2

# Containers: rebuild the image from the new base, do not just restart
docker build --pull -t minha-app:php-corrigido .

In containers, --pull matters: without it, Docker may reuse the old base image from cache and you ship the same vulnerable PHP under a new tag.

Step 3: Mitigations While You Cannot Upgrade Yet

Sometimes you cannot upgrade today: shared hosting, a legacy app pinned to a version, a scheduled maintenance window. php.net lists mitigations, but makes it clear they do not make the installation safe, they only reduce the attack surface.

For CVE-2026-109619, turn off session upload progress in php.ini or in the FPM pool:

; php.ini or FPM pool - temporary PHP2Shell mitigation
; Closes the known CVE-2026-109619 vector (breaks upload progress bars)
session.upload_progress.enabled = 0

; PHP 8.5 only: disables backtraces on fatal errors
; fatal_error_backtraces = 0

; CVE-2026-109618 mitigation: limit POST size
; 8M if the application does not receive uploads, 32M if it does
post_max_size = 8M
upload_max_filesize = 6M

Details that trip up people in a hurry:

  • Do not rely on ini_set() or FPM's .user.ini for this. The setting has to live in php.ini, in the pool or in the server configuration.
  • Restart every worker after the change.
  • php.net also mentions memory_limit=-1 (preferably via php_admin_value) as a CVE-2026-109618 mitigation and asks you not to use RLIMIT_AS or RLIMIT_DATA. Weigh this carefully: with no memory limit, a leaking script can take down the whole machine.

There is one more reason to keep session.upload_progress disabled even after upgrading. On October 23, 2026, PHP published an informational advisory about a third issue, CVE-2026-115560, which is not fixed by the releases from the 21st. It only affects applications that set session.serialize_handler to php_binary or php_serialize and switch it to php at runtime with ini_set() before session_start(). It is a rare scenario, and the fix comes in the next regular release. The official recommendation is to keep session.upload_progress.enabled=0 until then.

Step 4: Block What You Do Not Use at the Edge

If your application does not receive multipart/form-data forms on certain routes, there is no reason to accept that body type there. Blocking at the edge (Nginx, WAF or CDN) cuts the vector before the request reaches PHP. An Nginx example for an API that only speaks JSON:

# Nginx - rejects multipart on routes that should only receive JSON
location /api/ {
    # Limits body size to get in the way of gigantic field names
    client_max_body_size 1m;

    if ($content_type ~* "multipart/form-data") {
        return 415;
    }

    include fastcgi_params;
    fastcgi_param SCRIPT_FILENAME $document_root/index.php;
    fastcgi_pass unix:/run/php/php8.3-fpm.sock;
}

Public WAF rules for ModSecurity and Suricata already exist. Abstract Security recommends enabling them in log mode first, checking the false positives and only then switching to blocking. A poorly tuned WAF rule will take down an e-commerce checkout on a Friday night.

Another point: any old local file inclusion finding like include($_GET['pagina']) that was sitting in the backlog as "medium" should go back into the queue as critical. Many of the proofs of concept that circulated depend precisely on an LFI to turn into RCE.

Step 5: Look for Signs That Someone Already Tried

The details leaked on October 20, and security teams reported attack attempts as early as the 21st. Your investigation window should start on the 20th, not on the day you upgraded. Some signs Abstract Security points out as characteristic:

  • PHP fatal errors ending in in Unknown on line 0 in the web server logs.
  • POSTs of 1 MB or more to PHP endpoints, especially bursts of 10 or more from the same IP within 10 minutes.
  • On Apache, hundreds of large POSTs from the same IP generating AH00052 entries, followed by a slow response, which suggests brute force against ASLR.
  • sh -c processes spawned by Apache, Nginx or PHP workers, running id, curl or nc.

A quick first pass over the logs:

# 1. Exploit signature in the error logs (since 10/20/2026)
sudo grep -h "in Unknown on line 0" /var/log/apache2/error.log* /var/log/php*-fpm.log 2>/dev/null | tail -n 50

# 2. Apache child process crashes, grouped by day
sudo zgrep -h "AH00052" /var/log/apache2/error.log* | awk '{print $2, $3, $5}' | sort | uniq -c

# 3. IPs with the most POSTs in the access log (combined format)
sudo awk '$6 == "\"POST" {print $1}' /var/log/apache2/access.log | sort | uniq -c | sort -rn | head -n 20

# 4. New files in temp directories and the web root since 10/20
sudo find /tmp /dev/shm /var/www -newermt "2026-10-20" -type f -name "*.php" 2>/dev/null

Found something? Also check for new keys in authorized_keys, cron jobs nobody created and unexpected outbound connections. If there is concrete evidence of compromise, treating it as an incident (isolate, preserve evidence, rotate credentials) is safer than just upgrading and moving on.

What This Case Teaches About Security in 2026

Anyone who followed React2Shell, the critical vulnerability in React Server Components, will recognize the pattern: a flaw in the runtime or framework itself, which no code review of your application would have caught, and a race between the patch and the first working exploit.

Three practical lessons:

  1. Version inventory is security. The first step of every incident like this is answering "what runs where". If that takes a day, you have already lost the most important day.
  2. An embargo is not a guarantee. This time the leak got ahead of the release by almost a day. Have an update process that runs in hours, not weeks.
  3. AI changed the volume on both sides. The same kind of tool that helped find these flaws also generates bogus reports (the PHP team discarded more than 100) and speeds up those hunting for holes.

One honest caveat: researcher Elias Taylor argued that many proofs of concept that circulated on announcement day were fake or only worked in a lab with an extra vulnerable component added. That does not change his own conclusion: upgrade anyway.

Outlook: What Is Next for PHP

PHP 8.6.0 is already at RC4, and the PHP Foundation strengthened the team with Volker Dusch as resident AI security engineer and Daniel Scherzer focused on hardening, plus builds with sanitizers on every supported platform. The foundation also asked companies, hosting providers, distributions and governments that depend on PHP to fund the work in proportion to that dependency. The full-time work that fixed the memory bug was paid for by sponsorship.

For anyone running servers, the agenda for the next few days is short: upgrade to 8.5.12, 8.4.27, 8.3.36 or 8.2.35, restart the workers, keep session.upload_progress.enabled=0 until the next regular release and review the logs since October 20. And if you are still on 8.2, put the migration on the calendar now: end of life is December 31, 2026.

Let's go! 🦅

📚 Want to Keep Up With What Is Coming?

This article covered PHP2Shell and how to protect PHP servers from CVE-2026-109618 and CVE-2026-109619, but the ecosystem changes every week and not everything becomes an article here.

On X I share what I am testing, behind the scenes of my projects and the news that shows up before it turns into a post.

Follow Me There

👉 Follow @jeffbruchado on X

💡 Daily content about development, career and the tools I actually use

Comments (0)

This article has no comments yet 😢. Be the first! 🚀🦅

Add comments