Security · Python and PHP

Python and PHP, on security: what PHP2Shell made obvious

On 21 October 2026 the PHP project shipped emergency releases for two critical bugs, now widely called PHP2Shell. CVE-2026-109618 and CVE-2026-109619 are both scored CVSS 9.2. Together they can let an unauthenticated attacker run their own code on a server that is still on an older build. They are a useful comparison, because the bugs are in the engine that turns an HTTP request into a PHP program.

The short version
  • Install PHP 8.5.12, 8.4.27, 8.3.36 or 8.2.35. Updating is the only complete fix.
  • CVE-2026-109618 and CVE-2026-109619 are both rated 9.2 and can lead to remote code execution.
  • The bugs run in the PHP engine, sometimes before your application code runs.
  • Ordinary Python cannot express this class of memory bug, and the interpreter is a separate process.
  • Python does not remove the bugs you write yourself. It raises the floor under the runtime.

What the two bugs actually are

The fixed releases are PHP 8.5.12, 8.4.27, 8.3.36 and 8.2.35. The PHP project says there is no complete workaround. Updating is the only safe choice. CISA's alert the same day told organisations to patch immediately, and said it had not seen exploitation in the wild at the time it published. Proof-of-concept material was already circulating. At least one European government took a site offline as a precaution.

Both bugs sit in memory management and error handling, which nearly every PHP request touches. The details below are from the PHP Foundation and the PHP security advisory.

CVE-2026-109618 (GHSA-w8hw-w9fx-j79w) is an out-of-memory corruption bug. If a request hits PHP's memory limit at the wrong moment, the engine can leave internal request data only partly initialised, then later treat that data as valid. On Apache with mod_php, the Foundation says an unauthenticated request can trigger this before any application code runs, and remote code execution was demonstrated with default settings. It was also confirmed on PHP-FPM through application code paths.

CVE-2026-109619 (GHSA-6wmf-4rv5-p3rp) is a stale-state bug. A fatal error at the wrong point can leave internal state pointing at memory that belonged to a previous request. A later request on the same worker can use that freed memory. A crash was confirmed, and the reporter demonstrated remote code execution against a real application on Apache with mod_php.

Every PHP install that serves web requests is in scope: PHP-FPM, Apache with mod_php, php-cgi, FrankenPHP and LiteSpeed. The project considers mod_php the highest risk, because the interpreter shares the web server's address space.

PHP 8.1 and older no longer receive normal updates. Some Linux distributions and long-term-support vendors shipped their own backports, so those packages are a real fix when the vendor says so. The PHP security team also published minimal source-backports for PHP 7.4, 8.0 and 8.1, for people building from source or without a vendor. Those backports stop at 7.4. Anything older is outside what the PHP project handed out. A source-backport is not a reason to stay on an unsupported branch.

Which PHP to install

Patched PHP releases, 21 October 2026
Branch you runWhat the PHP project shippedWhat to do
8.58.5.12Update, then retest the site
8.48.4.27Update, then retest the site
8.38.3.36Update, then retest the site
8.28.2.35Update, then retest the site
8.1, 8.0 or 7.4A minimal source-backportPrefer a vendor package, then plan the upgrade
Older than 7.4No PHP-project backportTake it off the internet, then upgrade or replace it

This is a runtime bug, which is the awkward kind

Application bugs are local. A missing check in one controller is a patch in that controller. PHP2Shell is not like that. The dangerous code runs while PHP is still building the request, and again while it is tearing the request down. Your framework, your coding standard and your code review never see it.

That is the structural weakness. PHP's web SAPIs do a lot of C work before the first line of the application: reading the body, building the request arrays, managing per-request globals, and bailing out when something fatal happens. Those structures live in a worker that handles the next request too. A half-finished structure, or a pointer into freed memory, becomes the next request's reality.

A Python web application does not work this way. The program you write is Python. In ordinary Python you cannot express a buffer overflow, a use-after-free, or a half-initialised struct. Integers and strings are objects. When a function fails, the interpreter unwinds with an exception. It does not jump out of C and leave the previous request's globals for the next one.

The HTTP server is a separate process. gunicorn, uvicorn or uWSGI sit behind a reverse proxy. The interpreter is not compiled into Apache. A worker that dies is a failed request and a new process. It is not the web server's address space, and it is not every other site on that Apache.

Two limits belong in the same breath. CPython itself is written in C, and so are many extensions: image libraries, XML parsers, database drivers. Those can have memory bugs, and they have. A compromised worker is still a compromised application. Python does not make remote code execution impossible. It moves that class of bug out of the language you write every day, and out of the code path that turns a stranger's request body into the next request's interpreter state.

How a request becomes data

PHP's historic default is still the mental model of the language. The engine parses the query string and the form body and drops the result into superglobals before your code runs. For years it also dropped those fields into real variables. That setting, register_globals, was removed in 5.4. The instinct that request data is already variables never fully left. extract on the request is still a one-liner in old code. So is include of a path that came from the request.

Python frameworks go the other way. Nothing in the standard library maps a query string onto your local variables. Django gives you the query and the form when you ask. FastAPI and Pydantic parse the body into a model you declared, and reject the request when a field has the wrong shape. A missing field is an error. It is not a silent null that later gets cast.

That difference showed up in PHP2Shell in a blunt way. The attacker does not need a bug in your controller. On mod_php they may not need your controller to run at all. The engine's own parser is the attack surface. In a Django or FastAPI app, the equivalent surface is the framework and the server, which are versioned and usually updated without rewriting the product.

Types, and the comparisons that lie

PHP will still compare two numeric strings as numbers. "0e123" == "0e456" is true, because both are treated as zero in scientific notation. That has been a real authentication bug wherever a hash or a token was checked with == instead of ===. PHP 8 tightened some of the older casts. It did not remove this one. The safe comparison is a function you have to remember to call.

Python's == on two strings compares the strings. "0e123" == "0e456" is false. There is no second, looser equality sitting next to the normal one. A Django form or a Pydantic model either produces the type you asked for or it refuses the input. PHP will often coerce, and the request continues. The bug appears three layers down. Coercion is convenient. It is also how unexpected input becomes a branch you never tested.

The standard library kept the dangerous tools

PHP's unserialize will rebuild objects, and those objects can run code as they wake up or shut down. It has been the root of gadget-chain attacks for a decade, and it is still what some session and cache formats are built on. Dynamic include, extract, and assert on a string are all one call away, because they are the language.

Python has an equivalent footgun: pickle. Unpickling untrusted bytes is remote code execution. The difference is the default. Django's session backend is signed JSON, not pickle. A normal Django or FastAPI app never unpickles a request. You have to go and choose the dangerous parser. In a lot of PHP code the dangerous parser is how the framework already stored the session.

Templating has the same shape. A Django template escapes output by default. You mark a string safe when you mean to. PHP's echo prints what you give it. Laravel Blade and modern Twig escape by default too, so a current framework can close the gap. The language does not. Every custom template is a decision someone has to get right every time.

SQL follows the pattern. PHP's oldest database APIs were string interpolation, and plenty of plugins still concatenate. Prepared statements exist and are the right tool. Django's ORM parameterises queries on the default path. You can still write a raw query badly in both languages. One of them makes the safe path the path of least resistance.

What the ecosystem ships turned on

A new Django project starts with CSRF middleware, a clickjacking header, password hashes in the auth framework, and the debug page switched off the moment debug mode is false. None of that stops a careless developer. It does mean the empty project is not already open.

PHP's empty project is a file the web server will execute, often on a shared host, often with the interpreter inside Apache. The platform that made PHP successful is the same platform that made PHP2Shell so widely deployed: WordPress, Laravel, Magento, a booking form left on PHP 7.4, a Docker image nobody rebuilt. PHP is often a component inside other software, so an inventory of "the PHP app" misses most of it.

Python is not free of ordinary incidents. A forgotten debug mode, a URL fetched from user input with no allow-list, a secret in a repository, a compromised package: all of those happen. Supply-chain risk is shared. The difference is density. PHP is still the default language of a large part of the public web, and its interpreter is the thing exposed to the request. When the interpreter breaks, every site on that build breaks with it.

What a rewrite does not fix

Choosing Python does not patch a server that is still running an older PHP. If you operate PHP today, the work is the upgrade: 8.5.12, 8.4.27, 8.3.36 or 8.2.35, on the branch you are already on. Then find the copies that are not that version. Containers, CI images, the microsite on the old host, the admin tool someone installed once.

Choosing Python also does not remove the bugs you write yourself. Broken access control, server-side request forgery, a template marked safe, a raw SQL string, a pickle load on a queue you do not fully trust. Those are application bugs, and Python will run them faithfully.

What Python changes is the floor. The code your team reviews is memory-safe. Request data stays in objects you asked for. The dangerous deserialiser is not the session format. The template escapes until someone opts out. And a flaw on the scale of PHP2Shell, a memory-corruption bug in the engine, reachable before your code runs, shared across every product on that runtime, is not the failure mode of a Django or FastAPI process.

If the product you have is PHP and the runtime can move to 8.2.35 or newer, do that this week. If it is stuck on 8.1 or older because every upgrade breaks it, the runtime is no longer a patch. It is a plan to leave. Where that plan is a Django rebuild, the shape of the work is on Django development and PHP development and maintenance.

Email [email protected] or send a brief with the version from php -v and whether the site is on the public internet. If you do not know the version, say that.

Questions

What is PHP2Shell?

The public name for two critical PHP bugs disclosed on 21 October 2026: CVE-2026-109618 and CVE-2026-109619, both scored CVSS 9.2. They can allow unauthenticated remote code execution on PHP that serves web requests. The fixed releases are PHP 8.5.12, 8.4.27, 8.3.36 and 8.2.35.

Does Python have the same bug?

No. PHP2Shell is in PHP's own request engine. Ordinary Python cannot express that kind of memory corruption, and a Django or FastAPI process is not embedded in Apache. Python can still have application bugs, and C extensions can have memory bugs of their own.

What if we are on PHP 8.1 or older?

Those branches no longer receive normal PHP updates. The PHP security team published minimal source-backports for 7.4, 8.0 and 8.1, and some vendors shipped packages. Anything older than 7.4 has no PHP-project backport. Updating onto a supported release is the safe path.

Need this on your codebase? Talk to us about Django development.
See Django development
R
Rizwan Mansuri · Founder, WEBBYFOX Builds and upgrades Python web applications for UK teams. More about us.

Keep reading