Skip to content

2.1.0 - timing-safe payment-window verification, strict_types

Choose a tag to compare

@tomsommer tomsommer released this 07 Sep 15:55
· 13 commits to master since this release

Security and correctness release on the payment-acceptance path, plus strict_types across the tree.

composer require tomsommer/onpay-php-sdk:^2.1

Payment window verification

PaymentWindow::validatePayment() had three problems, all inherited from upstream:

  • The HMAC was compared with ===, which is not timing-safe. It now uses hash_equals().
  • Signed fields were selected by substring, not prefix. The window writes its fields with an onpay_ prefix, but verification collected anything matching strpos($key, 'onpay_') !== false. An unrelated query parameter that merely contained the substring — tracking_onpay_campaign, for instance — was pulled into the comparison set and broke verification of a perfectly good payment. Selection is now by prefix. There is a regression test that fails against the old code.
  • An array-valued onpay_hmac_sha1 reached the comparison. ?onpay_hmac_sha1[]=x is trivially injectable and hash_equals() rejects a non-string with a TypeError, so non-strings are now turned away before the compare.

The window secret is also typed and required where it is used. Hashing with an empty key produced a signature that could never match, so getFormFields(), generateSecret() and validatePayment() now raise MissingDataException rather than failing quietly.

Refresh tokens survive a refresh

RFC 6749 §6 permits a server to omit refresh_token from a refresh response, in which case the previous one stays valid. The SDK saved the response verbatim, dropping the refresh token and making the next expiry unrecoverable. The old token is now carried over.

strict_types

declare(strict_types=1) in all 61 files. Amounts travel through this SDK as numeric strings and get compared against ints, which is precisely where silent coercion does damage.

Breaking changes

  • PaymentWindow::setSecret() is typed ?string; getSecret() returns ?string.
  • getFormFields(), generateSecret() and validatePayment() throw OnPay\API\Exception\MissingDataException when no window secret has been set.
  • A query parameter containing onpay_ other than as a prefix is no longer part of the signed set. If you were relying on the old substring behaviour, you were relying on a bug.

Everything from 2.0.1 applies unchanged: league/oauth2-client, PSR-18 transport, PSR-3 logging, PSR-7 last-exchange accessors, PHP 8.2+.