From e6b5e106670fde53b1fe14fdc16766f9b713fdf2 Mon Sep 17 00:00:00 2001 From: Ross Addison Date: Fri, 19 Jun 2026 15:41:08 +0100 Subject: [PATCH] fix(encryption): read alg/enc exclusively from protected header in JWEDecrypter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit RFC 7516 §4.1.1 (alg) and §4.1.2 (enc) require both parameters to be integrity-protected. The previous implementation passed the merged $completeHeader (sharedProtectedHeader + sharedHeader + recipientHeader) to getKeyEncryptionAlgorithm() and getContentEncryptionAlgorithm(). Because array_merge() exhibits last-wins behaviour, an attacker could override alg or enc by placing a different value in an unprotected header field, creating a TOCTOU split between HeaderCheckerManager validation and actual decryption. Fix: extract alg/enc exclusively from getSharedProtectedHeader(). The merged $completeHeader is still forwarded to decryptCEK() because ECDH parameters (epk, apu, apv) may legitimately reside in per-recipient unprotected headers. Also adds is_string() guards in both getter methods so a malformed non-string header value cannot reach the AlgorithmManager. Note: JWSVerifier::getAlgorithm() already reads alg from getProtectedHeader() only and is not affected. Fixes #114 --- src/Library/Encryption/JWEDecrypter.php | 34 ++++++++++++++++++------- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/src/Library/Encryption/JWEDecrypter.php b/src/Library/Encryption/JWEDecrypter.php index b70b3296..cca60b66 100644 --- a/src/Library/Encryption/JWEDecrypter.php +++ b/src/Library/Encryption/JWEDecrypter.php @@ -124,8 +124,12 @@ private function decryptRecipientKey( ); $this->checkCompleteHeader($completeHeader); - $key_encryption_algorithm = $this->getKeyEncryptionAlgorithm($completeHeader); - $content_encryption_algorithm = $this->getContentEncryptionAlgorithm($completeHeader); + // RFC 7516 §4.1.1 (alg) and §4.1.2 (enc) require both parameters to be + // integrity-protected. Reading them from the merged $completeHeader allows + // an attacker to override either value via an unprotected header field. + $sharedProtectedHeader = $jwe->getSharedProtectedHeader(); + $key_encryption_algorithm = $this->getKeyEncryptionAlgorithm($sharedProtectedHeader); + $content_encryption_algorithm = $this->getContentEncryptionAlgorithm($sharedProtectedHeader); $this->checkIvSize($jwe->getIV(), $content_encryption_algorithm->getIVSize()); @@ -253,26 +257,38 @@ private function checkCompleteHeader(array $completeHeaders): void } } - private function getKeyEncryptionAlgorithm(array $completeHeaders): KeyEncryptionAlgorithm + private function getKeyEncryptionAlgorithm(array $protectedHeader): KeyEncryptionAlgorithm { - $key_encryption_algorithm = $this->keyEncryptionAlgorithmManager->get($completeHeaders['alg']); + $alg = $protectedHeader['alg'] ?? null; + if (! is_string($alg) || $alg === '') { + throw new InvalidArgumentException( + 'The "alg" parameter must be a non-empty string in the protected header (RFC 7516 §4.1.1).' + ); + } + $key_encryption_algorithm = $this->keyEncryptionAlgorithmManager->get($alg); if (! $key_encryption_algorithm instanceof KeyEncryptionAlgorithm) { throw new InvalidArgumentException(sprintf( 'The key encryption algorithm "%s" is not supported or does not implement KeyEncryptionAlgorithm interface.', - $completeHeaders['alg'] + $alg )); } return $key_encryption_algorithm; } - private function getContentEncryptionAlgorithm(array $completeHeader): ContentEncryptionAlgorithm + private function getContentEncryptionAlgorithm(array $protectedHeader): ContentEncryptionAlgorithm { - $content_encryption_algorithm = $this->contentEncryptionAlgorithmManager->get($completeHeader['enc']); + $enc = $protectedHeader['enc'] ?? null; + if (! is_string($enc) || $enc === '') { + throw new InvalidArgumentException( + 'The "enc" parameter must be a non-empty string in the protected header (RFC 7516 §4.1.2).' + ); + } + $content_encryption_algorithm = $this->contentEncryptionAlgorithmManager->get($enc); if (! $content_encryption_algorithm instanceof ContentEncryptionAlgorithm) { throw new InvalidArgumentException(sprintf( - 'The key encryption algorithm "%s" is not supported or does not implement the ContentEncryption interface.', - $completeHeader['enc'] + 'The content encryption algorithm "%s" is not supported or does not implement the ContentEncryption interface.', + $enc )); }