Repository navigation
LI-197450 - Pin JWE alg/enc headers before decryption - #137
Conversation
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
Why this change1. The core fix (
The fix pins 2. The CI workflow change ( Unrelated to the vulnerability itself. Composer's resolver now excludes package versions with known security advisories from consideration by default.
Testing performed
|
| #get dependencies | ||
| - name: Install dependencies | ||
| env: | ||
| COMPOSER_NO_BLOCKING: 1 |
There was a problem hiding this comment.
Why do we need this param ?
There was a problem hiding this comment.
Composer's resolver was blocking PR with PHP version. phpunit/phpunit's older branches (needed for the PHP 5.6/7.1/7.2 test jobs) have advisories, so the resolver was left with only PHPUnit 9.x, which requires PHP ≥7.3 and broke composer install on the older PHP jobs in this repo's CI matrix.
phpunit is a require-dev-only testing dependency - it is never installed by consumers of this SDK. Setting COMPOSER_NO_BLOCKING=1 for CI's install step only affects dependency resolution for running our own test suite in CI, and has no effect on the SDK's production dependency tree. Verified with composer audit --no-dev that production dependencies have zero advisories.
There was a problem hiding this comment.
below is error it throwing , Hence we need to add COMPOSER_NO_BLOCKING=1
https://github.com/hyperwallet/php-sdk/actions/runs/36620439369/job/109584156782?pr=137
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 <noreply@anthropic.com>
|
Added it to this same PR rather than a follow-up, to keep the version bump tied directly to the fix it documents:
Full suite re-verified passing (2150 tests, 7908 assertions) after the bump. |
Summary
HyperwalletEncryption::decrypt()called$jwe->decrypt($privateJweKey)without validating the untrustedalg/encheader fields embedded in the ciphertext, so an attacker who could intercept/tamper with an encrypted response could flipalgto legacyRSA1_5and force the client's real private key to be used with PKCS#1 v1.5 padding — vulnerable to Bleichenbacher-style padding-oracle attacks.HyperwalletEncryption::checkJweHeaderAlgorithm($header), called immediately afterJOSE_JWT::decode($body)and before$jwe->decrypt()is ever invoked, which rejects the token unless bothalgandencexactly match the algorithm/encryption method this client instance was configured with. This mirrors the algorithm pinning already applied on the JWS verification side ($this->signAlgorithm).Testing
Local PHP/Composer installs were blocked in my environment (non-standard Homebrew prefix + corporate TLS-intercepting proxy blocking GitHub zip downloads), so I ran the full suite inside a Docker container with a fully installed
vendor/:Target file (
HyperwalletEncryptionTest.php): 11/11 passed, including 3 new regression tests added in this PR:testShouldThrowExceptionWhenJweAlgHeaderDoesNotMatchExpectedAlgorithmalg: RSA1_5(downgrade attempt) is rejected with'While trying to decrypt JWE, unexpected [alg] header found'testShouldThrowExceptionWhenJweEncHeaderDoesNotMatchExpectedEncryptionMethodencvalue is rejected with'While trying to decrypt JWE, unexpected [enc] header found'testShouldRejectTamperedJweAlgHeaderBeforeDecryptionalgheader of the resulting JWE toRSA1_5(leaving the ciphertext/key encryption untouched, as an attacker intercepting traffic would), and assertsdecrypt()throws the header-rejection exception — proving the check fires before$jwe->decrypt()is reachedFull suite: 2150 tests, 7908 assertions — all passed. No regressions in existing encryption/decryption, signature verification, or any other test group.
Test plan
alg/encheaders before private key is usedtestShouldSuccessfullyEncryptAndDecryptTextMessage) still passes unchanged