Introduction
A single invisible newline character in a PHPUnit configuration file can grant an attacker full remote code execution inside your CI pipeline's test runner. CVE-2026-41570 exposes a flaw in how PHPUnit forwards PHP INI settings to child processes, allowing INI directive injection that turns routine test execution into an opportunity for supply chain compromise.
PHPUnit is the de facto standard testing framework for the PHP ecosystem, used by virtually every PHP project that performs automated testing. Its ubiquity in CI/CD pipelines means that a vulnerability in PHPUnit's process isolation mechanism has a broad blast radius across open source and enterprise environments.
Technical Information
Root Cause: Unsanitized INI Value Forwarding
The vulnerability resides in the settingsToParameters() method of src/Util/PHP/JobRunner.php. When PHPUnit forks child PHP processes for process isolated tests or PHPT test execution, it forwards every PHP INI setting to the child process via -d name=value CLI arguments. In vulnerable versions (12.5.21 and 13.1.5), these values were passed through entirely unescaped.
PHP's CLI INI parser interprets several characters with special meaning:
"(quotation mark) acts as a string delimiter;(semicolon) starts a comment, causing everything after it to be ignored\n(newline) serves as a directive separator
Because none of these metacharacters were neutralized before being passed to the child process, the INI parser in the child would misinterpret crafted values. The most dangerous consequence: a value containing a literal newline character would be parsed as multiple distinct INI directives, enabling full INI directive injection.
This weakness maps to CWE-88 (Argument Injection) and CWE-93 (CRLF Injection).
Attack Flow
-
Identify an injection point. The attacker needs to influence the content of an INI value that PHPUnit reads and forwards to child processes. The two primary sources are
<ini name="..." value="..."/>entries inphpunit.xmlorphpunit.xml.dist, and settings inherited from the host PHP runtime viaini_get_all(). -
Craft a malicious INI value. The attacker embeds a newline character within a single INI value, followed by an arbitrary directive. For example, injecting
auto_prepend_file=/tmp/payload.phpafter a newline causes the child process to prepend and execute an attacker controlled PHP file on startup. -
Deliver the payload. The most realistic delivery mechanism is a malicious pull request that modifies
phpunit.xmlto include the injected newline within an<ini>element. Because a newline character is visually indistinguishable from legitimate whitespace in a standard code diff, reviewers are unlikely to notice it. -
Trigger execution. When a CI system picks up the pull request and runs PHPUnit without proper sandboxing, the child process spawned for isolated test execution receives the injected directives. The
auto_prepend_filedirective causes the attacker's payload to execute before any test code runs. -
Achieve impact. With code execution inside the CI runner, the attacker can exfiltrate secrets and environment variables, tamper with build artifacts, establish persistence, or pivot to other systems accessible from the CI network.
This attack pattern aligns with the broader category of Poisoned Pipeline Execution, where adversaries target CI/CD systems by submitting malicious code that gets executed automatically by build infrastructure. The injectable directives go beyond auto_prepend_file and include extension, disable_functions, open_basedir, and memory_limit, each of which can be weaponized for different objectives.
Patch Information
The fix for CVE-2026-41570 was introduced via Pull Request #6592, authored by contributor kayw-geek and manually cherry picked by PHPUnit maintainer Sebastian Bergmann into the 12.5 and 13.1 branches on April 17, 2026. The fix shipped in PHPUnit 12.5.22 and PHPUnit 13.1.6, both released the same day.
The patch was delivered across two commits that together add a new private method, quoteSettingValue(), and integrate it into the settings forwarding pipeline. Here is the core change as seen in the final commit (7b1b97a):
// In settingsToParameters(), each setting now passes through quoteSettingValue: foreach ($settings as $setting) { $buffer[] = '-d'; - $buffer[] = $setting; + $buffer[] = $this->quoteSettingValue($setting); }
The new quoteSettingValue() method performs targeted, conditional quoting:
private function quoteSettingValue(string $setting): string { $position = strpos($setting, '='); if ($position === false) { return $setting; } $value = substr($setting, $position + 1); if (!str_contains($value, ';') && !str_contains($value, '"')) { return $setting; } $name = substr($setting, 0, $position); return $name . '="' . str_replace('"', '\\"', $value) . '"'; }
The method works as follows:
- Find the
=separator. If there is no=(a flag style directive), the setting is returned untouched. - Extract the value portion (everything after
=) and check whether it contains the dangerous metacharacters;or". - If neither metacharacter is present, return early without quoting. This is a deliberate design decision: values like
output_buffering=Offorerror_reporting=E_ALL & ~E_NOTICEcontain INI boolean keywords and bitwise expressions that must not be wrapped in quotes, or PHP would interpret them as literal strings instead of their special INI meanings. - If metacharacters are present, wrap the value in double quotes and escape any internal double quotes by replacing
"with\".
The PR description provides a clear before/after illustration:
- Before:
-d highlight.string=(?:"foo";bar)results in the child seeing(?:foobecause everything after;is treated as a comment and"breaks parsing. - After:
-d highlight.string="(?:\"foo\";bar)"results in the child seeing(?:"foo";bar)correctly, byte for byte.
As a defense in depth benefit, the quoting also closes the newline based INI directive injection vector. Once the value is enclosed in double quotes, a newline character cannot break out of the quoted string context, meaning it can no longer trick PHP into interpreting subsequent content as separate -d directives.
The PR also included a regression test in JobRunnerTest.php that feeds a real world 634 byte regex (from the Datadog ddtrace extension) through a forked process and asserts the child's ini_get() output matches the input byte for byte, validating end to end correctness.
Additionally, the patch design ensures that any PHP setting value containing a newline or carriage return is explicitly rejected with a PhpProcessException, ensuring anomalous states fail loudly in CI output rather than silently passing through.
Affected Systems and Versions
| PHPUnit Release Series | Vulnerable Versions | Patched Versions | Status |
|---|---|---|---|
| 13.x | 13.1.5 | 13.1.6 | Patch Available |
| 12.x | 12.5.21 | 12.5.22 | Patch Available |
| 11.x and earlier | None | N/A | Not Affected |
Only PHPUnit 12.5.21 and 13.1.5 are affected. PHPUnit 11 and earlier versions do not contain the vulnerable code path and are not impacted. The vulnerability is specifically relevant to environments where PHPUnit executes process isolated tests or PHPT tests, as these are the scenarios where child processes receive forwarded INI settings.
CI/CD environments that automatically run PHPUnit against untrusted pull requests (particularly from forks) represent the highest risk configuration.
Vendor Security History
PHPUnit has faced severe vulnerabilities in the past. Most notably, CVE-2017-9841 was a remote code execution flaw in eval-stdin.php that saw widespread exploitation in the wild and remains a commonly scanned for vulnerability years after its disclosure.
The handling of CVE-2026-41570 reflects meaningful maturation in the project's security posture. Patches were released rapidly, the maintainer actively corrected false positives in the GitHub Advisory Database that incorrectly flagged PHPUnit 11 as vulnerable, and the patch design prioritized making exploitation attempts visible through loud exceptions rather than allowing silent failures.
References
- PHPUnit Security Advisory GHSA-qrr6-mg7r-m243
- GitHub Advisory Database: CVE-2026-41570
- Pull Request #6592: INI metacharacter fix
- Commit 7b1b97a (patch commit)
- Commit 74fa51b (patch commit)
- PHPUnit 12.5.22 Release
- PHPUnit 13.1.6 Release
- NVD: CVE-2017-9841 (historical PHPUnit vulnerability)
- PHPUnit Official Site
- Security researcher discussion on X



