From ff0f8487047a5a6d184980f47f54b4636a8cadaa Mon Sep 17 00:00:00 2001 From: basi Date: Wed, 26 Aug 2026 16:17:06 +0900 Subject: [PATCH 1/7] build(deps-dev): require phpunit/phpunit ^9.6.33 (CVE-2026-24765) - app/composer.json pinned phpunit 3.7.* which cannot resolve to any patched release, triggering Dependabot alert #3 (GHSA-vvj3-c3rp-c85p, unsafe deserialization in PHPT code coverage handling). Raise it and the root constraint to ^9.6.33 so only patched 9.6.x can resolve; a fresh install currently resolves to 9.6.36. - Ignore the squizlabs/php_codesniffer 1.x security advisories via config.policy.advisories.ignore: Composer >= 2.9 refuses to install advisory-affected packages, which has broken every CI composer install since Nov 2025. The 1.x line is required by the CakePHP2 coding standard (cakephp-codesniffer 1.x), is dev-only, and is frozen upstream. Co-Authored-By: Claude Fable 5 --- app/composer.json | 2 +- composer.json | 11 +++++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/app/composer.json b/app/composer.json index d1590e56ca..d6de2f77fb 100644 --- a/app/composer.json +++ b/app/composer.json @@ -23,7 +23,7 @@ "cakephp/cakephp": "~2.9" }, "require-dev": { - "phpunit/phpunit": "3.7.*" + "phpunit/phpunit": "^9.6.33" }, "suggest": { "cakephp/cakephp-codesniffer": "Easily check code formatting against the CakePHP coding standards." diff --git a/composer.json b/composer.json index 6dd6e56677..30bf5c57c3 100644 --- a/composer.json +++ b/composer.json @@ -29,12 +29,19 @@ "ext-intl": "Required to use IntlDateFormatter instead of strftime, if not Symfony polyfill will be used." }, "require-dev": { - "phpunit/phpunit": "^9.5", + "phpunit/phpunit": "^9.6.33", "cakephp/cakephp-codesniffer": "^1.0.0" }, "config": { "vendor-dir": "vendors/", - "process-timeout": 0 + "process-timeout": 0, + "policy": { + "advisories": { + "ignore": { + "squizlabs/php_codesniffer": "Pinned to 1.x by cakephp-codesniffer 1.x (CakePHP2 coding standard); dev-only tool, never shipped to production. The 1.x line is frozen upstream, so its advisories are accepted." + } + } + } }, "bin": [ "lib/Cake/Console/cake" From e2ef91b8d385b9bd213fc479d8f692767b9f6c88 Mon Sep 17 00:00:00 2001 From: basi Date: Wed, 26 Aug 2026 16:36:55 +0900 Subject: [PATCH 2/7] test: skip testVerifyPeer when the bad-cert test host no longer resolves HttpSocketTest::testVerifyPeer connects to https://tv.eurosport.com/ expecting a TLS peer-verification failure, but that hostname no longer resolves, so the SocketException carries a DNS error instead of 'Failed to enable crypto' and the assertion fails on every CI job. Skip on name-resolution failure, in the same style as the existing environmental skip conditions in this test. This was masked until now because CI had been failing at composer install since Nov 2025. Co-Authored-By: Claude Fable 5 --- lib/Cake/Test/Case/Network/Http/HttpSocketTest.php | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/Cake/Test/Case/Network/Http/HttpSocketTest.php b/lib/Cake/Test/Case/Network/Http/HttpSocketTest.php index 07dfb6c87a..e412c77aae 100644 --- a/lib/Cake/Test/Case/Network/Http/HttpSocketTest.php +++ b/lib/Cake/Test/Case/Network/Http/HttpSocketTest.php @@ -1844,6 +1844,7 @@ public function testVerifyPeer() { $this->markTestSkipped('Found valid certificate, was expecting invalid certificate.'); } catch (SocketException $e) { $message = $e->getMessage(); + $this->skipIf(strpos($message, 'php_network_getaddresses') !== false, 'Test host could not be resolved, skipping.'); $this->skipIf(strpos($message, 'Invalid HTTP') !== false, 'Invalid HTTP Response received, skipping.'); $this->assertStringContainsString('Failed to enable crypto', $message); } From 5bff587af8f06ab905446ed2167c2bfc97e4eaa3 Mon Sep 17 00:00:00 2001 From: basi Date: Wed, 26 Aug 2026 17:18:51 +0900 Subject: [PATCH 3/7] build(deps-dev): drop phpunit from the app skeleton composer.json The skeleton pins upstream cakephp/cakephp ~2.9 and php >= 5.3.0, which are incompatible with PHPUnit 9 (upstream CakeTestCase extends the PHPUnit 3 era PHPUnit_Framework_TestCase, and PHPUnit 9.6 requires PHP >= 7.3). The skeleton is not exercised by CI nor by the documented consumption path (the README instructs the VCS-repository method), so the dev dependency is vestigial. Removing it resolves Dependabot alert #3 for this manifest without advertising an impossible install. Co-Authored-By: Claude Fable 5 --- app/composer.json | 3 --- 1 file changed, 3 deletions(-) diff --git a/app/composer.json b/app/composer.json index d6de2f77fb..197bb66b06 100644 --- a/app/composer.json +++ b/app/composer.json @@ -22,9 +22,6 @@ "ext-mcrypt": "*", "cakephp/cakephp": "~2.9" }, - "require-dev": { - "phpunit/phpunit": "^9.6.33" - }, "suggest": { "cakephp/cakephp-codesniffer": "Easily check code formatting against the CakePHP coding standards." }, From bf5e5a6cbb7234de978ce076abe9cfb35cfc206b Mon Sep 17 00:00:00 2001 From: basi Date: Wed, 26 Aug 2026 17:18:51 +0900 Subject: [PATCH 4/7] build(composer): scope the advisory ignore to specific phpcs advisories Replace the package-wide policy.advisories.ignore for squizlabs/php_codesniffer with ignore-id entries for the two advisories that block resolution (PKSA-6vdd-n4sx-knhy and CVE-2026-67434), scoped with on-audit: false so only install/update blocking is lifted while composer audit keeps reporting them. Verified against Composer 2.10.2: resolution succeeds (php_codesniffer 1.5.6) and both advisories remain visible in composer audit and in the post-install warning. The scoping semantics follow the implementation (AdvisoriesPolicyConfig) and the doc example; the prose in Composer's 06-config.md currently describes on-block/on-audit inverted. Co-Authored-By: Claude Fable 5 --- composer.json | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 30bf5c57c3..f1fe7b536e 100644 --- a/composer.json +++ b/composer.json @@ -37,8 +37,15 @@ "process-timeout": 0, "policy": { "advisories": { - "ignore": { - "squizlabs/php_codesniffer": "Pinned to 1.x by cakephp-codesniffer 1.x (CakePHP2 coding standard); dev-only tool, never shipped to production. The 1.x line is frozen upstream, so its advisories are accepted." + "ignore-id": { + "PKSA-6vdd-n4sx-knhy": { + "on-audit": false, + "reason": "phpcs 1.x arbitrary shell execution (GHSA-mhfv-8rc9-w38c). squizlabs/php_codesniffer is pinned to 1.x by cakephp-codesniffer 1.x (CakePHP2 coding standard), a dev-only tool never shipped to production. Unblocks install only; still reported by composer audit. Temporary exception until a fix is backported." + }, + "CVE-2026-67434": { + "on-audit": false, + "reason": "phpcs OS command injection (PKSA-rdkp-vv9z-mjkg / GHSA-hmqg-cxww-wqhq). Same rationale as PKSA-6vdd-n4sx-knhy." + } } } } From 8ebbcbdc58d21191bf502205354c27bfc1769e52 Mon Sep 17 00:00:00 2001 From: basi Date: Wed, 26 Aug 2026 17:18:51 +0900 Subject: [PATCH 5/7] test: verify TLS peer rejection against a local self-signed server testVerifyPeer previously depended on an external bad-certificate host (tv.eurosport.com) whose DNS no longer resolves, leaving the test permanently skipped and TLS peer verification without regression coverage. Replace it with a helper (lib/Cake/Test/test_app/tls_server.php) that generates a throwaway self-signed certificate at runtime and serves one TLS connection on 127.0.0.1, so the verification failure is asserted deterministically with no network dependency. An unexpectedly successful request is now a test failure instead of a skip. Co-Authored-By: Claude Fable 5 --- .../Test/Case/Network/Http/HttpSocketTest.php | 47 +++++++++++--- lib/Cake/Test/test_app/tls_server.php | 65 +++++++++++++++++++ 2 files changed, 103 insertions(+), 9 deletions(-) create mode 100644 lib/Cake/Test/test_app/tls_server.php diff --git a/lib/Cake/Test/Case/Network/Http/HttpSocketTest.php b/lib/Cake/Test/Case/Network/Http/HttpSocketTest.php index e412c77aae..1c26a86db1 100644 --- a/lib/Cake/Test/Case/Network/Http/HttpSocketTest.php +++ b/lib/Cake/Test/Case/Network/Http/HttpSocketTest.php @@ -1832,21 +1832,50 @@ public function testPartialReset() { } /** - * Test that requests fail when peer verification fails. + * Test that requests fail when peer verification fails, using a local TLS server with a self-signed certificate. * * @return void */ public function testVerifyPeer() { $this->skipIf(!extension_loaded('openssl'), 'OpenSSL is not enabled cannot test SSL.'); - $socket = new HttpSocket(); + $this->skipIf(!function_exists('proc_open'), 'proc_open is not available, cannot start the TLS fixture server.'); + + $descriptorSpec = array( + 1 => array('pipe', 'w'), + 2 => array('pipe', 'w'), + ); + $process = proc_open( + PHP_BINARY . ' ' . escapeshellarg(CAKE . 'Test' . DS . 'test_app' . DS . 'tls_server.php'), + $descriptorSpec, + $pipes + ); + if (!is_resource($process)) { + $this->markTestSkipped('Unable to start the TLS fixture server.'); + } + + stream_set_timeout($pipes[1], 10); + $port = trim((string)fgets($pipes[1])); + if (!is_numeric($port)) { + fclose($pipes[1]); + fclose($pipes[2]); + proc_terminate($process); + proc_close($process); + $this->markTestSkipped('TLS fixture server did not start.'); + } + try { - $socket->get('https://tv.eurosport.com/'); - $this->markTestSkipped('Found valid certificate, was expecting invalid certificate.'); - } catch (SocketException $e) { - $message = $e->getMessage(); - $this->skipIf(strpos($message, 'php_network_getaddresses') !== false, 'Test host could not be resolved, skipping.'); - $this->skipIf(strpos($message, 'Invalid HTTP') !== false, 'Invalid HTTP Response received, skipping.'); - $this->assertStringContainsString('Failed to enable crypto', $message); + $socket = new HttpSocket(array('timeout' => 10)); + try { + $socket->get('https://127.0.0.1:' . $port . '/'); + $this->fail('Peer verification must reject the self-signed certificate, but the request succeeded.'); + } catch (SocketException $e) { + $this->assertStringContainsString('Failed to enable crypto', $e->getMessage()); + } + } finally { + fclose($pipes[1]); + fclose($pipes[2]); + proc_terminate($process); + proc_close($process); } } diff --git a/lib/Cake/Test/test_app/tls_server.php b/lib/Cake/Test/test_app/tls_server.php new file mode 100644 index 0000000000..5a19ed9872 --- /dev/null +++ b/lib/Cake/Test/test_app/tls_server.php @@ -0,0 +1,65 @@ + 2048, + 'private_key_type' => OPENSSL_KEYTYPE_RSA, +)); +$csr = openssl_csr_new(array('commonName' => 'localhost'), $pkey); +$cert = openssl_csr_sign($csr, null, $pkey, 1, array('digest_alg' => 'sha256')); + +openssl_x509_export($cert, $certPem); +openssl_pkey_export($pkey, $keyPem); + +$pemFile = tempnam(sys_get_temp_dir(), 'cake_tls_'); +file_put_contents($pemFile, $certPem . $keyPem); + +$context = stream_context_create(array( + 'ssl' => array( + 'local_cert' => $pemFile, + ), +)); +$server = @stream_socket_server( + 'tls://127.0.0.1:0', + $errNo, + $errStr, + STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, + $context +); +if ($server === false) { + unlink($pemFile); + fwrite(STDERR, $errStr . "\n"); + exit(1); +} + +$name = stream_socket_get_name($server, false); +$port = substr($name, strrpos($name, ':') + 1); +fwrite(STDOUT, $port . "\n"); +fflush(STDOUT); + +$conn = @stream_socket_accept($server, 10); +if (is_resource($conn)) { + fclose($conn); +} + +unlink($pemFile); +exit(0); From c9c3b9ac9359139cbde5affa73cd9b03b70328d5 Mon Sep 17 00:00:00 2001 From: basi Date: Wed, 26 Aug 2026 17:49:41 +0900 Subject: [PATCH 6/7] test: harden local TLS peer verification fixture --- .../Test/Case/Network/Http/HttpSocketTest.php | 102 ++++++++--- lib/Cake/Test/test_app/tls_server.php | 158 ++++++++++++++---- 2 files changed, 201 insertions(+), 59 deletions(-) diff --git a/lib/Cake/Test/Case/Network/Http/HttpSocketTest.php b/lib/Cake/Test/Case/Network/Http/HttpSocketTest.php index 1c26a86db1..cdc45d8e37 100644 --- a/lib/Cake/Test/Case/Network/Http/HttpSocketTest.php +++ b/lib/Cake/Test/Case/Network/Http/HttpSocketTest.php @@ -1844,39 +1844,95 @@ public function testVerifyPeer() { 1 => array('pipe', 'w'), 2 => array('pipe', 'w'), ); - $process = proc_open( - PHP_BINARY . ' ' . escapeshellarg(CAKE . 'Test' . DS . 'test_app' . DS . 'tls_server.php'), - $descriptorSpec, - $pipes - ); - if (!is_resource($process)) { - $this->markTestSkipped('Unable to start the TLS fixture server.'); - } - - stream_set_timeout($pipes[1], 10); - $port = trim((string)fgets($pipes[1])); - if (!is_numeric($port)) { - fclose($pipes[1]); - fclose($pipes[2]); - proc_terminate($process); - proc_close($process); - $this->markTestSkipped('TLS fixture server did not start.'); - } + $process = null; + $pipes = array(); + $configFile = null; + $pemFile = null; + $fixtureInteractionsComplete = false; + $processExitCode = null; try { + $configFile = tempnam(sys_get_temp_dir(), 'cake_tls_config_'); + $pemFile = tempnam(sys_get_temp_dir(), 'cake_tls_'); + if ($configFile === false || $pemFile === false) { + $this->fail('Unable to create the TLS fixture temporary files.'); + } + + $process = proc_open( + array( + PHP_BINARY, + CAKE . 'Test' . DS . 'test_app' . DS . 'tls_server.php', + $configFile, + $pemFile, + ), + $descriptorSpec, + $pipes + ); + if (!is_resource($process)) { + $this->fail('Unable to start the TLS fixture server.'); + } + + stream_set_timeout($pipes[1], 10); + stream_set_blocking($pipes[2], false); + $fixture = json_decode(trim((string)fgets($pipes[1])), true); + if (!is_array($fixture) || !isset($fixture['port'])) { + $stderr = trim((string)stream_get_contents($pipes[2])); + $message = 'TLS fixture server did not start.'; + if ($stderr !== '') { + $message .= ' ' . $stderr; + } + $this->fail($message); + } + + $port = (int)$fixture['port']; + clearstatcache(true, $pemFile); + if ($port < 1 || $port > 65535 || !is_file($pemFile) || filesize($pemFile) < 1) { + $this->fail('TLS fixture server returned invalid startup information.'); + } + + $url = 'https://127.0.0.1:' . $port . '/'; + $allowSelfSignedSocket = new HttpSocket(array( + 'timeout' => 10, + 'ssl_allow_self_signed' => true, + )); + $response = $allowSelfSignedSocket->get($url); + $this->assertEquals(200, $response->code, 'The TLS fixture certificate must be valid for 127.0.0.1.'); + $socket = new HttpSocket(array('timeout' => 10)); try { - $socket->get('https://127.0.0.1:' . $port . '/'); + $socket->get($url); $this->fail('Peer verification must reject the self-signed certificate, but the request succeeded.'); } catch (SocketException $e) { $this->assertStringContainsString('Failed to enable crypto', $e->getMessage()); } + $fixtureInteractionsComplete = true; } finally { - fclose($pipes[1]); - fclose($pipes[2]); - proc_terminate($process); - proc_close($process); + foreach ($pipes as $pipe) { + if (is_resource($pipe)) { + fclose($pipe); + } + } + if (is_resource($process)) { + if (!$fixtureInteractionsComplete) { + proc_terminate($process); + } + $processExitCode = proc_close($process); + } + foreach (array($configFile, $pemFile) as $temporaryFile) { + if (is_string($temporaryFile)) { + clearstatcache(true, $temporaryFile); + if (is_file($temporaryFile)) { + @unlink($temporaryFile); + } + } + } } + + $this->assertEquals(0, $processExitCode, 'TLS fixture server exited with an error.'); + clearstatcache(true, $configFile); + $this->assertFalse(is_file($configFile), 'TLS fixture OpenSSL config file was not removed.'); + clearstatcache(true, $pemFile); + $this->assertFalse(is_file($pemFile), 'TLS fixture certificate file was not removed.'); } /** diff --git a/lib/Cake/Test/test_app/tls_server.php b/lib/Cake/Test/test_app/tls_server.php index 5a19ed9872..a0d4088879 100644 --- a/lib/Cake/Test/test_app/tls_server.php +++ b/lib/Cake/Test/test_app/tls_server.php @@ -20,46 +20,132 @@ * @license https://opensource.org/licenses/mit-license.php MIT License */ -$pkey = openssl_pkey_new(array( - 'private_key_bits' => 2048, - 'private_key_type' => OPENSSL_KEYTYPE_RSA, -)); -$csr = openssl_csr_new(array('commonName' => 'localhost'), $pkey); -$cert = openssl_csr_sign($csr, null, $pkey, 1, array('digest_alg' => 'sha256')); +$server = null; +$exitCode = 0; -openssl_x509_export($cert, $certPem); -openssl_pkey_export($pkey, $keyPem); +try { + if (!isset($argv[1], $argv[2])) { + throw new RuntimeException('TLS fixture temporary file paths were not provided.'); + } + $configFile = $argv[1]; + $pemFile = $argv[2]; -$pemFile = tempnam(sys_get_temp_dir(), 'cake_tls_'); -file_put_contents($pemFile, $certPem . $keyPem); + $config = "[ req ]\n" . + "distinguished_name = req_distinguished_name\n" . + "req_extensions = v3_req\n" . + "prompt = no\n" . + "\n" . + "[ req_distinguished_name ]\n" . + "CN = 127.0.0.1\n" . + "\n" . + "[ v3_req ]\n" . + "basicConstraints = CA:FALSE\n" . + "keyUsage = critical, digitalSignature, keyEncipherment\n" . + "extendedKeyUsage = serverAuth\n" . + "subjectAltName = IP:127.0.0.1\n"; + if (file_put_contents($configFile, $config) === false) { + throw new RuntimeException('Unable to write the TLS fixture OpenSSL config file.'); + } -$context = stream_context_create(array( - 'ssl' => array( - 'local_cert' => $pemFile, - ), -)); -$server = @stream_socket_server( - 'tls://127.0.0.1:0', - $errNo, - $errStr, - STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, - $context -); -if ($server === false) { - unlink($pemFile); - fwrite(STDERR, $errStr . "\n"); - exit(1); -} + $pkey = openssl_pkey_new(array( + 'config' => $configFile, + 'private_key_bits' => 2048, + 'private_key_type' => OPENSSL_KEYTYPE_RSA, + )); + if ($pkey === false) { + throw new RuntimeException('Unable to generate the TLS fixture private key.'); + } + + $csr = openssl_csr_new( + array('commonName' => '127.0.0.1'), + $pkey, + array( + 'config' => $configFile, + 'digest_alg' => 'sha256', + 'req_extensions' => 'v3_req', + ) + ); + if ($csr === false) { + throw new RuntimeException('Unable to generate the TLS fixture certificate request.'); + } + + $cert = openssl_csr_sign( + $csr, + null, + $pkey, + 1, + array( + 'config' => $configFile, + 'digest_alg' => 'sha256', + 'x509_extensions' => 'v3_req', + ) + ); + if ($cert === false) { + throw new RuntimeException('Unable to sign the TLS fixture certificate.'); + } + + if (!openssl_x509_export($cert, $certPem) || !openssl_pkey_export($pkey, $keyPem)) { + throw new RuntimeException('Unable to export the TLS fixture certificate.'); + } + + if (file_put_contents($pemFile, $certPem . $keyPem) === false) { + throw new RuntimeException('Unable to write the TLS fixture certificate.'); + } + + $context = stream_context_create(array( + 'ssl' => array( + 'local_cert' => $pemFile, + ), + )); + $server = @stream_socket_server( + 'tls://127.0.0.1:0', + $errNo, + $errStr, + STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, + $context + ); + if ($server === false) { + throw new RuntimeException('Unable to start the TLS fixture server: ' . $errStr); + } -$name = stream_socket_get_name($server, false); -$port = substr($name, strrpos($name, ':') + 1); -fwrite(STDOUT, $port . "\n"); -fflush(STDOUT); + $name = stream_socket_get_name($server, false); + if ($name === false) { + throw new RuntimeException('Unable to read the TLS fixture server address.'); + } + $port = substr($name, strrpos($name, ':') + 1); + $startupInfo = json_encode(array( + 'port' => (int)$port, + )); + if ($startupInfo === false) { + throw new RuntimeException('Unable to encode the TLS fixture startup information.'); + } + fwrite(STDOUT, $startupInfo . "\n"); + fflush(STDOUT); -$conn = @stream_socket_accept($server, 10); -if (is_resource($conn)) { - fclose($conn); + for ($i = 0; $i < 2; $i++) { + $conn = @stream_socket_accept($server, 10); + if (is_resource($conn)) { + stream_set_timeout($conn, 10); + $request = ''; + while (!feof($conn) && strpos($request, "\r\n\r\n") === false) { + $chunk = fread($conn, 1024); + if ($chunk === false) { + break; + } + $request .= $chunk; + } + fwrite($conn, "HTTP/1.1 200 OK\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"); + fflush($conn); + fclose($conn); + } + } +} catch (Throwable $e) { + fwrite(STDERR, $e->getMessage() . "\n"); + $exitCode = 1; +} finally { + if (is_resource($server)) { + fclose($server); + } } -unlink($pemFile); -exit(0); +exit($exitCode); From 9850847892292ee4abd8e04e9f5658af37020769 Mon Sep 17 00:00:00 2001 From: basi Date: Wed, 26 Aug 2026 18:14:03 +0900 Subject: [PATCH 7/7] fix: support PHP 8.4 debugger highlight markup --- lib/Cake/Test/Case/Utility/DebuggerTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Cake/Test/Case/Utility/DebuggerTest.php b/lib/Cake/Test/Case/Utility/DebuggerTest.php index a14fd8110b..97d44213e1 100644 --- a/lib/Cake/Test/Case/Utility/DebuggerTest.php +++ b/lib/Cake/Test/Case/Utility/DebuggerTest.php @@ -86,7 +86,7 @@ public function testExcerpt() { $this->assertTrue(is_array($result)); $this->assertEquals(4, count($result)); - $pattern = '/.*?.*?<\?php/'; + $pattern = '/]*)?>.*?.*?<\?php/'; $this->assertMatchesRegularExpression($pattern, $result[0]); $result = Debugger::excerpt(__FILE__, 11, 2);