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·238 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•GHSA-534H-C3CW-V3H9
5.5

GHSA-534h-c3cw-v3h9: Local Information Disclosure via Abstract-Namespace Socket in Nuxt Dev Server

A local security vulnerability in the Nuxt development server (nuxt dev) allows local unprivileged users to access sensitive configuration files and source code. On Linux environments running Node.js 20+, Nuxt bound its internal vite-node IPC server to an abstract-namespace Unix socket without any peer authentication, enabling co-resident local users to connect and request module code directly.

Amit Schendel
Amit Schendel
2 views•5 min read
•about 2 hours ago•GHSA-8RFP-98V4-MMR6
0.0

GHSA-8RFP-98V4-MMR6: Protocol-Filtering Bypass via Unicode Obfuscation in Mozilla Bleach

Mozilla Bleach is an open-source HTML sanitizing library for Python. Versions up to and including 6.3.0 contain an incomplete filtering implementation in the URI validation logic ('sanitize_uri_value'). This logic fails to detect disallowed protocols, such as 'javascript:', if they contain Unicode invisible characters, whitespace characters, or characters with a code point greater than U+00A0. While standard-compliant web browsers do not directly execute invalid URI schemes containing these non-standard characters, downstream systems that normalize Unicode text by stripping invisible or non-ASCII characters can unintentionally reactivate the 'javascript:' prefix, causing Cross-Site Scripting (XSS). Additionally, this behavior violates Bleach's core sanitization contract by outputting URIs that bypass protocol allowlists configured by the caller.

Amit Schendel
Amit Schendel
2 views•7 min read
•about 2 hours ago•GHSA-G75F-G53V-794X
4.3

GHSA-G75F-G53V-794X: CPU Exhaustion via Unbounded Email Regular Expression Scanning in Bleach

An uncontrolled resource consumption vulnerability exists in the Python package Bleach when parsing text to linkify email addresses. When `parse_email=True` is enabled, the regular expression engine is forced into a quadratic-time complexity scan on specially crafted payloads lacking an '@' symbol. This causes immediate CPU exhaustion and blocks application server worker processes.

Amit Schendel
Amit Schendel
2 views•6 min read
•about 3 hours ago•GHSA-GR75-JV2W-4656
4.7

GHSA-GR75-JV2W-4656: Path Traversal and Sandbox Escape in LangChain File-Search Middleware and Loaders

A path traversal and sandbox escape vulnerability in LangChain and LangChain-Anthropic Python packages allows unauthenticated local attackers to access files outside the restricted directory via crafted input, symbolic links, or prefix bypasses.

Alon Barad
Alon Barad
2 views•8 min read
•about 3 hours ago•GHSA-M557-WRGG-6RP4
5.8

GHSA-m557-wrgg-6rp4: Server-Side Request Forgery via Authority Information Access (AIA) Chasing in phpseclib

The PHP Secure Communications Library (phpseclib) contains a Server-Side Request Forgery (SSRF) vulnerability due to an insecure default implementation of Authority Information Access (AIA) certificate chasing. This flaw allows remote, unauthenticated attackers to coerce applications validating user-supplied X.509 certificates into generating arbitrary outbound HTTP requests to internal networks or local interfaces.

Amit Schendel
Amit Schendel
3 views•6 min read
•about 4 hours ago•CVE-2026-45491
6.2

CVE-2026-45491: Directory Traversal via Improper Link Resolution in .NET System.Formats.Tar

A directory traversal vulnerability exists in the Microsoft .NET System.Formats.Tar library during archive extraction. When extracting a TAR archive using the TarFile.ExtractToDirectory API, the extraction engine improperly resolves symbolic links prior to file creation, allowing local unauthorized attackers to write or overwrite arbitrary files outside the target directory. This can lead to local tampering, privilege escalation, or arbitrary code execution.

Amit Schendel
Amit Schendel
7 views•6 min read