From 05c4c9fed4280088d722c8dd37ae28377f198cb2 Mon Sep 17 00:00:00 2001 From: pkedar Date: Tue, 29 Sep 2026 15:27:01 -0400 Subject: [PATCH 1/4] LI-197450 - Pin JWE alg/enc headers before decryption Prevents an algorithm-confusion/downgrade attack (CWE-347): decrypt() previously called $jwe->decrypt($privateJweKey) without validating the untrusted alg/enc header fields, so an attacker intercepting a response could flip alg to legacy RSA1_5 and force the private key to be used with PKCS#1 v1.5 padding, vulnerable to Bleichenbacher-style attacks. Mirrors the algorithm pinning already applied on the JWS side. Co-Authored-By: Claude Sonnet 5 --- .../Util/HyperwalletEncryption.php | 20 ++++++ .../Tests/Util/HyperwalletEncryptionTest.php | 62 +++++++++++++++++++ 2 files changed, 82 insertions(+) diff --git a/src/Hyperwallet/Util/HyperwalletEncryption.php b/src/Hyperwallet/Util/HyperwalletEncryption.php index 6f6896fe..73cb6529 100644 --- a/src/Hyperwallet/Util/HyperwalletEncryption.php +++ b/src/Hyperwallet/Util/HyperwalletEncryption.php @@ -136,6 +136,7 @@ public function encrypt($body) { public function decrypt($body) { $privateJweKey = $this->getPrivateJweKey(); $jwe = JOSE_JWT::decode($body); + $this->checkJweHeaderAlgorithm($jwe->header); $decryptedBody = $jwe->decrypt($privateJweKey); $publicJwsKey = $this->getPublicJwsKey(); @@ -351,6 +352,25 @@ public function checkJwsExpiration($header) { } } + /** + * Checks that the JWE header advertises the key-management algorithm and content-encryption method + * this client expects, before the header-controlled algorithm is ever used to decrypt with the + * private key. Prevents an attacker from forcing algorithm downgrade (e.g. to legacy RSA1_5) by + * tampering with the untrusted alg/enc header fields of an intercepted response. + * + * @param array $header JWE header array + * + * @throws HyperwalletException + */ + public function checkJweHeaderAlgorithm($header) { + if (!isset($header['alg']) || $header['alg'] !== $this->encryptionAlgorithm) { + throw new HyperwalletException('While trying to decrypt JWE, unexpected [alg] header found'); + } + if (!isset($header['enc']) || $header['enc'] !== $this->encryptionMethod) { + throw new HyperwalletException('While trying to decrypt JWE, unexpected [enc] header found'); + } + } + /** * Finds the path of composer vendor directory * diff --git a/tests/Hyperwallet/Tests/Util/HyperwalletEncryptionTest.php b/tests/Hyperwallet/Tests/Util/HyperwalletEncryptionTest.php index b3f96304..8ae705fe 100644 --- a/tests/Hyperwallet/Tests/Util/HyperwalletEncryptionTest.php +++ b/tests/Hyperwallet/Tests/Util/HyperwalletEncryptionTest.php @@ -137,6 +137,68 @@ public function testShouldThrowExceptionWhenJwsSignatureExpHeaderParamIsNotInteg } } + public function testShouldThrowExceptionWhenJweAlgHeaderDoesNotMatchExpectedAlgorithm() { + // Setup data + $header = array( + "alg" => "RSA1_5", + "enc" => "A256CBC-HS512" + ); + $clientPath = __DIR__ . "/../../../resources/private-jwkset1"; + $hyperwalletPath = __DIR__ . "/../../../resources/public-jwkset1"; + $encryption = new HyperwalletEncryption($clientPath, $hyperwalletPath); + + // Execute test + try { + $encryption->checkJweHeaderAlgorithm($header); + $this->fail('HyperwalletException expected'); + } catch (HyperwalletException $e) { + $this->assertEquals('While trying to decrypt JWE, unexpected [alg] header found', $e->getMessage()); + } + } + + public function testShouldThrowExceptionWhenJweEncHeaderDoesNotMatchExpectedEncryptionMethod() { + // Setup data + $header = array( + "alg" => "RSA-OAEP-256", + "enc" => "A128CBC-HS256" + ); + $clientPath = __DIR__ . "/../../../resources/private-jwkset1"; + $hyperwalletPath = __DIR__ . "/../../../resources/public-jwkset1"; + $encryption = new HyperwalletEncryption($clientPath, $hyperwalletPath); + + // Execute test + try { + $encryption->checkJweHeaderAlgorithm($header); + $this->fail('HyperwalletException expected'); + } catch (HyperwalletException $e) { + $this->assertEquals('While trying to decrypt JWE, unexpected [enc] header found', $e->getMessage()); + } + } + + public function testShouldRejectTamperedJweAlgHeaderBeforeDecryption() { + // Setup data: an attacker downgrades the alg header on an intercepted, otherwise valid JWE + $clientPath = __DIR__ . "/../../../resources/private-jwkset1"; + $hyperwalletPath = __DIR__ . "/../../../resources/public-jwkset1"; + $originalMessage = "Test message"; + $encryption = new HyperwalletEncryption($clientPath, $hyperwalletPath); + $encryptedMessage = $encryption->encrypt($originalMessage); + + $parts = explode('.', $encryptedMessage); + $header = json_decode(base64_decode(strtr($parts[0], '-_', '+/')), true); + $header['alg'] = 'RSA1_5'; + $tamperedHeader = rtrim(strtr(base64_encode(json_encode($header)), '+/', '-_'), '='); + $parts[0] = $tamperedHeader; + $tamperedMessage = implode('.', $parts); + + // Execute test + try { + $encryption->decrypt($tamperedMessage); + $this->fail('HyperwalletException expected'); + } catch (HyperwalletException $e) { + $this->assertEquals('While trying to decrypt JWE, unexpected [alg] header found', $e->getMessage()); + } + } + public function testShouldThrowExceptionWhenJwsSignatureHasExpired() { // Setup data $header = array( From 2c6daf1bfe455f61fb84dc5f0239d957a9214b9e Mon Sep 17 00:00:00 2001 From: pkedar Date: Tue, 29 Sep 2026 15:53:28 -0400 Subject: [PATCH 2/4] Fix CI: allow installing dev dependencies with security advisories Composer's dependency resolver now excludes package versions with known security advisories from the pool by default. phpunit/phpunit's ^5.7/^7.0.0 branches (needed for the PHP 5.6/7.1/7.2 CI jobs) carry advisories, so the resolver was left only with phpunit 9.6.x, which requires PHP >=7.3 and breaks composer install on those older PHP versions in CI. phpunit is a require-dev-only testing dependency; consumers of this SDK never install it (composer install --no-dev skips it), so this has no effect on production dependency security. Verified via `composer audit --no-dev` that the production dependency tree has zero advisories. Add --no-blocking to the CI composer install commands so the resolver can still pick a PHP-compatible phpunit version for each matrix entry. Co-Authored-By: Claude Sonnet 5 --- .github/workflows/ci.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e35438c3..306715c5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,7 +36,7 @@ jobs: #get dependencies - name: Install dependencies - run: composer install --prefer-dist --no-progress --no-suggest + run: composer install --prefer-dist --no-progress --no-suggest --no-blocking #get static analysis tool - name: Install static analysis tool @@ -72,7 +72,7 @@ jobs: run: composer validate - name: Install dependencies - run: composer install --prefer-dist --no-progress --no-suggest + run: composer install --prefer-dist --no-progress --no-suggest --no-blocking - name: Run test suite run: | @@ -102,7 +102,7 @@ jobs: run: composer validate - name: Install dependencies - run: composer install --prefer-dist --no-progress --no-suggest + run: composer install --prefer-dist --no-progress --no-suggest --no-blocking - name: Run test suite run: | From 86e0d82b71b4886a2be9e492d5218f5ba0241777 Mon Sep 17 00:00:00 2001 From: pkedar Date: Tue, 29 Sep 2026 15:56:17 -0400 Subject: [PATCH 3/4] Fix CI: use COMPOSER_NO_BLOCKING env var instead of CLI flag The --no-blocking CLI flag doesn't exist in the older Composer version bundled for the PHP 7.1 CI job, causing a hard "option does not exist" error there. Composer also supports this via the COMPOSER_NO_BLOCKING=1 environment variable, which older Composer versions simply ignore rather than erroring on, so it works across the entire PHP version matrix. Co-Authored-By: Claude Sonnet 5 --- .github/workflows/ci.yml | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 306715c5..3211e6f4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,7 +36,9 @@ jobs: #get dependencies - name: Install dependencies - run: composer install --prefer-dist --no-progress --no-suggest --no-blocking + env: + COMPOSER_NO_BLOCKING: 1 + run: composer install --prefer-dist --no-progress --no-suggest #get static analysis tool - name: Install static analysis tool @@ -72,7 +74,9 @@ jobs: run: composer validate - name: Install dependencies - run: composer install --prefer-dist --no-progress --no-suggest --no-blocking + env: + COMPOSER_NO_BLOCKING: 1 + run: composer install --prefer-dist --no-progress --no-suggest - name: Run test suite run: | @@ -102,7 +106,9 @@ jobs: run: composer validate - name: Install dependencies - run: composer install --prefer-dist --no-progress --no-suggest --no-blocking + env: + COMPOSER_NO_BLOCKING: 1 + run: composer install --prefer-dist --no-progress --no-suggest - name: Run test suite run: | From f30d279b728b3e97629c62b11759fea308c60e88 Mon Sep 17 00:00:00 2001 From: pkedar Date: Thu, 1 Oct 2026 13:02:01 -0400 Subject: [PATCH 4/4] Bump version to 2.2.7 for JWE algorithm-pinning security fix Update CHANGELOG.md and ApiClient::VERSION so consumers can identify and pull in the fix via the package manager. Co-Authored-By: Claude Sonnet 5 --- CHANGELOG.md | 4 ++++ src/Hyperwallet/Util/ApiClient.php | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 24c1cc54..91e59e89 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ ChangeLog ========= +2.2.7 +------------------- +- Security fix: JWE decryption now validates the `alg`/`enc` header fields against the client's configured algorithm before decrypting, preventing an algorithm-confusion/downgrade attack where a tampered response header could force use of the legacy, Bleichenbacher-vulnerable RSA1_5 key-management algorithm. + 2.2.6 ------------------- - Added a new notes field to the Payment object, allowing users to include supplementary information or comments related to a payment. diff --git a/src/Hyperwallet/Util/ApiClient.php b/src/Hyperwallet/Util/ApiClient.php index f51327dd..286b3f7e 100644 --- a/src/Hyperwallet/Util/ApiClient.php +++ b/src/Hyperwallet/Util/ApiClient.php @@ -21,7 +21,7 @@ class ApiClient { * * @var string */ - const VERSION = '2.2.3'; + const VERSION = '2.2.7'; /** * The Guzzle http client