diff --git a/app/composer.json b/app/composer.json index d1590e56ca..197bb66b06 100644 --- a/app/composer.json +++ b/app/composer.json @@ -22,9 +22,6 @@ "ext-mcrypt": "*", "cakephp/cakephp": "~2.9" }, - "require-dev": { - "phpunit/phpunit": "3.7.*" - }, "suggest": { "cakephp/cakephp-codesniffer": "Easily check code formatting against the CakePHP coding standards." }, diff --git a/composer.json b/composer.json index 6dd6e56677..f1fe7b536e 100644 --- a/composer.json +++ b/composer.json @@ -29,12 +29,26 @@ "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-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." + } + } + } + } }, "bin": [ "lib/Cake/Console/cake" diff --git a/lib/Cake/Test/Case/Network/Http/HttpSocketTest.php b/lib/Cake/Test/Case/Network/Http/HttpSocketTest.php index 07dfb6c87a..cdc45d8e37 100644 --- a/lib/Cake/Test/Case/Network/Http/HttpSocketTest.php +++ b/lib/Cake/Test/Case/Network/Http/HttpSocketTest.php @@ -1832,21 +1832,107 @@ 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 = null; + $pipes = array(); + $configFile = null; + $pemFile = null; + $fixtureInteractionsComplete = false; + $processExitCode = null; + 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, 'Invalid HTTP') !== false, 'Invalid HTTP Response received, skipping.'); - $this->assertStringContainsString('Failed to enable crypto', $message); + $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($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 { + 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/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); 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..a0d4088879 --- /dev/null +++ b/lib/Cake/Test/test_app/tls_server.php @@ -0,0 +1,151 @@ + $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); + 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); + + 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); + } +} + +exit($exitCode);