PHP2Shell Explained: How CVE-2026-109618 and CVE-2026-109619 Let Hackers Hijack a PHP Server Without a Password

A new PHP flaw nicknamed PHP2Shell lets an attacker take over a server with a single request — no login, no stolen password, no bug in your code required. Here's exactly how CVE-2026-109618 and CVE-2026-109619 work, how to check if you're exposed with a free script, and how to fix it before someone else finds it first.

An incoming request bypasses a lock and reaches a server, illustrating PHP2Shell unauthenticated PHP server takeover.

Picture this: your website is running fine. No odd logins, no sketchy plugin, nothing unusual in the error log. And yet, somewhere on the other side of the world, a complete stranger who has never seen your admin password, never guessed your email, and never touched a single line of your code could be seconds away from running whatever they want on your server.

That's not a hypothetical anymore. As of October 21, 2026, it's the reality for a huge slice of the internet — anything running PHP without this week's patch. Security researchers are calling it PHP2Shell, and it's scored a rare 9.2 out of 10 on the CVSS severity scale for a simple reason: it doesn't exploit a weakness in your website. It exploits PHP itself.

TL;DR: Two critical bugs — CVE-2026-109618 and CVE-2026-109619, together nicknamed PHP2Shell — let an attacker run code on your server with one unauthenticated request. Fixed in PHP 8.2.35, 8.3.36, 8.4.27, and 8.5.12. PHP 7.4, 8.0, and 8.1 only get an emergency source patch, not a full release. If you haven't updated and restarted every PHP worker since October 21, 2026, you're probably still exposed. Jump to the free detection script if you want the short version.

What Exactly Is PHP2Shell?

PHP2Shell is the nickname the security community gave to a pair of vulnerabilities PHP patched together on October 21, 2026: CVE-2026-109618 and CVE-2026-109619. Both are classified as unauthenticated remote code execution (RCE) — the single most dangerous category a vulnerability can fall into, because it means the attacker needs absolutely nothing from you. No stolen credentials, no phishing click, no vulnerable plugin. Just a web request to any page your server happens to be serving.

Why the name? It riffs on the oldest trick in the web-hacking playbook — the "web shell," a snippet of attacker code smuggled onto a server to hand back remote command-line control. Normally that takes a file upload flaw or a code-injection bug in your app. PHP2Shell skips all of that and gets to shell-level control through a flaw in PHP itself, underneath every application running on it.

This Isn't a Theoretical Risk — It's Already Being Used

Within hours of the patch going live, security researchers at watchTowr reported active exploitation attempts against both flaws — timestamped 14:19 UTC on October 21, the very same day PHP published the fix. Cloudflare confirmed it was already seeing attack traffic, and more worryingly, watching attackers adjust their payloads in real time to slip past web application firewalls. Cloudflare's CTO put it bluntly:

"We are seeing early attacks emerging and are updating mitigations as new vectors show up."

PHP core developer Gina Peter Banyard summed up the fix in one sentence worth pinning to every sysadmin's monitor:

"The only mitigation is patching/upgrading PHP."

(Quotes reported by The Hacker News.)

The Two Bugs Behind PHP2Shell, in Plain English

Both flaws live inside the PHP engine itself — not in your application code. That means even a single static "Hello World" page, running on an unpatched PHP install, is exposed. No application bug required.

CVE-2026-109618 — RCE via Out-of-Memory Corruption

PHP has long used a mechanism called longjmp() to bail out instantly whenever it runs out of memory mid-request — think of it as an emergency eject button. The problem: PHP sometimes pulls that eject lever before a request's data has been safely packed away. The cleanup routine that runs right after then reaches into that half-built, half-initialized memory and acts on whatever it finds there — which, in the worst case, can be steered into executing attacker-controlled instructions.

What makes it worse: PHP automatically starts parsing incoming data (like file uploads) before your application code even runs. That gives an attacker a shot at triggering the "eject" moment at the very first stage of a request — which is exactly why Apache's mod_php is considered the highest-risk setup: it keeps running its shutdown routine even when startup already failed.

CVE-2026-109619 — RCE via Stale Global State

To save resources, PHP-FPM workers handle one request after another on the very same process. Normally, every request's data gets wiped clean before the next one starts. This bug means that under certain conditions — a fatal error, a timeout, or that same out-of-memory moment above — some of that data never gets fully wiped. It just sits there, in memory the system already considers "free."

Then a second request lands on that same worker and touches a feature called session upload progress — on by default in PHP, and it doesn't even require a login or an active session to reach. That feature ends up reading the leftover data instead of fresh data: a textbook use-after-free bug, one of the oldest tricks in the memory-corruption playbook for turning "the program got confused" into "the attacker is in control."

Who's Actually Affected?

Screenshot From 2026 10 22 13 32 25

PHP BranchStatusFixed Version8.5Supported8.5.128.4Supported8.4.278.3Supported8.3.368.2Supported — but reaches end-of-life Dec 31, 20268.2.358.1 / 8.0 / 7.4End of life — emergency source backport only, no full releaseSee PHP's GitHub backport branches

At-risk server setups include Apache with mod_php (highest risk — PHP's own advisory notes it can be triggered before any application code runs), PHP-FPM, php-cgi, FrankenPHP, and LiteSpeed. If your site runs PHP and serves live web traffic, assume you're affected until you've actually confirmed otherwise.

Is Your Server Vulnerable? Here's How to Actually Check

Here's the part that trips people up: these are memory-corruption bugs, not a misconfigured setting you can spot by poking your own website from a browser. There's no version banner, no telltale header, nothing a normal request reveals. And you genuinely shouldn't try to "test" it by throwing attack-shaped traffic at your own live server — the only way to see whether a memory-corruption trigger "works" is to actually corrupt memory, which risks crashing or compromising production for a fact a one-line version check already gives you for free.

The reliable way is to check the version running where requests actually land — which isn't always what a quick php -v in your terminal tells you, since that can point at a completely different install than the one serving your website.

Below is a read-only Bash script that:

  • Finds every PHP process actually serving web traffic — FPM pools, Apache mod_php, php-cgi, and more

  • Compares each running version against the real fixed releases

  • Audits the specific config flags that widen or narrow your exposure (session.upload_progress.enabled, memory_limit, post_max_size)

  • Flags the single most common mistake: a patched package sitting under a worker process that was never restarted — which is still fully vulnerable

  • Auto-generates the exact fix commands for your OS and package manager

It changes nothing on your server by default. It only reads.

#!/usr/bin/env bash
#==============================================================================
# php2shell_audit.sh  —  advanced detector + auto-remediation advisor
#   CVE-2026-109618 (RCE via OOM corruption) + CVE-2026-109619 (RCE via stale state)
#   Fixed: PHP 8.2.35 / 8.3.36 / 8.4.27 / 8.5.12   (7.x/8.0/8.1 = EOL -> backport/upgrade)
#
# WHAT IT DOES
#   * Finds every PHP binary, FPM pool, and mod_php that serves web traffic
#   * Reads the RUNNING version of each (not just the CLI default)
#   * Marks each web-facing branch VULNERABLE / PATCHED / EOL
#   * Audits the config that gates exposure (session.upload_progress, memory_limit,
#     post_max_size, fatal_error_backtraces, pm.max_requests)
#   * Detects "patched binary but workers never restarted"
#   * AUTO-PICKS the exact remediation for THIS host (distro-aware) and prints it
#   * Exit code 2 if anything is VULNERABLE/AT-RISK (handy for CI / cron)
#
# MODES
#   (default)   read-only: detect + print the recommended commands. Changes nothing.
#   --apply     after a y/N confirm, actually runs the SAFE recommended fixes:
#               package upgrade + restart workers + session.upload_progress.enabled=0
#               (memory_limit/post_max_size changes are PRINTED, not auto-applied,
#                because they carry trade-offs you should choose deliberately)
#   --json      also emit a one-line JSON summary
#
# USAGE
#   bash php2shell_audit.sh                 # inspect this host (read-only)
#   ssh user@host 'bash -s' < php2shell_audit.sh
#   sudo bash php2shell_audit.sh --apply    # detect, then fix the safe items
#==============================================================================
set -u

APPLY=0; JSON=0
for a in "$@"; do case "$a" in
  --apply) APPLY=1 ;; --json) JSON=1 ;;
  -h|--help) sed -n '2,40p' "$0" | sed 's/^#//'; exit 0 ;;
  *) echo "unknown arg: $a"; exit 64 ;;
esac; done

declare -A FIX=( [8.2]=8.2.35 [8.3]=8.3.36 [8.4]=8.4.27 [8.5]=8.5.12 )
RISK=0                      # 0 ok, 2 action needed
declare -a ACTIONS=()       # human-readable recommended steps
declare -a PKG_UPGRADE=()   # packages to upgrade
declare -a SVC_RESTART=()   # services to restart
declare -a INI_UPROGRESS=() # ini files to set session.upload_progress.enabled=0

c_red(){ printf '\033[31m%s\033[0m' "$1"; }; c_grn(){ printf '\033[32m%s\033[0m' "$1"; }
c_yel(){ printf '\033[33m%s\033[0m' "$1"; }; c_bld(){ printf '\033[1m%s\033[0m' "$1"; }
hr(){ printf '%s\n' '--------------------------------------------------------------------'; }
have(){ command -v "$1" >/dev/null 2>&1; }
ver_lt(){ [ "$1" != "$2" ] && [ "$(printf '%s\n%s\n' "$1" "$2" | sort -V | head -1)" = "$1" ]; }
php_ver(){ "$1" -r 'echo PHP_VERSION;' 2>/dev/null; }   # exact x.y.z

#--- distro / package manager detection ---------------------------------------
OS_ID=""; OS_LIKE=""
[ -r /etc/os-release ] && . /etc/os-release && OS_ID="${ID:-}" && OS_LIKE="${ID_LIKE:-}"
PKG="";
if have apt-get; then PKG=apt
elif have dnf; then PKG=dnf
elif have yum; then PKG=yum
fi
# config trees that may hold php.ini (Debian + RHEL/Remi + Amazon)
INI_DIRS=(/etc/php/*/fpm /etc/php/*/apache2 /etc/php/*/cgi \
          /etc/php.ini /etc/php.d /etc/php-fpm.d \
          /etc/opt/remi/php*/php.ini /etc/opt/remi/php*/php.d)

echo "$(c_bld "PHP2Shell audit") — host: $(hostname) — $(date -u '+%F %H:%M UTC')"
echo "distro: ${PRETTY_NAME:-$OS_ID}   pkgmgr: ${PKG:-unknown}"
echo "fixed versions: 8.2.35 / 8.3.36 / 8.4.27 / 8.5.12"
hr

#--- 1. web-facing SAPIs & their branches -------------------------------------
echo "$(c_bld '1) SAPIs serving web requests')"
declare -A WEBBR=()   # branch -> how it serves
# running FPM masters
while read -r line; do
  br=$(printf '%s' "$line" | grep -oE 'php/[0-9]+\.[0-9]+/fpm|php-fpm[0-9]+\.[0-9]+' | grep -oE '[0-9]+\.[0-9]+' | head -1)
  [ -n "$br" ] && WEBBR[$br]+="fpm "
done < <(ps -eo cmd 2>/dev/null | grep -E 'php-fpm|php/[0-9].*fpm' | grep -v grep)
# mod_php (Debian apache module)
for m in /etc/apache2/mods-enabled/php*.load; do
  [ -e "$m" ] || continue
  br=$(basename "$m" .load | grep -oE '[0-9]+\.[0-9]+'); [ -n "$br" ] && WEBBR[$br]+="mod_php(apache) "
done 2>/dev/null
# mod_php (RHEL httpd)
if have httpd; then
  br=$(httpd -M 2>/dev/null | grep -qi php && php_ver "$(command -v php)" | grep -oE '^[0-9]+\.[0-9]+'); :
fi
ps -eo etimes,cmd 2>/dev/null | grep -Ei 'php-fpm|apache2|httpd|php-cgi|frankenphp|lsphp|litespeed' | grep -v grep \
  | awk '{printf "   up=%ss  %s\n",$1,substr($0,index($0,$2))}' | head -20
if [ "${#WEBBR[@]}" -eq 0 ]; then echo "   (no running PHP web SAPI detected; checking installed versions anyway)"; fi
hr

#--- 2. version verdict -------------------------------------------------------
echo "$(c_bld '2) Version verdict')"
# gather candidate branches: web-facing first, then any installed CLI
declare -A SEEN=()
check_branch(){  # branch, running_version, how
  local br="$1" v="$2" how="$3" f="${FIX[$br]:-}"
  [ -n "${SEEN[$br]:-}" ] && return; SEEN[$br]=1
  if [ -z "$f" ]; then
    echo "   PHP $br ($v) [$how] -> $(c_red 'EOL branch'): no upstream fix; backport or upgrade  [AT RISK]"
    RISK=2; ACTIONS+=("EOL: migrate PHP $br -> a supported branch (8.3/8.4), or apply vendor backport")
  elif [ "$v" = "?" ]; then
    echo "   PHP $br (version unknown) [$how] -> confirm running >= $f"
  elif ver_lt "$v" "$f"; then
    echo "   PHP $br $(c_red "$v  <  $f") [$how] -> $(c_red '>>> VULNERABLE <<<')"
    RISK=2
  else
    echo "   PHP $br $(c_grn "$v >= $f") [$how] -> patched (verify workers restarted, §4)"
  fi
}
# web-facing branches
for br in "${!WEBBR[@]}"; do
  v="?"; for cand in "php-fpm$br" "php$br" php; do have "$cand" && v=$(php_ver "$(command -v "$cand")") && [ -n "$v" ] && break; done
  [ -z "$v" ] && v="?"
  check_branch "$br" "$v" "${WEBBR[$br]}"
done
# any other installed php CLIs (not serving, but vulnerable binaries on disk)
for b in php php7.2 php7.3 php7.4 php8.0 php8.1 php8.2 php8.3 php8.4 php8.5; do
  have "$b" || continue; v=$(php_ver "$(command -v "$b")"); [ -z "$v" ] && continue
  br=$(printf '%s' "$v" | grep -oE '^[0-9]+\.[0-9]+')
  [ -n "${SEEN[$br]:-}" ] && continue
  check_branch "$br" "$v" "installed (not actively serving)"
done
hr

#--- 3. config exposure / mitigation ------------------------------------------
echo "$(c_bld '3) Config exposure')"
scan=$(grep -rhsE '^[[:space:]]*(memory_limit|post_max_size|upload_max_filesize|session\.upload_progress\.enabled|fatal_error_backtraces|pm\.max_requests)[[:space:]]*=' \
        "${INI_DIRS[@]}" 2>/dev/null | grep -vE '\.(dpkg|ucf|p2s)-')
[ -n "$scan" ] && printf '%s\n' "$scan" | sed 's/^/   /' || echo "   (no matching directives found)"
# session.upload_progress default-ON check per ini tree
for d in "${INI_DIRS[@]}"; do
  [ -d "$d" ] || continue
  if ! grep -rqsE '^[[:space:]]*session\.upload_progress\.enabled[[:space:]]*=[[:space:]]*0' "$d" 2>/dev/null; then
    # exposed (either set to 1 or unset -> default On)
    main="$d/php.ini"; [ -f "$main" ] || main=$(ls "$d"/php.ini 2>/dev/null | head -1)
    [ -n "$main" ] && INI_UPROGRESS+=("$main")
  fi
done
if [ "${#INI_UPROGRESS[@]}" -gt 0 ]; then
  echo "   $(c_yel '! session.upload_progress.enabled is ON/undefined') => CVE-2026-109619 session path exposed"
  RISK=2
fi
# finite memory_limit (OOM path) + large post_max_size
if printf '%s' "$scan" | grep -qE 'memory_limit[[:space:]]*=[[:space:]]*[0-9]'; then
  echo "   $(c_yel '! memory_limit is finite') (not -1) => CVE-2026-109618 OOM path remains reachable"
fi
big=$(printf '%s' "$scan" | grep -E 'post_max_size' | grep -oE '[0-9]+M' | sed 's/M//' | sort -rn | head -1)
if [ -n "$big" ] && [ "$big" -gt 32 ] 2>/dev/null; then
  echo "   $(c_yel "! post_max_size up to ${big}M") widens the OOM window; reduce to 8M (32M if uploads needed)"
  ACTIONS+=("HARDEN: reduce post_max_size (<=8M, or 32M w/ uploads) and keep upload_max_filesize below it")
fi
hr

#--- 4. worker freshness ------------------------------------------------------
echo "$(c_bld '4) Worker freshness (patched binary only helps after a restart)')"
if have systemctl; then
  for svc in $(systemctl list-units --type=service --state=running 2>/dev/null \
               | grep -oE 'php[0-9.]*-fpm\.service|php-fpm\.service|apache2\.service|httpd\.service' | sort -u); do
    st=$(systemctl show -p ActiveEnterTimestampMonotonic --value "$svc" 2>/dev/null)
    human=$(systemctl show -p ActiveEnterTimestamp --value "$svc" 2>/dev/null)
    echo "   $svc  started: $human"
    SVC_RESTART+=("$svc")
  done
  echo "   -> if a service started BEFORE you upgraded php, its workers still run OLD code"
else
  echo "   (systemctl unavailable; restart FPM/web server manually after upgrading)"
fi
hr

#--- 5. compute upgrade targets (auto-pick) -----------------------------------
echo "$(c_bld '5) Available fix from your repos')"
if [ "$PKG" = apt ]; then
  mapfile -t inst < <(dpkg-query -W -f='${Package}\n' 'php*' 'libapache2-mod-php*' 2>/dev/null | sort -u)
  PKG_UPGRADE=("${inst[@]}")
  echo "   installed php packages: ${#inst[@]} (will be upgraded together)"
  echo "   checking candidates (run 'sudo apt update' first for fresh data):"
  for p in libapache2-mod-php8.3 php8.3-fpm php8.2-fpm php8.4-fpm php8.3-cli; do
    dpkg -s "$p" >/dev/null 2>&1 || continue
    cand=$(apt-cache policy "$p" 2>/dev/null | awk '/Candidate:/{print $2}')
    echo "     $p candidate=$cand"
  done
elif [ "$PKG" = dnf ] || [ "$PKG" = yum ]; then
  echo "   $PKG check-update 'php*' will show available fixed builds"
  PKG_UPGRADE=(php)
fi
hr

#============================= RECOMMENDATION =================================
echo "$(c_bld '=== RECOMMENDED ACTIONS (auto-selected for this host) ===')"
n=0
if [ "$RISK" = 2 ]; then
  # 1) patch
  n=$((n+1))
  if [ "$PKG" = apt ]; then
    echo " [$n] PATCH — upgrade PHP to the fixed build:"
    echo "       sudo apt update"
    if [ "${#PKG_UPGRADE[@]}" -gt 0 ]; then
      echo "       sudo apt install --only-upgrade -y ${PKG_UPGRADE[*]}"
    else
      echo "       sudo apt upgrade"
    fi
  elif [ "$PKG" = dnf ] || [ "$PKG" = yum ]; then
    echo " [$n] PATCH — upgrade PHP to the fixed build:"
    echo "       sudo $PKG -y update 'php*'"
  else
    echo " [$n] PATCH — update PHP to >= 8.2.35/8.3.36/8.4.27/8.5.12 via your package source or source backport"
  fi
  # 2) restart workers
  n=$((n+1))
  if [ "${#SVC_RESTART[@]}" -gt 0 ]; then
    echo " [$n] RESTART workers (mandatory after patch):"
    echo "       sudo systemctl restart ${SVC_RESTART[*]}"
  else
    echo " [$n] RESTART every PHP-FPM pool and the web server after patching"
  fi
  # 3) interim mitigation: session.upload_progress
  if [ "${#INI_UPROGRESS[@]}" -gt 0 ]; then
    n=$((n+1))
    echo " [$n] MITIGATE (interim, until patched + for defence in depth) — disable session upload progress:"
    for f in "${INI_UPROGRESS[@]}"; do echo "       set 'session.upload_progress.enabled = 0' in $f"; done
    echo "       then restart workers (step above)."
  fi
  # 4) extra hardening notes
  for act in "${ACTIONS[@]}"; do n=$((n+1)); echo " [$n] $act"; done
  # 5) verify
  n=$((n+1)); echo " [$n] VERIFY — re-run this script; every branch should read '>= fixed' and services restarted AFTER the upgrade."
else
  echo " $(c_grn 'No vulnerable PHP detected on web-facing SAPIs.') Keep workers restarted after any future upgrade."
fi
hr

#--- optional apply -----------------------------------------------------------
if [ "$APPLY" = 1 ] && [ "$RISK" = 2 ]; then
  if [ "$(id -u)" != 0 ]; then echo "$(c_red '--apply needs root (sudo).')"; exit 3; fi
  printf '%s' "$(c_bld 'Apply the SAFE fixes now (upgrade + restart + upload_progress=0)? [y/N] ')"
  read -r ans </dev/tty 2>/dev/null || ans=n
  if [ "$ans" = y ] || [ "$ans" = Y ]; then
    if [ "$PKG" = apt ]; then apt-get update && apt-get install --only-upgrade -y "${PKG_UPGRADE[@]}"
    elif [ "$PKG" = dnf ] || [ "$PKG" = yum ]; then "$PKG" -y update 'php*'; fi
    for f in "${INI_UPROGRESS[@]}"; do
      [ -f "$f" ] || continue; cp -a "$f" "$f.p2s-bak.$(date +%s)"
      if grep -qsE '^[[:space:]]*;?[[:space:]]*session\.upload_progress\.enabled' "$f"; then
        sed -ri 's|^[[:space:]]*;?[[:space:]]*(session\.upload_progress\.enabled)[[:space:]]*=.*|\1 = 0|' "$f"
      else printf '\nsession.upload_progress.enabled = 0\n' >> "$f"; fi
      echo "   set upload_progress=0 in $f (backup: $f.p2s-bak.*)"
    done
    [ "${#SVC_RESTART[@]}" -gt 0 ] && systemctl restart "${SVC_RESTART[@]}" && echo "   restarted: ${SVC_RESTART[*]}"
    echo "$(c_bld 'Applied. Re-running verification...')"; hr
    exec bash "$0"   # re-verify
  else echo "   apply cancelled; nothing changed."; fi
fi

[ "$JSON" = 1 ] && printf '{"host":"%s","risk":%s,"vulnerable":%s,"upgrade_pkgs":%s}\n' \
  "$(hostname)" "$RISK" "$([ "$RISK" = 2 ] && echo true || echo false)" "${#PKG_UPGRADE[@]}"

[ "$RISK" = 2 ] && echo "$(c_red 'RESULT: action needed (see RECOMMENDED ACTIONS).')" || echo "$(c_grn 'RESULT: no web-facing PHP vulnerable to PHP2Shell.')"
exit "$RISK"

Save it as php2shell_audit.sh and run it directly on the box that serves your traffic:

bash php2shell_audit.sh

Checking a remote server over SSH instead of copying files around? Pipe it straight in:

ssh user@your-server 'bash -s' < php2shell_audit.sh

Want it to also apply the safe fixes — package upgrade, worker restart, and disabling the risky session feature — automatically? Add --apply. It will ask for confirmation first and back up any config file it touches before changing it:

sudo bash php2shell_audit.sh --apply

As with any script that touches production, test it on a staging box first if you have one.

How to Actually Fix It

  1. Update PHP to the fixed build for your branch (8.2.35 / 8.3.36 / 8.4.27 / 8.5.12).
    Debian/Ubuntu: sudo apt update && sudo apt upgrade
    RHEL-based: sudo dnf update 'php*'

  2. Restart every worker. This is the step people skip, and it's the one that actually matters: installing a new package changes nothing until the processes already handling live traffic are restarted onto it. sudo systemctl restart php8.3-fpm apache2 (swap in your real service names).

  3. Stuck on an EOL branch (7.4 / 8.0 / 8.1)? Check whether your distro or hosting provider already shipped a backported fix under the same version number — many did within a day of disclosure. If not, PHP published minimal emergency source patches for each branch on GitHub. Long-term, migrate to a supported branch.

  4. Can't patch immediately? These reduce — but don't eliminate — risk, and still require a full worker restart to take effect:

    • session.upload_progress.enabled = 0

    • memory_limit = -1, set administratively (e.g. php_admin_value in your FPM pool)

    • post_max_size = 8M (or 32M if your app genuinely needs uploads)

    • PHP 8.5 only: fatal_error_backtraces = 0

    Turning off error display (display_errors, @, custom error suppression) does not help — it hides the symptom, not the hole.

  5. Re-run the script. Every branch should report a version at or above the fixed release, and every service should show a restart time after your upgrade — not before it.

The Takeaway

PHP2Shell is a reminder that "my code is secure" was never the whole picture — the language your code runs on is part of your attack surface too, and it needs the same patching discipline as any other dependency. The good news: unlike most critical RCEs, the actual fix here is almost insultingly simple — update, restart, verify. The bad news: "almost insultingly simple" only protects the people who do it this week, not the ones who bookmark this article for later.

Run the script. Patch it. Restart it. Then go check the next server.

Sources & Further Reading

FAQ

Frequently asked questions

What is PHP2Shell?

PHP2Shell is the nickname given to two critical PHP vulnerabilities, CVE-2026-109618 and CVE-2026-109619, disclosed and patched on October 21, 2026. Both allow unauthenticated remote code execution against PHP installations that haven't been updated.

Which PHP versions are safe?

PHP 8.2.35, 8.3.36, 8.4.27, and 8.5.12 (and anything later) contain the full fix. Any version below those numbers on the same branch is vulnerable.

I'm on PHP 7.4, 8.0, or 8.1 — what do I do?

These branches are past their official end of life and don't receive a full release. PHP published minimal emergency source patches for each on GitHub; check with your Linux distribution or hosting provider first, since many already ship a backported fix under the same version number. Moving to a supported branch (8.3 or 8.4) is the most durable long-term fix.

Do I really need to restart my server after updating?

Yes — and it's the step most people forget. PHP-FPM and Apache workers keep running the old code in memory even after the package on disk is updated. Skip the restart, and you're still exposed despite having technically "patched."

Can a firewall (WAF) protect me instead of patching?

Only partially, and not reliably. Cloudflare has already reported attackers changing their payloads specifically to slip past WAF rules. A WAF can buy you time; it can't replace the actual patch.

Does hiding PHP errors or disabling error display stop this?

No. Suppressing error output (<code>display_errors</code>, <code>@</code>, custom error handlers) hides the symptoms but does nothing to close the underlying memory-safety issue.

How do I know if I'm being actively targeted?

Watch your logs for unusual multipart/form-data requests, unexpected worker crashes or restarts, and traffic spikes on endpoints that normally see little activity. Researchers confirmed active scanning and exploitation attempts within hours of disclosure.

Where do I get a script to check if I'm vulnerable?

See the detection script earlier in this article — it's safe to run on production since it only reads your PHP version and configuration, and changes nothing unless you explicitly pass <code>--apply</code>.

Vishal
· Developer

Vishal writes Trickyworld's tutorials and interview-preparation guides on programming, databases, cyber security and AI. Vishal has been shipping production software long enough to have firm opinions about Python, PHP, Node.js, and JavaScript, and to have been wrong about each of them at least once. The work has run the whole stack: Drupal and Laravel applications, React front ends, MySQL and MongoDB underneath, Redis and Memcached for the things that had to be fast, Apache Solr for the things that had to be findable. That last part turned out to matter more than anyone expected. Tune a Solr index for a season, then watch a language model confidently retrieve the wrong document, and you recognise an old problem wearing new clothes. Most of what is hard about AI systems is not the model. It is retrieval, caching, latency budgets, and deciding what the system does when the answer comes back wrong — which is work he has been doing since well before anyone called it AI engineering. He plays a lot of games, which is less a hobby disclosure than an admission of where a good share of his engineering instinct came from. Real-time systems with a frame budget. State that has to stay coherent while a player does something no designer anticipated. The permanent question of what happens when the frame cannot finish in time. Those habits carry over more directly than they have any right to. He has written several books on programming, all built on the same conviction: most technical writing is unclear because the author never decided what to leave out. More from Vishal → · Editorial policy

More on Cyber Security.

Latest articles

$ subscribe --weekly

Get the next tutorial in your inbox.

Programming, security and AI — the practical bits, no spam. Unsubscribe any time.