php2shell.io
Independent technical resourceCVE-2026-109618 / CVE-2026-109619Reviewed 2026-10-21
RESPONSE / 02 · TEMPORARY & PARTIAL

Compensating controls.

If patching is impossible, reduce exposure while you prepare the runtime upgrade. Each example below addresses a limited attack surface; none is a substitute for the patch.

Read the scope first

Patch remains the recommended action

The two flaws have multiple paths. A multipart block or upload-progress toggle does not cover all of them. Go to the patch guide →

The frontend examples deliberately reject all multipart/form-data requests in scope, across HTTP methods. They can break legitimate forms and uploads. Use them only where that traffic can be stopped; route necessary uploads to a patched or isolated runtime.

These are configuration templates, not vendor-certified PHP2Shell signatures. Adapt them in staging, test syntax and application behavior, and verify enforcement. Keep direct origin access behind the same control. The snippets have not been deployed or exploit-tested here.

PHP configuration

CONTROL / 01
PARTIAL COVERAGEConfirmed session and fatal-backtrace paths

Why it helps: disabling upload progress removes confirmed session-related consumers of stale state; disabling PHP 8.5 fatal backtraces removes one confirmed producer. These settings do not repair memory lifetime handling or cover all variants. They are partial controls for CVE-2026-109619, not a fix for CVE-2026-109618.

PHP configuration / ini / partial control
; In the web runtime's php.ini, before request parsing.
; Partial control only. Restart every PHP worker.
session.upload_progress.enabled = Off

; PHP 8.5 only: confirmed fatal-backtrace path.
fatal_error_backtraces = Off
Alternative / PHP-FPM pool configuration
; In each relevant PHP-FPM pool configuration.
php_admin_flag[session.upload_progress.enabled] = off

; PHP 8.5 pools only.
php_admin_flag[fatal_error_backtraces] = off
  • Disabling upload progress blocks confirmed session consumers of CVE-2026-109619, not every stale-state variant.
  • Use php.ini, an FPM pool or server-level settings. ini_set() and FPM .user.ini are too late for these startup paths.
  • Restart every worker; merely editing files leaves existing workers and stale state in service.
  • Remove invalid/deprecated per-request INI settings that cause initialization diagnostics and fix attacker-triggered fatal errors/timeouts.

Verify: Use a private diagnostic through the actual web runtime to confirm PHP_VERSION, PHP_SAPI and the effective INI values; remove it afterward. Do not expose phpinfo().

Sources: Configuration reference

Sources: Scope of the state-lifecycle mitigation

PHP OOM control

CONTROL / 02
PARTIAL COVERAGETemporary administrative setting with an availability boundary

Why it helps: an enforced unlimited PHP memory limit removes a controllable per-request failure trigger. Real allocation failures can still reach vulnerable cleanup. The underlying bug remains until PHP is patched.

Availability tradeoff — read before applying

Setting memory_limit = -1 removes PHP’s memory cap. Establish a container/cgroup boundary first. An underlying allocation failure can still reach the vulnerable path; this is not complete remediation.

PHP OOM control / ini / partial control
; FPM pool example: only while an upgrade is impossible.
; Establish a container/cgroup memory boundary first.
php_admin_value[memory_limit] = -1
php_admin_value[post_max_size] = 8M
php_admin_value[upload_max_filesize] = 2M
  • The upstream advisory proposes an enforced unlimited PHP memory_limit to remove the attacker-controlled per-request OOM trigger. Underlying allocation failure can still reach the flaw.
  • This removes PHP's memory guardrail. Use a container/cgroup boundary and an operational policy appropriate to worker termination; do not replace it with RLIMIT_AS or RLIMIT_DATA.
  • The size values are a no-upload example; review application needs. Upstream suggests 32M post_max_size for uploads and upload_max_filesize below it.
  • Bound decoded request bodies and parser work at the frontend, including requests without a usable Content-Length. Body size alone does not bound multipart metadata expansion.
  • Restart workers and check no pool, directory or application restores a finite PHP memory limit.

Sources: Configuration reference

Apache HTTP Server

CONTROL / 03
PARTIAL COVERAGEBlock multipart before PHP
Apache HTTP Server / apache / partial control
# Inside each affected VirtualHost; requires mod_rewrite.
# Stops multipart forms and uploads for this virtual host.
RewriteEngine On
RewriteCond %{HTTP:Content-Type} multipart/form-data [NC]
RewriteRule ^ - [F,L]

# Optional body-size ceiling; example: 8 MiB.
LimitRequestBody 8388608
  • Keep the rule ahead of proxy/front-controller rewrite rules. Use server/VirtualHost configuration rather than depending on application code.
  • A body ceiling reduces risk but does not bound PHP's internal memory expansion. Header filtering leaves non-multipart paths unaddressed.
  • For mod_php, restart all Apache workers after PHP upgrades or runtime INI changes. For proxy_fcgi, restart the FPM pool as well.

Verify: apachectl configtest; after a successful result, reload using your service manager. Service and binary names vary by distribution.

Sources: Configuration reference

nginx

CONTROL / 04
PARTIAL COVERAGEReject multipart at the frontend
nginx / nginx / partial control
# In the http {} context.
map $http_content_type $php2shell_multipart {
    default 0;
    ~*multipart/form-data 1;
}
nginx / add to each existing server {} block
# Add to each existing server {} serving unpatched PHP.
# This intentionally disables multipart forms and uploads.
if ($php2shell_multipart) {
    return 403;
}
client_max_body_size 8m;
  • The map belongs in http {}; the active if/return and body limit belong in the existing server {} before PHP/proxy handling.
  • Add both nginx blocks in their indicated contexts; a map alone does not block requests.
  • Test upload and API paths, including larger bodies and requests without Content-Length. A request-body size cap is not a cap on PHP memory use.
  • Updating nginx does not update PHP-FPM; replace PHP workers separately.

Verify: nginx -t; after a successful result, reload nginx using your service manager. Restart PHP workers when changing their binary or INI.

Sources: Configuration reference

AWS WAF

CONTROL / 05
PARTIAL COVERAGECloudFormation rule for an existing web ACL
AWS WAF / yaml / partial control
# Add this item to AWS::WAFv2::WebACL Properties.Rules.
# Choose a free priority before any terminating Allow rule.
- Name: BlockMultipartToUnpatchedPHP
  Priority: 10
  Action:
    Block: {}
  Statement:
    ByteMatchStatement:
      SearchString: multipart/form-data
      PositionalConstraint: CONTAINS
      FieldToMatch:
        Headers:
          MatchPattern:
            IncludedHeaders:
              - content-type
          MatchScope: VALUE
          OversizeHandling: MATCH
      TextTransformations:
        - Priority: 0
          Type: LOWERCASE
  VisibilityConfig:
    CloudWatchMetricsEnabled: true
    MetricName: BlockMultipartToUnpatchedPHP
    SampledRequestsEnabled: true
  • This is a CloudFormation fragment, not a complete stack or raw WAF API payload. Associate the ACL with every relevant CloudFront/ALB/API entry point.
  • Headers inspection is capped at 8 KiB or 200 headers. MATCH blocks requests when that inspection limit is exceeded, which can reject otherwise legitimate traffic.
  • Review rule priority and any Allow/Skip behavior. A Count action can measure impact but does not block traffic.
  • It blocks the media type, not a full exploit signature, and does not patch any runtime.

Verify: Validate the full CloudFormation template, review the change set, then confirm sampled requests and WAF logs show a benign multipart probe being blocked. Verify direct origin access is restricted.

Sources: Configuration reference

lighttpd

CONTROL / 06
PARTIAL COVERAGEHeader condition with mod_access
lighttpd / lighttpd / partial control
# Add at server scope or inside the affected host condition.
# Ensure mod_access is already loaded in server.modules.
$REQUEST_HEADER["Content-Type"] =~ "(?i)multipart/form-data" {
    url.access-allow = ( )
    url.access-deny = ( "" )
}
  • The empty deny suffix matches every URL when the header condition is true. This stops all multipart forms/uploads in scope.
  • Clearing url.access-allow in the matching condition prevents an inherited allow-suffix list from taking precedence over access-deny; review later matching conditions for overrides.
  • The condition and allow/deny precedence were checked against current lighttpd source; validate compatibility with the installed release.
  • Patch/restart the PHP FastCGI backend separately; changing lighttpd does not change the PHP runtime.

Verify: lighttpd -tt -f /etc/lighttpd/lighttpd.conf; after a successful result, reload using your service manager. Check the real configuration path for your distribution.

Sources: Configuration reference

GCP Cloud Armor

CONTROL / 07
PARTIAL COVERAGECustom deny rule using request headers
GCP Cloud Armor / cel / partial control
has(request.headers['content-type']) &&
request.headers['content-type'].lower().contains('multipart/form-data')
  • Use this as an advanced match expression with action deny(403) on the backend security policy protecting the application.
  • Choose an unused priority before broad allow rules, attach the policy to the actual backend service, and restrict access that bypasses the load balancer.
  • Cloud Armor joins repeated header values with commas, so the substring check also sees that representation.
  • Preview mode reports matches only; turn preview off to enforce. This header rule avoids request-body inspection limits but only reduces multipart exposure.

Verify: Preview against legitimate requests, inspect security-policy logs, then enforce and verify that a benign multipart request is denied. Preview alone is not protection.

Sources: Configuration reference

Cloudflare

CONTROL / 08
PARTIAL COVERAGECustom multipart block
Cloudflare / ruleset / partial control
http.request.headers.truncated or
any(lower(http.request.headers["content-type"][*])[*] contains "multipart/form-data")
  • Use a WAF custom rule with action Block. This intentionally rejects all multipart traffic and requests whose headers were truncated for inspection.
  • Keep the DNS record proxied and restrict direct origin traffic. Review earlier Skip rules and rule ordering.
  • Do not block the PHPSESSID cookie: that breaks normal sessions and is not a reliable exploit indicator.
  • Managed protections and narrower form-field rules are additional controls; do not describe any as a complete fix for both CVEs.

Verify: Validate the expression in the dashboard and check Security Events with a harmless multipart request. Confirm legitimate business flows after enforcement.

Sources: Configuration reference

Check the control without an exploit

Send an ordinary multipart request to your local test application. For the blanket block examples above, expect the configured rejection (usually HTTP 403) and a matching edge or web-server log entry.

shell / benign local multipart probe
curl --include --max-time 10 \
  --form 'probe=php2shell-rule-check' \
  --form 'file=@/dev/null;filename=probe.txt' \
  http://127.0.0.1:8080/upload.php

This checks request filtering only. It does not exercise the vulnerabilities, verify a PHP patch, or validate a rule that only matches a particular progress field. For AWS WAF, Cloud Armor, or Cloudflare, replace the localhost URL with your owned HTTPS test hostname that routes through that edge; localhost does not test cloud enforcement. Also test regular forms, APIs, header variants, and upload paths used by your application.

Return to the complete fix