Archtin
All articles
Backend EngineeringSecurityPHP16 min read

PHP2Shell Explained: Two PHP Runtime Flaws, From Request to Risk

Understand PHP2Shell's out-of-memory corruption and stale-state bugs with step-by-step diagrams, attacker impact, patched versions, and official guidance updated October 23, 2026.

Security update · checked October 23, 2026
Update the PHP runtime, not just your application. PHP's October 23 follow-up also recommends disabling session upload progress until the next regular bugfix release for an unusual additional RCE vector. Read PHP's current advisory.

What happened: the runtime became the attack surface

On October 21, 2026, the PHP project released emergency fixes for two critical vulnerabilities collectively called PHP2Shell: CVE-2026-109618 and CVE-2026-109619. Both concern the core PHP runtime's handling of memory when requests fail. They are not ordinary SQL-injection bugs, insecure Laravel controllers, or WordPress-plugin vulnerabilities. A well-written application can still sit on top of an affected runtime. PHP's announcement identifies all PHP installations serving web requests as at risk, including PHP-FPM, Apache with mod_php, php-cgi, FrankenPHP, and LiteSpeed.

The PHP team considers Apache mod_php the highest-risk configuration, with a demonstrated unauthenticated attack that can trigger before application code runs. That detail matters: middleware cannot protect a path that is reached before the middleware executes. It does not mean every deployment has an identical exploit path or that every malformed request succeeds.

IssueBroken invariantPotential security outcome
CVE-2026-109618Cleanup must not treat incomplete state as fully initializedMemory corruption and remote code execution
CVE-2026-109619A reference must not outlive request-owned memoryUse-after-free, memory disclosure, and remote code execution

Google Cloud reports a critical CVSS 9.2 rating for both flaws. Severity describes the risk under the scoring model, not the percentage of servers already compromised. The exact trigger, memory layout, worker configuration, and available execution path affect exploitability. Google Cloud's response provides operational context.

A conceptual explanation, not an exploit recipe
The diagrams below simplify the runtime to make object lifetime and failure handling understandable. They are not exact PHP structures, payloads, or instructions for exploiting a live server.

Watch: both flaws explained in 72 seconds

Conceptual motion graphics with on-screen explanations and background music; no spoken narration. Covers both original CVEs and potential worker-level impact, not an exploit demonstration. The October 23 follow-up is explained separately below.

First: understand a normal PHP request

Think of PHP as a runtime inside a web-serving process, not merely the code in your repository. Before your application can decide whether to authorize an action, the runtime must create enough request state for the application to run.

  1. The HTTP request arrives. A web server receives headers and, where applicable, a body containing forms or upload data.
  2. PHP starts the request. Its native runtime initializes request-specific structures and processes relevant input.
  3. Application code executes. Framework routing, login checks, validation, and business logic now have their turn.
  4. The response is returned. The normal result or error is sent to the client.
  5. Request resources are cleaned up. Memory and references belonging to this request must be released or reset. A persistent worker can then serve another request.
Request lifecycle: HTTP enters PHP runtime before application authentication, then response and cleanupHTTP requestPublic entry pointPHP runtimeParse requestBuild internal stateApplicationLogin checksBusiness logicResponseReturn to clientRequest cleanupFree request memoryReset internal referencesA runtime flaw can sit before your login screen.A pre-application attack path was demonstrated for mod_php.Persistent workers repeat this lifecycle for later requests.
The runtime sits before application authentication. Every exit path must preserve safe cleanup and reset, not only the successful path.

Three memory concepts to keep in mind

  • Allocation: reserve a region of memory for an object or buffer.
  • Initialization: write the fields that make that object valid. Allocated memory is not automatically a completely initialized object.
  • Lifetime: the interval during which that memory belongs to the object. After it is freed, an old pointer does not remain valid.

Both bugs are violations of these contracts during abnormal execution. A request that runs to completion may never encounter the broken path. A request that fails midway can expose assumptions the successful path hides.

Bug 1 — CVE-2026-109618: initialization stops, but cleanup continues

1. PHP begins building internal request state

A request parser may need multiple allocations to construct its internal representation. Some fields become valid before others. For example, imagine a structure with a name, a value, and metadata. This is a teaching model, not the exact affected PHP structure.

RequestState
  name      → initialized
  value     → initialized
  metadata  → not initialized yet
An illustrative object halfway through construction — not PHP implementation code

2. An allocation reaches an out-of-memory path

PHP has a configurable limit on PHP-managed memory for a request. A later allocation can reach that limit while initialization is incomplete. Native allocation failures can also matter; “out of memory” is not exclusively a machine having no RAM left.

PHP's failure handling can take a non-local bailout rather than returning normally through every intermediate function. A useful analogy is an emergency exit in the middle of assembling a machine: the assembly sequence stops, but the half-built machine is still on the table. An emergency jump does not automatically undo previous writes or establish all the invariants cleanup expects.

3. Cleanup interprets the partial structure incorrectly

The runtime must clean up after the failed request. If cleanup assumes initialization finished, it can interpret uninitialized or invalid data as trustworthy internal state. That may lead to an invalid access or incorrect memory operation. PHP summarizes the issue as cleanup treating invalid internal request data as valid. The PHP security advisory for CVE-2026-109618 is the authoritative technical reference.

Out-of-memory error interrupts initialization; unsafe cleanup treats incomplete state as valid1 · Begin initializationAllocate request structureSome fields are ready2 · Memory limit reachedNext allocation failsEmergency bailout3 · Unsafe cleanupAssumes complete stateReads invalid metadataIllustrative partially built objectname: initializedvalue: initializedmetadata: not initializedPossible outcomesCrash or memory corruptionUnder exploit conditions: RCEA memory-limit error alone does not imply compromise.
Bug 1 breaks the transition from construction to destruction. A failed allocation must not leave cleanup guessing which fields are valid.

4. Why this is more serious than a failed upload

A normal rejected upload should produce a controlled failure. Memory corruption is different: it can change how the runtime interprets memory rather than merely rejecting a request. Researchers demonstrated remote-code-execution capability through these flaws. Turning corruption into a reliable exploit requires suitable conditions and more than “send one large file.” Not every memory-limit error is an attack, and not every crash proves successful exploitation.

Memory limits are not the vulnerability itself
A finite memory limit is ordinarily a useful availability control. The bug is failure handling that cannot safely cope when an allocation fails. Removing the limit is only a temporary, path-specific measure with its own operational risks—not a substitute for patching.

Bug 2 — CVE-2026-109619: memory is freed, but a reference survives

1. Request A creates data inside a persistent worker

In a persistent-worker model, one PHP process handles multiple requests over time. Request A's memory should be valid only for A's lifetime. Internal runtime state can temporarily refer to that memory. Here, “global state” means state inside the PHP engine's execution context, not necessarily an application variable declared with PHP's global keyword.

2. An abnormal exit frees the backing memory

A fatal error or another failing path causes cleanup. The request-owned memory is released, but an internal reference to it survives. Releasing the allocation and clearing every reference to it are separate responsibilities. Completing the first does not guarantee the second.

3. The allocator can reuse the region

The freed memory may later hold something completely different. An old pointer has no knowledge that ownership changed. If the runtime follows it, the bytes may still exist, but they no longer represent the object the runtime thinks they do. This is why a use-after-free can be dangerous even when the address is readable and the program does not immediately crash.

4. Request B reaches stale state before reset

PHP's advisory describes a later request handled by the same worker using stale state before normal initialization resets it. This is a lifetime boundary inside that worker. It is not a claim that any two unrelated requests anywhere in a server fleet automatically share the same vulnerable state. The PHP advisory for CVE-2026-109619 explains the affected internal paths.

Same-worker request sequence: memory is freed but a runtime reference survives and is reused incorrectlyOne persistent PHP worker · two request lifetimesRequest ACreates request dataRuntime holds a referenceFatal error / cleanupBacking memory freedReference is not resetRequest BSame workerStale state reached too earlyLingering referenceStill points to old locationReused memory regionNo longer Request A's dataMay contain different dataUse-after-freeRead or interpret memorywith the wrong lifetime.“Global” here means internal runtime state, not your application's PHP variables.
Bug 2 is a cross-request lifetime mismatch: a surviving reference is longer-lived than its target.

5. The runtime reads or interprets the wrong data

A stale-state access can cause a crash, disclose memory, or contribute to corruption and code execution under appropriate exploit conditions. The critical mistake is not simply forgetting to erase sensitive bytes. The runtime is accessing data through a reference whose ownership and lifetime are no longer valid.

A cleared buffer is not enough
Zeroing an old region does not repair every dangling reference or make its later reuse safe. Correct lifetime management must prevent access to released objects and reset request-owned state before it can be reused.

Related bugs, different broken transitions

QuestionOut-of-memory corruptionStale global state
When does the invariant fail?During interrupted initialization and cleanupAfter cleanup, before later state reset
What is the mistaken assumption?The object is fully initializedThe reference still points to live request data
What lifetime matters?A partially constructed objectRequest-owned data across a worker's requests
The safe mental modelDestruction must handle partial constructionNo access after release; reset before reuse

The defects share a root theme: the runtime must remain correct when control flow stops being ordinary. A patch must fix the engine's invariants, not merely add a validation rule to the application. The two CVEs are distinct; explaining them together does not establish that every attack needs to chain them.

Why this article follows PHP's CVE mapping
Some researcher write-ups group individual exploitation variants differently. Here, CVE-2026-109618 means the OOM-corruption root cause and CVE-2026-109619 means stale global state, following PHP's official labels. Do not assume every variant table in a secondary account maps cleanly to those root causes.

How could attackers benefit?

At a high level, an attacker reaches an exposed PHP web-serving path, causes abnormal processing under suitable conditions, and exploits the resulting invalid memory behavior. The practical consequences depend on the deployment and what the compromised worker can access. We deliberately omit payloads and memory-manipulation recipes.

Potential runtime compromise is limited initially to worker privileges; secrets and permissions determine further impactRuntime exploitationUnauthenticated pathConfiguration-dependentPHP worker privilegesNot automatically rootExisting permissions applyConfidentialityReadable secrets / dataIntegrityWritable files / recordsAvailabilityCrashes / disruption
Initial execution is in the worker's security context. Least privilege limits impact; it does not make the runtime bug harmless.
  • Read secrets or sensitive data: memory disclosure may expose data present in the process; worker-level code execution may make readable configuration, credentials, or application data accessible.
  • Modify what the worker can write: site content, upload directories, or database records could be affected if the worker's permissions and credentials allow those actions.
  • Interrupt service: repeated crashes or resource pressure can disrupt requests. Availability impact can occur even without a successful code-execution chain.
  • Attempt further compromise: accessible credentials and network permissions can create opportunities to reach other systems. Lateral movement is a possible consequence, not an automatic property of the CVE.

Remote code execution does not mean automatic root access. If the worker runs with constrained privileges, a read-only filesystem, narrow database permissions, and limited network access, the blast radius can be smaller. Conversely, a worker with broadly privileged credentials has a larger blast radius. A login screen and a managed cloud host are not, by themselves, proof that an affected runtime is protected.

Why application-only fixes miss the point

Updating a CMS or adding controller validation remains worthwhile, but it is not the same as updating PHP. Some vulnerable processing happens outside application code; the mod_php pre-application path is specifically demonstrated. Compare that boundary with the normal request journey in how an HTTP request works and with the application-level boundary in SQL injection explained.

What happened in the real world?

Date (2026)Reported eventWhat it establishes
October 17An exploitation attempt observed by Amazon Threat Intelligence, cited in Sophos's advisoryAttempted exploitation preceded public disclosure; not proof every target was compromised
October 20Advance notice of an urgent PHP security releaseOperators were given a patch-preparation window
October 21PHP released fixes; ENISA says updates issued at 13:40 UTCSupported fixed releases and coordinated response became available
October 22Vendor advisories and detection guidance updatedOperational recommendations continued to evolve
October 23PHP issued session-upload-progress follow-upA configuration-specific additional vector needs the published precaution

Sources: Amazon Threat Intelligence's primary MadPot report, Sophos's dated advisory, ENISA's joint statement, and PHP's news archive. AWS explicitly says it was unclear whether the observed attempts would have succeeded against real-world systems. Attempts justify urgent patching and investigation, but do not establish compromise of a particular server. This article does not claim a verified worldwide victim count.

What about the “70% of the Internet” headline?

Wiz's research disclosure emphasizes PHP's widespread use and the role of frontier AI in discovering and investigating the flaws. PHP's popularity helps explain the large potential attack surface. It should not be interpreted as “70% of all internet machines were compromised” or as a measurement of successful attacks. Exposure, exploitability, attempted exploitation, and confirmed compromise are different metrics.

What to update, and how to verify it

The October 21 fixed releases listed by the PHP project are:

Supported branchAffected upstream stable releasesOctober 21 fixed release
PHP 8.58.5.0–8.5.118.5.12
PHP 8.48.4.0–8.4.268.4.27
PHP 8.38.3.0–8.3.358.3.36
PHP 8.28.2.0–8.2.348.2.35

These are the named upstream fixes for the two CVEs, not a recommendation to stop applying future updates. PHP 8.1 and older no longer receive normal upstream updates. Some distributions and LTS providers backport fixes; PHP also published minimal source backports for 7.4, 8.0, and 8.1. A backport requires explicit vendor evidence—the old version number alone neither proves safety nor proves the backported package is unpatched. Very old installations should move to a supported runtime or be handled through a verified vendor remediation plan.

Remediation flow: inventory running PHP, update runtime, restart workers, verify serving versionInventoryWeb runtimeBundled PHP tooPatchFixed upstream releaseOr verified vendor fixRestart / redeployReplace old workersRebuild old imagesVerifyServing runtime fixedInvestigate past exposure
A package update is not complete until old workers and old images are replaced and the serving runtime is verified.
  1. Inventory the actual web runtime. Include all FPM pools, Apache modules, containers, server images, hosted applications, and appliances bundling PHP. A CLI version can differ from the version handling web requests.
  2. Check the vendor advisory and install the fix. Use your distribution or image vendor's supported upgrade process. Rebuild container images and retire old image tags where appropriate.
  3. Restart or redeploy affected workers. Installed files do not guarantee a long-lived process has stopped running the old runtime. Include every replica and pool.
  4. Verify privately. Confirm the running web SAPI and package/build provenance through trusted operational tooling. Do not leave a public phpinfo() endpoint exposing configuration.
  5. Smoke-test application behavior. Check login, forms, upload flows, sessions, queues that use the same image, and error handling after the update.
  6. Review the period of exposure. Preserve logs and inspect crashes, unexpected files, suspicious processes, outbound connections, and credential use. If compromise is suspected, isolate and investigate rather than only restarting.
Patching prevents future exploitation; it does not undo past compromise
A fixed runtime does not remove a file an attacker already wrote or invalidate a secret already exposed. Incident response may require clean redeployment and credential rotation based on evidence and exposure.

Important October 23 follow-up: session upload progress

The latest PHP informational advisory recommends setting session.upload_progress.enabled=0 until the next regular bugfix release to fully protect against an unlikely additional RCE vector. This precaution was already listed with the stale-state advisory on October 21; if you applied it, PHP says to keep it in place.

; php.ini / server-managed PHP configuration
session.upload_progress.enabled = 0
Defensive server configuration; apply before request parsing and restart workers

PHP says the additional issue requires an unusual combination of configuration and application behavior involving a runtime change of the session serialization handler while upload progress remains enabled. Applications that never change the handler at runtime can keep that feature enabled, according to PHP's notice. PHP names CVE-2026-115560 for the forthcoming full advisory. This is a follow-up, not a renaming of the original two PHP2Shell CVEs. As of this article's check, the notice points to the next regular release rather than naming a newly released fix for this additional vector.

Operationally: keep the original runtime patches, check whether upload-progress functionality is actually needed, and follow PHP's latest guidance. Do not infer that the October 21 update alone resolves every newly discussed configuration-specific issue.

If you cannot patch immediately

PHP is explicit: only updating is a complete solution for the original vulnerabilities. Interim measures reduce particular attack paths and have trade-offs. ENISA recommends considering isolation from public networks when immediate remediation is not possible.

Out-of-memory path

The official interim guidance includes removing the controllable PHP memory-limit failure with a non-overridable memory_limit=-1, limiting decoded input at the web server or proxy, and using container/cgroup boundaries for availability. This is an emergency operational trade-off, not a safe generic tuning tip: underlying allocation failures can still reach the vulnerable path, and unbounded PHP memory without external resource controls can increase denial-of-service risk.

PHP suggests small post_max_size values—8M for applications not requiring uploads, 32M where uploads are necessary—with upload_max_filesize below that value. Proxy limits should cover decoded request size and structural counts, including requests without a usable Content-Length. The advisory warns against substituting RLIMIT_AS or RLIMIT_DATA, since they can cause the vulnerable allocation failure. Apply such changes with the hosting or operations team after checking application requirements.

Stale-state paths

  • Disable session upload progress in configuration that takes effect before request parsing, then restart all workers. PHP says not to rely on application ini_set() or FPM .user.ini for this mitigation.
  • For PHP 8.5, the advisory also lists disabling fatal_error_backtraces to block the confirmed fatal-backtrace path.
  • Remove invalid/deprecated per-request configuration that emits diagnostics before initialization. Hiding messages with display settings or @ does not fix the underlying path.
  • Fix reachable fatal errors and execution timeouts, including failures in callbacks and cleanup. These measures do not cover every possible variant.

A WAF rule can be another layer, not a guarantee. Signatures may cover known requests while missing a different route to the runtime bug. Consult PHP's full mitigation text rather than copying one setting and assuming the server is safe.

What engineers should learn from PHP2Shell

Failure paths are part of the security model

Tests that only send valid requests miss the exact transitions these bugs exploit. Runtime and native-extension authors need to exercise allocation failures, partial initialization, callbacks that fail, cleanup errors, and repeated requests in the same process. The question is not just “did an error return?” but “what state survives after the error?”

Object construction and destruction need a shared contract

Initialize safe defaults before failure-prone work; track which resources have actually been acquired; ensure cleanup can handle partially built objects. In native code, a non-local jump can bypass ordinary cleanup assumptions. The correct pattern depends on the codebase, but the invariant is universal: destruction must not require work that might never have completed.

Request isolation is also a memory-lifetime property

A stateless HTTP API can still run inside a stateful process. Persistent workers, pools, and caches intentionally outlive individual requests. Request-owned pointers must not accidentally inherit that longer lifetime. Error-recovery testing should include A-fails-then-B-runs sequences, not only isolated requests in fresh processes.

Least privilege limits consequences

Run workers with constrained identities, keep runtime images reproducible, reduce writable directories, give database accounts narrow permissions, and restrict unnecessary egress. These controls do not repair memory corruption, but they can reduce what an attacker gains if a layer fails. For the deployment side, continue with how code flows from local to production.

The interview-ready mental model
Bug 1: initialization is interrupted, then cleanup trusts incomplete state. Bug 2: memory is released, then a surviving reference is used by a later request. Both show why security must survive abnormal control flow—not merely valid user input.

Official sources and further reading

Prioritize the live PHP advisories over summaries. This article was checked on October 23, 2026; subsequent fixes and investigation findings can change operational guidance.

Keep reading

Suggested next articles based on this one.

Design it, don't just read it.

Practise LLD and system design problems with structured rubrics and AI feedback.

Start practising free