Skip to content

Feat/btcpay support#167

Open
d4rp4t wants to merge 27 commits intocashubtc:mainfrom
d4rp4t:feat/btcpay-support
Open

Feat/btcpay support#167
d4rp4t wants to merge 27 commits intocashubtc:mainfrom
d4rp4t:feat/btcpay-support

Conversation

@d4rp4t
Copy link
Copy Markdown
Collaborator

@d4rp4t d4rp4t commented Feb 2, 2026

This PR adds support for BTCPayServer with Cashu plugin (optionally) installed.

@github-project-automation github-project-automation bot moved this to Backlog in Numo Feb 2, 2026
@d4rp4t d4rp4t force-pushed the feat/btcpay-support branch from 2aeae96 to 397d403 Compare February 26, 2026 11:27
@d4rp4t d4rp4t marked this pull request as ready for review February 27, 2026 21:11
@d4rp4t d4rp4t requested review from a1denvalu3 and callebtc and removed request for a1denvalu3 February 27, 2026 21:11
@d4rp4t d4rp4t force-pushed the feat/btcpay-support branch 4 times, most recently from 4f85e30 to ff3d612 Compare February 28, 2026 17:22
Comment thread .github/workflows/btcpay-integration.yml
@d4rp4t d4rp4t force-pushed the feat/btcpay-support branch from d05aff2 to fdbba4a Compare March 6, 2026 15:50
@d4rp4t d4rp4t force-pushed the feat/btcpay-support branch from 3d5833b to a74b714 Compare March 27, 2026 18:15
@codecov
Copy link
Copy Markdown

codecov bot commented Mar 27, 2026

Codecov Report

❌ Patch coverage is 8.49220% with 1056 lines in your changes missing coverage. Please review.
✅ Project coverage is 19.93%. Comparing base (22b7b9f) to head (71dbe1d).
⚠️ Report is 3 commits behind head on main.

Files with missing lines Patch % Lines
...cdreams/numo/core/wallet/impl/CdkWalletProvider.kt 0.68% 292 Missing ⚠️
.../com/electricdreams/numo/PaymentRequestActivity.kt 0.00% 200 Missing ⚠️
...ams/numo/core/payment/impl/BTCPayPaymentService.kt 7.69% 131 Missing and 1 partial ⚠️
...com/electricdreams/numo/core/wallet/WalletError.kt 0.00% 87 Missing ⚠️
...ms/numo/feature/history/PaymentsHistoryActivity.kt 6.74% 81 Missing and 2 partials ⚠️
...com/electricdreams/numo/core/wallet/WalletTypes.kt 0.00% 65 Missing ⚠️
...com/electricdreams/numo/ndef/CashuPaymentHelper.kt 0.00% 41 Missing ⚠️
...eams/numo/core/payment/impl/LocalPaymentService.kt 7.89% 35 Missing ⚠️
...ms/numo/feature/settings/BtcPaySettingsActivity.kt 62.76% 21 Missing and 14 partials ⚠️
...icdreams/numo/ui/adapter/PaymentsHistoryAdapter.kt 0.00% 27 Missing ⚠️
... and 10 more
Additional details and impacted files
@@             Coverage Diff              @@
##               main     #167      +/-   ##
============================================
- Coverage     20.64%   19.93%   -0.71%     
- Complexity      773      815      +42     
============================================
  Files           145      157      +12     
  Lines         18575    19767    +1192     
  Branches       2274     2462     +188     
============================================
+ Hits           3834     3940     +106     
- Misses        14261    15327    +1066     
- Partials        480      500      +20     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@d4rp4t d4rp4t requested a review from a1denvalu3 March 28, 2026 19:25
@a1denvalu3
Copy link
Copy Markdown
Collaborator

Report: Payment Bypass via NFC Token Parameter Injection in BTCPay Integration

Vulnerability Description:
In BTCPayPaymentService.kt, the redeemToken function constructs the URL to the BTCNutServer by appending the user-supplied NFC token directly into the query string using a StringBuilder without any URL encoding:

val urlBuilder = StringBuilder("${baseUrl()}/cashu/pay-invoice?token=$token")
if (!paymentId.isNullOrBlank()) {
    urlBuilder.append("&invoiceId=$paymentId")
}
val request = Request.Builder()
    .url(urlBuilder.toString())
    // ...

Because the token variable is populated directly from the parsed NFC NDEF payload, an attacker can append a URL fragment identifier (#) to the end of a valid 1-satoshi Cashu token (e.g., cashuA...#).

When this spoofed token is submitted via NFC, the resulting URL becomes:
${baseUrl()}/cashu/pay-invoice?token=cashuA...#&invoiceId=<MERCHANT_INVOICE_ID>

The HTTP client (OkHttp) and the BTCPay server will treat everything after the # as a URL fragment. This effectively drops the &invoiceId= parameter from the server's perspective, decoupling the token from the merchant's pending invoice.
BTCNutServer will successfully process the valid 1-satoshi token, credit it to the store's general balance, and return a 200 OK HTTP response.
Because redeemToken only checks if the HTTP response is successful (code 200..299), it will return a WalletResult.Success.

In PaymentRequestActivity.kt, upon receiving success from paymentService.redeemToken(), the application immediately assumes the local payment invoice is fully settled and invokes handleLightningPaymentSuccess(). It skips any verification of the actual paid amount against the requested invoice amount. This allows an attacker to completely bypass the payment requirement for an invoice of any amount by presenting a valid NFC token worth only a fraction of a cent (1 satoshi).

Proof of Concept (PoC):
An attacker creates an NDEF message mimicking a Cashu token but appends a URL fragment identifier.

  1. Attacker purchases goods worth 100,000 satoshis. Merchant uses Numo to generate a payment request.
  2. Attacker prepares an NFC card or emulator with a text payload containing a valid 1-satoshi Cashu token followed by a hash character:
    cashuAeyJ0b2tlbiI6W3sibWludCI6Imh0dHBz...#
  3. Attacker taps their NFC device to the merchant's Numo POS.
  4. Numo reads the token, passes it to paymentService.redeemToken().
  5. The request /cashu/pay-invoice?token=cashuA...#&invoiceId=... is sent.
  6. Server processes the 1-satoshi token successfully, completely ignoring the invoiceId parameter hidden in the fragment.
  7. Numo receives HTTP 200 OK, assumes the 100,000 satoshi invoice is fully paid, and shows the success screen.

Impact:
Critical. Complete bypass of point-of-sale payment enforcement. An attacker can defraud the merchant of goods or services of any value by paying only 1 satoshi.

Remediation:
Properly URL-encode query parameters when constructing URLs. Using OkHttp's HttpUrl.Builder is highly recommended over string concatenation for safe URL construction:

val url = baseUrl().toHttpUrlOrNull()?.newBuilder()
    ?.addPathSegments("cashu/pay-invoice")
    ?.addQueryParameter("token", token)
    ?.apply {
        if (!paymentId.isNullOrBlank()) {
            addQueryParameter("invoiceId", paymentId)
        }
    }
    ?.build()

d4rp4t and others added 11 commits April 13, 2026 19:03
Implement NUT-18 token redemption via BTCPay POST endpoint.
Add helpers in CashuPaymentHelper to parse NUT-18 transport and ID.
Add redeemTokenToPostEndpoint to BtcPayPaymentService.
Add unit tests for PaymentServiceFactory and BtcPaySettingsActivity.
mark btcpay invoices as expired when btcpay integration turned off
@d4rp4t d4rp4t force-pushed the feat/btcpay-support branch from e644513 to 7da6e82 Compare April 13, 2026 18:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Backlog

Development

Successfully merging this pull request may close these issues.

2 participants