Skip to content

Fix amount tampering, broken IPN URL and invoice status handling (19.0) - #19

Open
SatoDri wants to merge 3 commits into
btcpayserver:19.0from
SatoDri:19.0-fix-security-and-ipn
Open

Fix amount tampering, broken IPN URL and invoice status handling (19.0)#19
SatoDri wants to merge 3 commits into
btcpayserver:19.0from
SatoDri:19.0-fix-security-and-ipn

Conversation

@SatoDri

@SatoDri SatoDri commented Jul 19, 2026

Copy link
Copy Markdown

What this fixes

This PR fixes a security issue and a couple of bugs in the checkout and notification flow of payment_btcpayserver on the 19.0 branch. (A companion PR does the same on 18.0: #18.)

Security: the invoice amount could be tampered with

The checkout controller built the BTCPay invoice from the redirect form's POST data (price, currency, orderId), and _extract_amount_data returned None to opt out of the framework's amount validation. Nothing ever compared the settled amount to the transaction, so a buyer could submit an arbitrary amount, pay that (e.g. a few cents) and still have the order confirmed for its full value.

Fix:

  • The invoice is now built server-side from the transaction record; the redirect form only carries the transaction reference.
  • _extract_amount_data now returns the amount and currency actually settled on BTCPay (read back from the invoice in the IPN), so the generic _validate_amount compares them against the transaction before it is set to done.

Other fixes

  • IPN was never received: the notification URL was concatenated without a leading slash (base_url + "payment/btcpay/ipn"), producing an unreachable host, so payments could never be confirmed. It is now built with urls.url_join.
  • expired invoices stayed pending forever: the expired status is now handled (transaction cancelled), and unknown statuses set an error instead of being ignored.
  • Removed the dead payment.provider.create() override (it guarded on an empty self.code, so it never ran; the private key is generated during pairing).
  • Declared the real Python dependency: the vendored client (models/libs) imports ecdsa, while the manifest listed the unused btcpay-python.

Notes

  • No model/schema change; version bumped to 19.0.1.1.
  • Uses the 19.0 payment API (_process / _extract_amount_data / _apply_updates); the double-confirmation-email and crsf typo issues present on 18.0 are already gone here thanks to that refactor.
  • Commits are split by concern (dependency, dead-code removal, checkout/notification fixes).
  • Syntax-checked and linted (pyflakes clean); I have not run it against a live BTCPay store on this branch, so a functional test on your side is welcome.

SatoDri added 3 commits July 20, 2026 00:06
The bundled BTCPay client (models/libs) imports ecdsa, while the manifest
and requirements only listed btcpay-python, which is not used (the client
is vendored). Installations that did not already have ecdsa available
failed the external-dependency check. Declare ecdsa instead and bump the
module version.
The override guarded on self.code, which is empty on the model-level
recordset passed to create(), so the branch never ran. The private key is
generated during pairing (in the onchange) instead, so the override is
dead code; removing it also keeps copying a provider from regenerating
(and thus invalidating) an already paired key.
Security: the checkout controller built the BTCPay invoice from the POSTed
redirect-form data (price, currency, orderId), and _extract_amount_data
returned None to opt out of amount validation, so the settled amount was
never checked against the transaction. A buyer could therefore submit an
arbitrary price and confirm an order after paying far less. The invoice is
now built server-side from the transaction, the redirect form only carries
the reference, and _extract_amount_data returns the amount and currency
settled on BTCPay (read back from the invoice in the IPN) so the framework
validates them against the transaction before it is set done.

Other fixes in the notification path:
- Fix the IPN notification URL: it was concatenated without a leading
  slash (base_url + "payment/btcpay/ipn"), producing an unreachable host
  so notifications never arrived. It is now built with urls.url_join.
- Handle the "expired" invoice status (cancel the transaction) instead of
  leaving it pending forever, and set an error on unknown statuses.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant