---
title: "CVE-2024-4577: How Windows Reopened PHP-CGI Command Injection"
description: "A reader-first variant analysis of CVE-2024-4577 and CVE-2024-8926: how Windows character conversion turned safe-looking query input into PHP-CGI options and why the first fix needed a follow-up."
date: 2026-02-19
analysisType: "Variant Analysis"
cves: ["CVE-2024-4577","CVE-2024-8926"]
author: Sevban Dönmez (@jankesec)
canonical: https://jankesec.com/research/php-cgi-best-fit-cve-2024-4577/
---

## CVE-2024-4577 in 60 seconds

Some Windows web-server configurations launch `php-cgi.exe` and turn a query string without an
equals sign into command-line arguments. PHP already tried to block dangerous arguments beginning
with `-`. CVE-2024-4577 bypassed that check because Windows could convert a different leading
character into the ASCII hyphen *after* PHP had approved the input.

The attack path was:

```text
HTTP request to a Windows PHP-CGI route
  -> query string looks like it does not begin with "-"
  -> PHP's 2012 guard allows option parsing
  -> Windows best-fit conversion changes the leading character to "-"
  -> php-cgi.exe consumes attacker-selected command-line options
  -> arbitrary PHP code can execute under the web-service account
```

This was not a vulnerability in every PHP website. The high-impact route required PHP-CGI on
Windows, a web-server mapping that forwarded the query into process arguments, a relevant code-page
conversion, and a vulnerable PHP release. CISA's Known Exploited Vulnerabilities catalog records
active exploitation of CVE-2024-4577.

## What are CGI, option parsing, and Windows best-fit conversion?

CGI is a process interface between a web server and an application. In the affected PHP mode, part
of an HTTP query could become the argument vector for `php-cgi.exe`. PHP options begin with a hyphen,
so controlling those arguments can change interpreter configuration rather than merely supply
application data.

Windows converts characters between Unicode and legacy code pages. A “best-fit” mapping may replace
a character that has no exact representation with a visually or functionally similar one. The
security problem arises when validation sees the pre-conversion character but the option parser sees
the post-conversion hyphen.

## What happened?

PHP's 2012 defense decoded query strings without `=` and disabled option parsing when the first
meaningful character was `-`. On affected Windows code pages, a different byte could pass that test
and later become `-` while Windows constructed the process command line. PHP therefore authorized one
representation and executed another.

The June 2024 releases—8.1.29, 8.2.20, and 8.3.8—added conversion-aware protection for
CVE-2024-4577. A non-standard code-page configuration could still bypass that model, tracked as
CVE-2024-8926. The September releases—8.1.30, 8.2.24, and 8.3.12—used the stronger invariant: on
Windows, CGI and FastCGI stop parsing command-line options from this path instead of trying to
predict every operating-system and web-server conversion.

## Who was actually affected?

The vulnerable unit was a deployment tuple, not “PHP” in isolation: Windows, PHP-CGI, the web
server's CGI mapping, character-conversion behavior, and PHP version all mattered. Ordinary PHP on
Linux, non-CGI SAPIs, or Windows deployments that did not expose the relevant argument path did not
automatically inherit this attack flow.

For remediation, the September release line is the durable boundary: PHP 8.1.30, 8.2.24, 8.3.12 or
later. Asset review should also check the actual web-server handler and whether `php-cgi.exe` is
directly reachable; package inventory alone cannot reconstruct the execution path.

## What I verified

- The first patch uses `WideCharToMultiByte(CP_ACP, ...)` to check whether the leading character
  fails conversion or becomes `-`; either result sets `skip_getopt`.
- That patch adds `sapi/cgi/tests/ghsa-3qgc-jrrr-25jv.phpt`; I inspected the regression test but did
  not claim to have run it on every affected Windows locale.
- The later patch states that emulating operating-system and Apache conversions is not a bounded
  strategy. On Windows it changes the decision to `skip_getopt = cgi || fastcgi`.
- I matched those two diffs to the CVE-2024-4577 and CVE-2024-8926 advisories and release entries.
- This is a static reproduction of the patch logic. I did not run a Windows command-execution
  payload or claim the original discovery.

## Deep dive: the 2012 defense assumed stable representation

In CGI mode, a query string without an equals sign can be passed as command-line arguments. PHP's
historical defense decoded the query string, skipped leading whitespace, and disabled option parsing
when the first meaningful character was `-`. That is a sensible local check if the decoded string is
the exact argument sequence the option parser will later consume.

On Windows, that identity was not guaranteed. Code pages can use best-fit mappings when converting
characters that have no exact representation. In affected language environments, a soft-hyphen-like
input could become the ASCII option marker during a later conversion. The security check saw a
non-option; the process argument parser saw an option.

<figure class="diagram">
<svg viewBox="0 0 760 302" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="A web query is URL decoded and checked in one character representation, where it does not start with a hyphen. Windows best-fit conversion later changes the leading character into an option marker consumed by PHP-CGI.">
<defs><marker id="php-a" viewBox="0 0 8 8" refX="7" refY="4" markerWidth="7" markerHeight="7" orient="auto"><path d="M0,0 L8,4 L0,8 z" class="dg-arrow"/></marker></defs>
<text x="0" y="24" class="dg-crit">TWO REPRESENTATIONS, ONE DECISION</text>
<rect x="0" y="56" width="154" height="72" rx="8" class="dg-box"/><text x="16" y="84" class="dg-label">HTTP query</text><text x="16" y="106" class="dg-muted">encoded bytes</text>
<line x1="156" y1="92" x2="198" y2="92" class="dg-line" marker-end="url(#php-a)"/>
<rect x="202" y="56" width="154" height="72" rx="8" class="dg-box-accent"/><text x="218" y="84" class="dg-label">PHP check</text><text x="218" y="106" class="dg-accent">not a hyphen</text>
<line x1="358" y1="92" x2="400" y2="92" class="dg-line dg-crit-line" marker-end="url(#php-a)"/>
<rect x="404" y="56" width="154" height="72" rx="8" class="dg-box-crit"/><text x="420" y="84" class="dg-label">Best-fit map</text><text x="420" y="106" class="dg-crit">meaning changes</text>
<line x1="560" y1="92" x2="602" y2="92" class="dg-line dg-crit-line" marker-end="url(#php-a)"/>
<rect x="606" y="56" width="154" height="72" rx="8" class="dg-box-crit"/><text x="622" y="84" class="dg-label">CGI options</text><text x="622" y="106" class="dg-crit">flag accepted</text>
<rect x="202" y="198" width="356" height="68" rx="8" class="dg-box"/><text x="218" y="226" class="dg-label">Broken invariant</text><text x="218" y="248" class="dg-muted">validated characters != consumed characters</text>
<line x1="279" y1="130" x2="279" y2="196" class="dg-line dg-dash"/><line x1="481" y1="130" x2="481" y2="196" class="dg-line dg-dash"/>
</svg>
<figcaption>The vulnerability exists because validation is performed before a meaning-changing conversion. The later option parser consumes a representation the security check never approved.</figcaption>
</figure>

This pattern appears beyond code pages. Unicode normalization, path canonicalization, URL decoding,
shell quoting, proxy rewrites, filesystem case folding, and archive extraction can all change how a
token is interpreted. A filter is valid only if no downstream layer can transform an allowed token
into a blocked one.

## Exploitability depended on deployment shape

The issue affected PHP built for Windows when PHP-CGI was exposed through a web server configuration
that forwarded query strings into the CGI process. It was not a universal property of every PHP
application, every operating system, or every PHP SAPI.

The high-impact path required three elements: an externally reachable CGI route, a Windows locale or
code-page behavior capable of the relevant mapping, and argument parsing that allowed the converted
input to influence PHP options. This is why scanning only application dependencies is incomplete.
The vulnerable unit is a deployment tuple: web-server mapping, SAPI, operating system conversion,
locale, and PHP version.

CISA later added CVE-2024-4577 to the Known Exploited Vulnerabilities catalog. That confirms active
abuse of the vulnerability class, but it does not prove every exposed PHP-CGI host was compromised.
Impact decisions still require host logs, process telemetry, web-server configuration, and package
history.

## The first fix and the variant belong in one model

The June 2024 PHP releases—8.1.29, 8.2.20, and 8.3.8—introduced the initial fix for CVE-2024-4577.
The September releases then recorded CVE-2024-8926 as a bypass of that protection. The later fix
landed in 8.1.30, 8.2.24, and 8.3.12.

The existence of a bypass does not mean the first patch had no value. It means the patch model did
not cover every configuration and conversion route. When a vulnerability crosses parsers, a narrow
test built from one locale and one web-server mapping can prove the reported input is blocked while
leaving the underlying representation mismatch alive elsewhere.

<figure class="diagram">
<svg viewBox="0 0 760 308" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="A variant analysis matrix crosses PHP version, Windows code page, web server CGI mapping, and final argument vector. The original regression input and alternate configuration must both remain blocked while ordinary requests continue to work.">
<text x="0" y="24" class="dg-accent">VARIANT TEST MATRIX</text>
<rect x="0" y="50" width="166" height="62" rx="8" class="dg-box"/><text x="16" y="76" class="dg-label">PHP branch</text><text x="16" y="98" class="dg-muted">before / after</text>
<rect x="198" y="50" width="166" height="62" rx="8" class="dg-box"/><text x="214" y="76" class="dg-label">Code page</text><text x="214" y="98" class="dg-muted">exact / best-fit</text>
<rect x="396" y="50" width="166" height="62" rx="8" class="dg-box"/><text x="412" y="76" class="dg-label">CGI mapping</text><text x="412" y="98" class="dg-muted">server config</text>
<rect x="594" y="50" width="166" height="62" rx="8" class="dg-box-accent"/><text x="610" y="76" class="dg-label">Final argv</text><text x="610" y="98" class="dg-accent">record meaning</text>
<line x1="0" y1="146" x2="760" y2="146" class="dg-line dg-dash"/>
<rect x="0" y="188" width="226" height="74" rx="8" class="dg-box-accent"/><text x="16" y="216" class="dg-label">Ordinary request</text><text x="16" y="238" class="dg-accent">application still runs</text>
<rect x="267" y="188" width="226" height="74" rx="8" class="dg-box-crit"/><text x="283" y="216" class="dg-label">Original regression</text><text x="283" y="238" class="dg-crit">option never appears</text>
<rect x="534" y="188" width="226" height="74" rx="8" class="dg-box-crit"/><text x="550" y="216" class="dg-label">Alternate route</text><text x="550" y="238" class="dg-crit">bypass also denied</text>
<text x="0" y="294" class="dg-muted">Assert on the argument vector, not only the HTTP response.</text>
</svg>
<figcaption>A parser-boundary fix needs a matrix. Positive controls preserve legitimate CGI behavior; negative controls prove that neither the original input nor alternate conversion routes become options.</figcaption>
</figure>

## Why blocking one byte at the web server is incomplete

Emergency web-server rewrite rules can reject known encodings before they reach PHP-CGI. They are
useful for immediate containment but fragile as a permanent control. Multiple decoding passes,
alternate URL encodings, proxy normalization, and different code pages can create equivalent final
characters from different input bytes.

The durable security property belongs near the command-line boundary: web-controlled query data
must not be interpreted as PHP process options. If the application does not require CGI mode,
migrating to a deployment model that does not translate query strings into command arguments also
removes the dangerous feature rather than extending a denylist.

## Evidence matrix

| Question | What I checked | Confidence | Limit |
| --- | --- | ---: | --- |
| What failed in CVE-2024-4577? | I matched PHP's advisory to the code-page conversion check added in the first patch. | High | Exact mapping depends on the active Windows code page. |
| Which builds were initially fixed? | I checked the release entries for 8.1.29, 8.2.20, and 8.3.8. | High | That fix did not cover every later-documented configuration. |
| Why include CVE-2024-8926? | I inspected the later tag diff and changelog entry that identify it as a bypass. | High | The private discovery path and every affected deployment permutation are not public. |
| Is exploitation only theoretical? | I checked CISA's Known Exploited Vulnerabilities record for CVE-2024-4577. | High | Catalog inclusion does not identify every victim or campaign. |
| Is PHP-FPM affected by the same route? | I kept the claim limited to the published Windows CGI/FastCGI argument path. | High | A host can expose multiple SAPIs; configuration inspection is still required. |
| What is the safest maintained baseline? | Later branch releases include both the original correction and its documented bypass fix. | High | Vendor-packaged backports require the vendor advisory, not upstream version comparison alone. |

## Safe validation without executing a command

Inventory the deployment before sending any request. Confirm the operating system, active PHP SAPI,
web-server handler mapping, PHP package revision, Windows system and process code pages, and whether
the CGI endpoint is externally reachable. A PHP version found in application metadata does not prove
that the vulnerable executable receives web traffic.

In a lab copy, instrument the CGI launcher or wrap process creation so the final argument vector is
recorded and execution stops before PHP processes any option. Feed inert synthetic query values that
exercise exact and best-fit conversion. The pass condition is that untrusted query content never
creates a PHP option in the final argument vector.

This observes the real security boundary without enabling code execution. It also avoids a weak test
that infers safety from an HTTP status: a request may fail after a dangerous option was accepted, or
succeed through application behavior unrelated to argument parsing.

Required controls include:

- an ordinary query containing an equals sign, proving normal CGI input still works;
- the historical leading-option shape, which must disable option parsing;
- a best-fit candidate under both a mapping and a non-mapping code page;
- the same matrix on the June 2024 fix and the later September branch release;
- a non-CGI SAPI, confirming that the test harness does not generalize the route incorrectly;
- a vendor-backported package whose security record explicitly covers both CVEs.

Do not run public exploit strings against production systems. Besides the risk of command execution,
intermediate proxies and endpoint controls can alter the input and make the result ambiguous. A
controlled argument-vector assertion is safer and produces better evidence.

## My conclusion

Use a currently supported PHP branch that includes the later CVE-2024-8926 correction—at minimum
8.1.30, 8.2.24, or 8.3.12 for those historical branches—or a vendor package explicitly documenting
both fixes. Remove unnecessary PHP-CGI mappings, restrict external reachability, and treat rewrite
rules as temporary containment.

My conclusion is that the second fix is the more durable one because it removes the risky capability
instead of extending a growing list of character conversions. If validation and final use happen in
different representations, one parser's approval is insufficient: a later conversion can still turn
the same input into an option marker.

## Public sources

- [PHP advisory: CVE-2024-4577](https://github.com/php/php-src/security/advisories/GHSA-3qgc-jrrr-25jv)
- [PHP 8 changelog and CVE-2024-8926 follow-up](https://www.php.net/ChangeLog-8.php)
- [PHP advisory: CVE-2024-8926](https://github.com/php/php-src/security/advisories/GHSA-p99j-rfp4-xqvq)
- [CISA Known Exploited Vulnerabilities catalog](https://www.cisa.gov/known-exploited-vulnerabilities-catalog?search_api_fulltext=CVE-2024-4577)
- [CVE.org record for CVE-2024-4577](https://www.cve.org/CVERecord?id=CVE-2024-4577)