pay-rails/pay - non-constant-time HMAC comparison in Paddle Billing webhook signature verifier
Published: July 01, 2026
SECURITY IDENTIFIERS
- GHSA: GHSA-mjgf-xj26-9qf9
GEM
SEVERITY
CVSS v3.x: 7.4 (High)
PATCHED VERSIONS
None available.
DESCRIPTION
Summary
The Pay::Webhooks::PaddleBillingController#valid_signature?
(app/controllers/pay/webhooks/paddle_billing_controller.rb)
verifies the Paddle Billing webhook signature by computing
OpenSSL::HMAC.hexdigest(...) and comparing it to the attacker-supplied
header value using Ruby's String#==. Ruby's == is non-constant-time —
it returns as soon as the first byte mismatches — and exposes a per-byte
timing side channel on the webhook signature verification path. The
canonical mitigation is to use a constant-time primitive
(OpenSSL.fixed_length_secure_compare /
ActiveSupport::SecurityUtils.secure_compare).
- IMPACT - CWE-208 — Observable Timing Discrepancy on the webhook
signature verifier.
- An attacker who can deliver requests to the
/pay/webhooks/paddle_billingmount point can probe the verifier with guessedPaddle-Signatureheader values. BecauseString#==short-circuits on the first mismatching byte, the response-time distribution shifts as the prefix of the guess matches the real hex digest. - A signature recovered through the oracle lets the attacker deliver
forged Paddle Billing webhook events (e.g.
subscription.created/transaction.completed) against the host application. Pay's webhook 'processor enqueues aPay::Webhooks::ProcessJobfor any accepted' webhook, which downstream applications use to update billing state — including provisioning paid features, recording refunds, and triggering customer notifications. - The endpoint is internet-reachable by definition (Paddle must POST events to it).
- An attacker who can deliver requests to the
- Credit - Reported by tonghuaroot (https://github.com/tonghuaroot)
