From 752ba1ed1e7eb667c48a4257c0c322e63b42446c Mon Sep 17 00:00:00 2001 From: Sander Kondratjev Date: Tue, 16 Jun 2026 13:25:51 +0300 Subject: [PATCH 1/2] NFC-169 Fix PHP auth cert chain, format, SSRF, cookie and OCSP checks Signed-off-by: Sander Kondratjev --- example/public/index.php | 10 +++++ src/certificate/CertificateValidator.php | 4 ++ src/validator/ocsp/OcspClientImpl.php | 4 +- .../AuthTokenVersion11Validator.php | 19 ++++++++- .../AuthTokenVersion1Validator.php | 6 +-- .../AuthTokenVersion11ValidatorTest.php | 39 ++++++++++++++++++- .../AuthTokenVersion1ValidatorTest.php | 4 +- 7 files changed, 76 insertions(+), 10 deletions(-) diff --git a/example/public/index.php b/example/public/index.php index b8494a8..2cf4dda 100644 --- a/example/public/index.php +++ b/example/public/index.php @@ -26,6 +26,16 @@ ini_set('display_errors', '0'); +session_name('__Host-PHPSESSID'); + +session_set_cookie_params([ + 'lifetime' => 0, + 'path' => '/', + 'secure' => true, + 'httponly' => true, + 'samesite' => 'Lax', +]); + session_start(); // Uncomment following line to define the custom log location (by default the server log is used) diff --git a/src/certificate/CertificateValidator.php b/src/certificate/CertificateValidator.php index c680f85..16fa9e1 100644 --- a/src/certificate/CertificateValidator.php +++ b/src/certificate/CertificateValidator.php @@ -73,6 +73,10 @@ public static function validateIsValidAndSignedByTrustedCA( $now = DefaultClock::getInstance()->now(); self::certificateIsValidOnDate($certificate, $now, "User"); + // Prevent SSRF via CA Issuers URI from user-provided certificate AIA. + // All trusted/intermediate CA certificates must be provided by configuration. + X509::disableURLFetch(); + foreach ($trustedCertificates->getCertificates() as $trustedCertificate) { $certificate->loadCA( $trustedCertificate->saveX509($trustedCertificate->getCurrentCert(), X509::FORMAT_PEM) diff --git a/src/validator/ocsp/OcspClientImpl.php b/src/validator/ocsp/OcspClientImpl.php index 4da3852..700eaa1 100644 --- a/src/validator/ocsp/OcspClientImpl.php +++ b/src/validator/ocsp/OcspClientImpl.php @@ -66,7 +66,9 @@ public function request(Uri $uri, string $encodedOcspRequest): OcspResponse $info = curl_getinfo($curl); if ($info["http_code"] !== 200) { - throw new UserCertificateOCSPCheckFailedException("OCSP request was not successful, response: " + $result); + throw new UserCertificateOCSPCheckFailedException( + "OCSP request was not successful, response: " . (is_string($result) ? $result : '') + ); } $response = new OcspResponse($result); diff --git a/src/validator/versionvalidators/AuthTokenVersion11Validator.php b/src/validator/versionvalidators/AuthTokenVersion11Validator.php index c0bd652..da02c2f 100644 --- a/src/validator/versionvalidators/AuthTokenVersion11Validator.php +++ b/src/validator/versionvalidators/AuthTokenVersion11Validator.php @@ -57,8 +57,7 @@ class AuthTokenVersion11Validator extends AuthTokenVersion1Validator public function supports(?string $format): bool { - return $format !== null && - str_starts_with($format, self::V11_SUPPORTED_TOKEN_FORMAT_PREFIX); + return $format === self::V11_SUPPORTED_TOKEN_FORMAT_PREFIX; } /** @@ -84,6 +83,7 @@ public function validate( $this->validateSameIssuer($subjectCertificate, $signingCertificate); $this->validateSigningCertificateValidity($signingCertificate); $this->validateKeyUsage($signingCertificate); + $this->validateSigningCertificateChain($signingCertificate); } return $subjectCertificate; @@ -239,6 +239,21 @@ private function validateKeyUsage(X509 $signingCertificate): void } } + /** + * @throws AuthTokenParseException + */ + private function validateSigningCertificateChain(X509 $signingCertificate): void + { + try { + $this->buildTrustValidatorBatch()->executeFor($signingCertificate); + } catch (AuthTokenException $e) { + throw new AuthTokenParseException( + "Signing certificate chain validation failed", + $e, + ); + } + } + /** * @throws AuthTokenException */ diff --git a/src/validator/versionvalidators/AuthTokenVersion1Validator.php b/src/validator/versionvalidators/AuthTokenVersion1Validator.php index 8e44a1e..f4689bd 100644 --- a/src/validator/versionvalidators/AuthTokenVersion1Validator.php +++ b/src/validator/versionvalidators/AuthTokenVersion1Validator.php @@ -73,8 +73,8 @@ public function __construct( public function supports(?string $format): bool { - return $format !== null && - str_starts_with($format, self::V1_SUPPORTED_TOKEN_FORMAT_PREFIX); + return $format === self::V1_SUPPORTED_TOKEN_FORMAT_PREFIX || + $format === "web-eid:1.0"; } public function validate( @@ -135,7 +135,7 @@ public function validate( return $subjectCertificate; } - private function buildTrustValidatorBatch(): SubjectCertificateValidatorBatch + protected function buildTrustValidatorBatch(): SubjectCertificateValidatorBatch { $trustedValidator = new SubjectCertificateTrustedValidator( $this->trustedCACertificates, diff --git a/tests/validator/versionvalidators/AuthTokenVersion11ValidatorTest.php b/tests/validator/versionvalidators/AuthTokenVersion11ValidatorTest.php index 4367738..a6cbf54 100644 --- a/tests/validator/versionvalidators/AuthTokenVersion11ValidatorTest.php +++ b/tests/validator/versionvalidators/AuthTokenVersion11ValidatorTest.php @@ -72,8 +72,6 @@ public static function validFormats(): array { return [ ['web-eid:1.1'], - ['web-eid:1.1.0'], - ['web-eid:1.10'], ]; } @@ -96,6 +94,8 @@ public static function invalidFormats(): array ['web-eid:1.0'], ['web-eid:2'], ['webauthn:1.1'], + ['web-eid:1.1.0'], + ['web-eid:1.10'], ]; } @@ -241,4 +241,39 @@ public function testMissingSupportedAlgorithmsFails(): void $spy->validate($token, 'nonce'); } + + /** + * @throws CertificateDecodingException + * @throws AuthTokenException + */ + public function testSigningCertificateChainValidationFails(): void + { + $certPath = __DIR__ . '/../../_resources/TEST_of_ESTEID2018.cer'; + $der = file_get_contents($certPath); + $this->assertIsString($der, "Certificate missing at: $certPath"); + + $signingCertificate = new X509(); + $this->assertNotFalse($signingCertificate->loadX509($der)); + + $config = new AuthTokenValidationConfiguration(); + $config->setUserCertificateRevocationCheckWithOcspDisabled(); + + $validator = new class( + $this->createMock(SubjectCertificateValidatorBatch::class), + CertificateValidator::buildTrustFromCertificates([]), + $this->createMock(AuthTokenSignatureValidator::class), + $config, + null, + null + ) extends AuthTokenVersion11Validator { + public function assertChainFails(X509 $certificate): void + { + $this->buildTrustValidatorBatch()->executeFor($certificate); + } + }; + + $this->expectException(AuthTokenException::class); + + $validator->assertChainFails($signingCertificate); + } } diff --git a/tests/validator/versionvalidators/AuthTokenVersion1ValidatorTest.php b/tests/validator/versionvalidators/AuthTokenVersion1ValidatorTest.php index c5065cb..e7dc913 100644 --- a/tests/validator/versionvalidators/AuthTokenVersion1ValidatorTest.php +++ b/tests/validator/versionvalidators/AuthTokenVersion1ValidatorTest.php @@ -68,8 +68,6 @@ public static function validFormats(): array return [ ['web-eid:1'], ['web-eid:1.0'], - ['web-eid:1.1'], - ['web-eid:1.10'], ]; } @@ -92,6 +90,8 @@ public static function invalidFormats(): array ['web-eid:0.9'], ['web-eid:2'], ['webauthn:1'], + ['web-eid:1.1'], + ['web-eid:1.10'], ]; } From 3789b0ec98307252a612d33c9223bde490c75a81 Mon Sep 17 00:00:00 2001 From: Sander Kondratjev Date: Tue, 16 Jun 2026 13:48:56 +0300 Subject: [PATCH 2/2] NFC-169 composer lock Signed-off-by: Sander Kondratjev --- composer.lock | 183 ++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 170 insertions(+), 13 deletions(-) diff --git a/composer.lock b/composer.lock index b05355e..da36393 100644 --- a/composer.lock +++ b/composer.lock @@ -4,27 +4,29 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "06a60bf5c664ea446c1588bc886b8552", + "content-hash": "f27b570b2569391348e4efe27ec77138", "packages": [ { "name": "guzzlehttp/psr7", - "version": "2.9.0", + "version": "2.11.1", "source": { "type": "git", "url": "https://github.com/guzzle/psr7.git", - "reference": "7d0ed42f28e42d61352a7a79de682e5e67fec884" + "reference": "640e2897bbee822dbc8af761d49e1a29b1f2a6b1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/psr7/zipball/7d0ed42f28e42d61352a7a79de682e5e67fec884", - "reference": "7d0ed42f28e42d61352a7a79de682e5e67fec884", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/640e2897bbee822dbc8af761d49e1a29b1f2a6b1", + "reference": "640e2897bbee822dbc8af761d49e1a29b1f2a6b1", "shasum": "" }, "require": { "php": "^7.2.5 || ^8.0", "psr/http-factory": "^1.0", "psr/http-message": "^1.1 || ^2.0", - "ralouphie/getallheaders": "^3.0" + "ralouphie/getallheaders": "^3.0", + "symfony/deprecation-contracts": "^2.5 || ^3.0", + "symfony/polyfill-php80": "^1.24" }, "provide": { "psr/http-factory-implementation": "1.0", @@ -32,9 +34,9 @@ }, "require-dev": { "bamarni/composer-bin-plugin": "^1.8.2", - "http-interop/http-factory-tests": "0.9.0", + "http-interop/http-factory-tests": "1.1.0", "jshttp/mime-db": "1.54.0.1", - "phpunit/phpunit": "^8.5.44 || ^9.6.25" + "phpunit/phpunit": "^8.5.52 || ^9.6.34" }, "suggest": { "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" @@ -105,7 +107,7 @@ ], "support": { "issues": "https://github.com/guzzle/psr7/issues", - "source": "https://github.com/guzzle/psr7/tree/2.9.0" + "source": "https://github.com/guzzle/psr7/tree/2.11.1" }, "funding": [ { @@ -121,7 +123,7 @@ "type": "tidelift" } ], - "time": "2026-03-10T16:41:02+00:00" + "time": "2026-06-12T21:50:12+00:00" }, { "name": "paragonie/constant_time_encoding", @@ -553,6 +555,161 @@ "source": "https://github.com/ralouphie/getallheaders/tree/develop" }, "time": "2019-03-08T08:55:37+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v3.7.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "50f59d1f3ca46d41ac911f97a78626b6756af35b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/50f59d1f3ca46d41ac911f97a78626b6756af35b", + "reference": "50f59d1f3ca46d41ac911f97a78626b6756af35b", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.7-dev" + } + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.7.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-04-13T15:52:40+00:00" + }, + { + "name": "symfony/polyfill-php80", + "version": "v1.37.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "dfb55726c3a76ea3b6459fcfda1ec2d80a682411" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/dfb55726c3a76ea3b6459fcfda1ec2d80a682411", + "reference": "dfb55726c3a76ea3b6459fcfda1ec2d80a682411", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php80\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ion Bazan", + "email": "ion.bazan@gmail.com" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php80/tree/v1.37.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-04-10T16:19:22+00:00" } ], "packages-dev": [ @@ -2307,12 +2464,12 @@ ], "aliases": [], "minimum-stability": "stable", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": false, "prefer-lowest": false, "platform": { "php": ">=8.3" }, - "platform-dev": [], - "plugin-api-version": "2.6.0" + "platform-dev": {}, + "plugin-api-version": "2.9.0" }