CVEReports
CVEReports

Automated vulnerability intelligence platform. Comprehensive reports for high-severity CVEs generated by AI.

Product

  • Home
  • Sitemap
  • RSS Feed

Company

  • About
  • Contact
  • Privacy Policy
  • Terms of Service

© 2026 CVEReports. All rights reserved.

Made with love by Amit Schendel & Alon Barad



CVE-2026-24765

The CI/CD Trojan Horse: Inside PHPUnit's Unsafe Deserialization

Amit Schendel
Amit Schendel
Senior Security Researcher

Jan 27, 2026·6 min read·274 visits

Executive Summary (TL;DR)

PHPUnit's PHPT runner blindly unserialized content from `.coverage` files without validating the class structure. By placing a malicious file in the test directory (e.g., via a Pull Request), an attacker can trigger a PHP gadget chain when the test runner executes, leading to RCE. This was fixed in versions 8.5.52, 9.6.34, 10.5.63, 11.5.50, and 12.5.8 by validating file existence before execution and whitelisting allowed classes during deserialization.

A critical insecure deserialization vulnerability in PHPUnit's PHPT test runner allows local attackers to achieve Remote Code Execution (RCE) by crafting malicious coverage files. This flaw is particularly dangerous in CI/CD environments, where it can be leveraged to compromise build pipelines via malicious Pull Requests.

The Hook: Trusting the Filesystem

We tend to view our testing frameworks as the arbiters of truth—the boring, reliable tools that tell us if our code is broken. But in the security world, "boring" usually just means "unexamined." PHPUnit, the de facto standard for PHP testing, recently reminded us that even the tools checking our security can be the source of insecurity. The vulnerability, CVE-2026-24765, isn't a complex buffer overflow or a subtle race condition. It’s the classic blunder of the PHP world: Unsafe Deserialization.

Specifically, this bug hides in the PHPT runner. For those uninitiated, PHPT is a regression testing format used primarily by PHP internal developers and extension maintainers. It’s a powerful way to test PHP itself, spawning separate processes to run code and capturing the output. When you add code coverage to the mix, these child processes need a way to report back to the parent process about which lines of code executed.

The mechanism for this reporting was simple: write a serialized object to a temporary file on disk, and have the parent read it back. It sounds efficient, but it relies on a fatal assumption: that the file on the disk was actually written by the child process and not planted by a malicious actor lurking in the repository.

The Flaw: A Classic Sink

The vulnerability resides in src/Runner/PhptTestCase.php, inside a method called cleanupForCoverage(). When PHPUnit finishes running a PHPT test, it looks for a file—typically sharing the name of the test but ending in .coverage. This file is supposed to contain a SebastianBergmann\CodeCoverage\RawCodeCoverageData object.

Here is the logic flaw in its rawest form. The code grabs the file contents and passes them directly to unserialize(). Before PHP 7, unserialize() was a loaded gun. In modern PHP, it's still a loaded gun, but we have a safety catch (allowed_classes). PHPUnit, however, was running with the safety off.

// The offending logic (simplified)
private function cleanupForCoverage(): array
{
    // ... determines filename ...
    $buffer = @file_get_contents($files['coverage']);
    if ($buffer !== false) {
        // FATAL ERROR: No filter on what classes can be instantiated
        $coverage = @unserialize($buffer);
        // ...
    }
}

Because there were no restrictions on allowed_classes, the PHP runtime would happily instantiate any class defined in the current scope. If the project includes libraries like Monolog, Guzzle, or even parts of PHPUnit itself that contain "magic methods" (like __destruct or __wakeup), an attacker can craft a "gadget chain." This chain allows them to turn a simple file read into full Remote Code Execution (RCE) purely by manipulating object properties.

The Exploit: Poisoning the Pipeline

You might be thinking, "But this is a local file vulnerability. I need write access to the server to exploit it!" In a traditional web hosting environment, you'd be right. But this is a testing framework. Where does it run? CI/CD Pipelines.

The attack vector here is "Poisoned Pipeline Execution" (PPE). An attacker doesn't need to hack the server; they just need to submit a Pull Request. Here is the kill chain:

  1. Preparation: The attacker generates a malicious serialized payload using a tool like PHPGGC. They target a gadget chain present in the vendor directory (e.g., Monolog/RCE1).
  2. The Trap: The attacker creates a new branch on the target repository. They add a harmless-looking .phpt test file. Alongside it, they commit a binary file named testname.coverage containing the payload.
  3. Execution: The attacker opens a Pull Request. The automated CI system kicks in, running phpunit --coverage-php.
  4. Detonation: PHPUnit runs the .phpt test. Even if the test fails or does nothing, the PhptTestCase runner calls cleanupForCoverage(). It sees the pre-existing .coverage file (the trap), deserializes it, and triggers the gadget chain.

The result? The attacker can execute arbitrary shell commands inside your CI runner. They can dump your AWS_SECRET_ACCESS_KEY, DATABASE_URL, or inject backdoors into the build artifacts that are about to be deployed to production.

The Code: Fixing the Leak

The remediation applied by Sebastian Bergmann (the creator of PHPUnit) was two-fold, implementing both a "Fail-Fast" check and "Defense in Depth" hardening.

1. The Fail-Fast Check: The first change was logical. A coverage file should be generated during the test. If it exists before the test starts, something is wrong. The patch adds a check in the constructor to throw an exception if the coverage file is already present.

// src/Runner/PhptTestCase.php
private function ensureCoverageFileDoesNotExist(): void
{
    $files = $this->getCoverageFiles();
    if (file_exists($files['coverage'])) {
        throw new Exception(
            sprintf('File %s exists, PHPT test %s will not be executed',
            $files['coverage'], $this->filename)
        );
    }
}

2. The Hardened Unserialize: The second fix addresses the root cause. The unserialize call now strictly whitelists the only class that should ever be in that file: RawCodeCoverageData.

$coverage = @unserialize(
    $buffer,
    [
        'allowed_classes' => [
            RawCodeCoverageData::class,
        ],
    ]
);

> [!NOTE] > Regression Drama: The initial fix attempted to set allowed_classes => false, assuming the data was simple arrays. This broke functionality because the data is actually an object. The patch had to be quickly revised (in versions like 9.6.34) to explicitly allow RawCodeCoverageData::class.

The Impact: Why You Should Panic (Just a Little)

This vulnerability scores a 7.8 (High) on the CVSS scale, and rightfully so. While it requires "Local" access, in the context of modern DevSecOps, the definition of "Local" has shifted.

If you run open-source projects or accept contributions from the public, your CI pipeline is effectively a "public-facing" application. This vulnerability turns a standard unit test run into a potential compromised server. The impact ranges from:

  • Credential Theft: Stealing environment secrets available to the CI runner.
  • Supply Chain Attacks: Modifying the build output (e.g., injecting malicious JS into a compiled frontend asset) before it gets pushed to production.
  • Lateral Movement: Using the CI runner's permissions to access internal networks or cloud resources.

It serves as a stark reminder that unserialize() on file contents is rarely safe unless you have absolute certainty about who wrote that file.

The Fix: Remediation

The fix is straightforward: Update PHPUnit. The maintainers have backported fixes to all supported release lines. You should ensure your composer.lock resolves to at least one of the following versions:

  • PHPUnit 12: 12.5.8
  • PHPUnit 11: 11.5.50
  • PHPUnit 10: 10.5.63
  • PHPUnit 9: 9.6.34
  • PHPUnit 8: 8.5.52

If you cannot upgrade immediately, you can mitigate this risk by ensuring your CI pipeline cleans the workspace aggressively before running tests. Specifically, ensure no .coverage files exist in your test directories prior to execution. However, given the nature of git (which allows checking in binary files), a code upgrade is the only robust solution.

Official Patches

PHPUnitInitial Patch
PHPUnitRegression Fix

Fix Analysis (2)

Technical Appendix

CVSS Score
7.8/ 10
CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H

Affected Systems

PHPUnit 8.x < 8.5.52PHPUnit 9.x < 9.6.34PHPUnit 10.x < 10.5.63PHPUnit 11.x < 11.5.50PHPUnit 12.x < 12.5.8

Affected Versions Detail

Product
Affected Versions
Fixed Version
phpunit
sebastianbergmann
< 8.5.528.5.52
phpunit
sebastianbergmann
>= 9.0.0, < 9.6.349.6.34
phpunit
sebastianbergmann
>= 10.0.0, < 10.5.6310.5.63
AttributeDetail
CWE IDCWE-502
Attack VectorLocal (File System)
CVSS Score7.8 (High)
CVSS VectorCVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H
ImpactRemote Code Execution (RCE)
ContextCI/CD Pipelines / PHPT Testing

MITRE ATT&CK Mapping

T1195Supply Chain Compromise
Initial Access
T1204User Execution
Execution
T1059.006Command and Scripting Interpreter: Python
Execution
CWE-502
Deserialization of Untrusted Data

The application deserializes untrusted data without sufficiently verifying that the resulting data will be valid.

Known Exploits & Detection

InternalExploitable via placing a serialized object in a .coverage file and running PHPUnit with coverage enabled.

Vulnerability Timeline

Initial Security Fix Committed
2026-01-26
GHSA Advisory Published
2026-01-27
Regression Identified & Fixed
2026-01-27

References & Sources

  • [1]GitHub Advisory GHSA-vvj3-c3rp-c85p
  • [2]CWE-502: Deserialization of Untrusted Data

Attack Flow Diagram

Press enter or space to select a node. You can then use the arrow keys to move the node around. Press delete to remove it and escape to cancel.
Press enter or space to select an edge. You can then press delete to remove it or escape to cancel.

More Reports

•about 1 hour ago•CVE-2026-64849
9.3

CVE-2026-64849: Server-Side Request Forgery (SSRF) in MLflow Webhooks via DNS Rebinding

CVE-2026-64849 is a critical Server-Side Request Forgery (SSRF) vulnerability affecting MLflow tracking servers prior to version 3.15.0. It allows unauthenticated remote attackers to bypass outbound request destination filters using DNS rebinding or HTTP redirects. This exposure risks compromising sensitive cloud infrastructure metadata and internal microservices.

Alon Barad
Alon Barad
2 views•5 min read
•about 2 hours ago•CVE-2026-69146
6.5

CVE-2026-69146: Missing Authorization Bypass in MLflow Basic Authentication Middleware

This technical report details a missing authorization vulnerability (CVE-2026-69146 / GHSA-3p64-6gvh-82v5) affecting the MLflow platform from version 3.13.0 to 3.15.0. When MLflow is configured with the built-in basic-auth plugin, authenticated users can bypass run-level UPDATE authorization checks, enabling unauthorized dataset and model lineage metadata injection.

Alon Barad
Alon Barad
1 views•7 min read
•about 3 hours ago•CVE-2026-69148
7.1

CVE-2026-69148: Broken Object Level Authorization (BOLA) in MLflow Model Registry

MLflow prior to version 3.15.0 fails to perform proper authorization checks when registering model versions, allowing authenticated users with access to a registered model to link and access artifacts from runs and models belonging to other users without authorization.

Amit Schendel
Amit Schendel
4 views•7 min read
•about 4 hours ago•CVE-2026-59893
7.5

CVE-2026-59893: Regular Expression Denial of Service in sqlparse Lexer

A high-severity Regular Expression Denial of Service (ReDoS) vulnerability in the sqlparse Python library prior to version 0.6.0 allows unauthenticated remote attackers to trigger CPU exhaustion and application denial of service via crafted SQL inputs containing unmatched dollar-quoted literals or unclosed multiline comments.

Alon Barad
Alon Barad
4 views•6 min read
•about 5 hours ago•GHSA-FHGH-WQ4Q-R37X
7.8

GHSA-FHGH-WQ4Q-R37X: Remote Code Execution via Sigstore Signature Verification Bypass in uniget CLI

A high-severity logic inversion flaw in the uniget CLI completely bypasses Sigstore cryptographic signature verification on metadata files by default. If an attacker can poison the package metadata cache or repository, they can execute arbitrary OS commands under the privileges of the active user.

Alon Barad
Alon Barad
5 views•5 min read
•about 6 hours ago•CVE-2026-59903
6.5

CVE-2026-59903: Cache Poisoning and Information Disclosure via CorsHandler Vary Header Overwrite

A technical analysis of CVE-2026-59903 in Netty's HTTP CORS handler, where the CorsHandler overwrites existing application Vary headers with Origin, leading to unauthorized caching of sensitive information.

Alon Barad
Alon Barad
5 views•5 min read