|
| 1 | +--- |
| 2 | +layout: advisory |
| 3 | +title: 'GHSA-mjgf-xj26-9qf9 (pay): pay-rails/pay - non-constant-time HMAC comparison |
| 4 | + in Paddle Billing webhook signature verifier' |
| 5 | +comments: false |
| 6 | +categories: |
| 7 | +- pay |
| 8 | +advisory: |
| 9 | + gem: pay |
| 10 | + ghsa: mjgf-xj26-9qf9 |
| 11 | + url: https://github.com/advisories/GHSA-mjgf-xj26-9qf9 |
| 12 | + title: pay-rails/pay - non-constant-time HMAC comparison in Paddle Billing webhook |
| 13 | + signature verifier |
| 14 | + date: 2026-07-01 |
| 15 | + description: |- |
| 16 | + ## Summary |
| 17 | +
|
| 18 | + The `Pay::Webhooks::PaddleBillingController#valid_signature?` |
| 19 | + (`app/controllers/pay/webhooks/paddle_billing_controller.rb`) |
| 20 | + verifies the Paddle Billing webhook signature by computing |
| 21 | + `OpenSSL::HMAC.hexdigest(...)` and comparing it to the attacker-supplied |
| 22 | + header value using Ruby's `String#==`. Ruby's `==` is non-constant-time — |
| 23 | + it returns as soon as the first byte mismatches — and exposes a per-byte |
| 24 | + timing side channel on the webhook signature verification path. The |
| 25 | + canonical mitigation is to use a constant-time primitive |
| 26 | + (`OpenSSL.fixed_length_secure_compare` / |
| 27 | + `ActiveSupport::SecurityUtils.secure_compare`). |
| 28 | +
|
| 29 | + - IMPACT - CWE-208 — Observable Timing Discrepancy on the webhook |
| 30 | + signature verifier. |
| 31 | + - An attacker who can deliver requests to the `/pay/webhooks/paddle_billing` |
| 32 | + mount point can probe the verifier with guessed `Paddle-Signature` |
| 33 | + header values. Because `String#==` short-circuits on the first |
| 34 | + mismatching byte, the response-time distribution shifts as the prefix |
| 35 | + of the guess matches the real hex digest. |
| 36 | + - A signature recovered through the oracle lets the attacker deliver |
| 37 | + forged Paddle Billing webhook events (e.g. `subscription.created` / |
| 38 | + `transaction.completed`) against the host application. Pay's webhook |
| 39 | + 'processor enqueues a `Pay::Webhooks::ProcessJob` for any accepted' |
| 40 | + webhook, which downstream applications use to update billing state — |
| 41 | + including provisioning paid features, recording refunds, and |
| 42 | + triggering customer notifications. |
| 43 | + - The endpoint is internet-reachable by definition (Paddle must |
| 44 | + POST events to it). |
| 45 | +
|
| 46 | + - Credit - Reported by tonghuaroot (https://github.com/tonghuaroot) |
| 47 | + cvss_v3: 7.4 |
| 48 | + related: |
| 49 | + url: |
| 50 | + - https://advisories.gitlab.com/gem/pay/GHSA-mjgf-xj26-9qf9 |
| 51 | + - https://github.com/pay-rails/pay/security/advisories/GHSA-mjgf-xj26-9qf9 |
| 52 | + - https://github.com/advisories/GHSA-mjgf-xj26-9qf9 |
| 53 | + notes: | |
| 54 | + - Latest (unpatched) release as of 7/2/2026 is 11.6.1 |
| 55 | + - `pay` (rubygem) ≤ v11.6.1 (latest release as of 2026-05-27). |
| 56 | + - https://github.com/pay-rails/pay/releases/tag/v11.6.1 |
| 57 | + - https://rubygems.org/gems/pay/versions/11.6.1 |
| 58 | + - cvss_v3 value comes from project advisory. |
| 59 | + - No cve value, so missing cvss_v2 and cvss_v4 values. |
| 60 | +--- |
0 commit comments