PoS: Can disable zero amount invoices (#7066)
What changed, and why it matters
This commit adds a new optional setting for BTCPay Server's Point of Sale (PoS) app that lets store owners block invoices with a zero total amount. Previously, a customer could complete a checkout flow that created an invoice worth nothing, which could be used to generate fake or confusing payment records, test transactions, or possibly abuse downstream systems that expect only paid invoices. The change is a defensive feature, not a fix for a known active attack, and it is disabled by default.
Treat this as a low-risk hardening change. Merchants who rely on the PoS app and do not want zero-amount invoices should enable the new setting after upgrading. Review whether zero-amount invoices can still be created through other app types, APIs, or payment flows, since this guard is limited to the Point of Sale plugin. No urgent patching is indicated absent a separate advisory.
Security signals we found
New server-side guard against zero-amount invoice creation in PoS checkout flow
Adds a per-app setting, defaulting to off, so behavior is opt-in
Precondition checks `!isTopup && summary.PriceTaxIncludedWithTips <= 0m` before invoice creation
UI checkbox added in Point of Sale settings page
No mention of CVE, bug bounty, or external report in commit or supplied references
Evidence from the diff
The patch introduces a DisableZeroAmountInvoice boolean setting in PointOfSaleSettings, exposes it through UpdatePointOfSaleViewModel, renders a checkbox in the PoS update UI, and enforces it in UIPointOfSaleController during invoice creation. The guard is: if the PoS is not a top-up, the computed price including tips is <= 0, and the setting is enabled, the controller returns an error (‘Zero amount invoices are disabled’) instead of calling CreateInvoiceCoreRaw. The setting is persisted and round-tripped through the update action. It is a partial, opt-in mitigation rather than a complete removal of zero-amount invoice support across the product.
Changed components
BTCPayServer/Plugins/PointOfSale/Controllers/UIPointOfSaleController.csBTCPayServer/Plugins/PointOfSale/Models/UpdatePointOfSaleViewModel.csBTCPayServer/Services/Apps/PointOfSaleSettings.csBTCPayServer/Views/Shared/PointOfSale/UpdatePointOfSale.cshtmlInspect captured patch +13 / −0
diff --git a/BTCPayServer/Plugins/PointOfSale/Controllers/UIPointOfSaleController.cs b/BTCPayServer/Plugins/PointOfSale/Controllers/UIPointOfSaleController.cs
index d50d324..60b8af1 100644
--- a/BTCPayServer/Plugins/PointOfSale/Controllers/UIPointOfSaleController.cs
+++ b/BTCPayServer/Plugins/PointOfSale/Controllers/UIPointOfSaleController.cs
@@ -330,6 +330,9 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
selectedChoices.Any(c => c.PriceType == AppItemPriceType.Topup);
var receiptData = PosReceiptData.Create(isTopup, selectedChoices, jposData, order, summary, settings.Currency, _displayFormatter);
+ if (!isTopup && summary.PriceTaxIncludedWithTips <= 0m && settings.DisableZeroAmountInvoice is true)
+ return Error(StringLocalizer["Zero amount invoices are disabled"].Value);
+
try
{
var invoice = await _invoiceController.CreateInvoiceCoreRaw(new CreateInvoiceRequest
@@ -566,6 +569,7 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
Description = settings.Description,
NotificationUrl = settings.NotificationUrl,
RedirectUrl = settings.RedirectUrl,
+ DisableZeroAmountInvoice = settings.DisableZeroAmountInvoice is true,
SearchTerm = app.TagAllInvoices ? $"storeid:{app.StoreDataId}" : $"appid:{app.Id}",
RedirectAutomatically = settings.RedirectAutomatically.HasValue ? settings.RedirectAutomatically.Value ? "true" : "false" : "",
FormId = settings.FormId
@@ -661,6 +665,7 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
HtmlLang = vm.HtmlLang,
HtmlMetaTags = _safe.RawMeta(vm.HtmlMetaTags, out bool wasHtmlModified),
Description = vm.Description,
+ DisableZeroAmountInvoice = vm.DisableZeroAmountInvoice,
RedirectAutomatically = string.IsNullOrEmpty(vm.RedirectAutomatically) ? null : bool.Parse(vm.RedirectAutomatically),
FormId = vm.FormId
};
diff --git a/BTCPayServer/Plugins/PointOfSale/Models/UpdatePointOfSaleViewModel.cs b/BTCPayServer/Plugins/PointOfSale/Models/UpdatePointOfSaleViewModel.cs
index 04d60c9..eda6c49 100644
--- a/BTCPayServer/Plugins/PointOfSale/Models/UpdatePointOfSaleViewModel.cs
+++ b/BTCPayServer/Plugins/PointOfSale/Models/UpdatePointOfSaleViewModel.cs
@@ -101,5 +101,8 @@ namespace BTCPayServer.Plugins.PointOfSale.Models
public string FormId { get; set; }
public bool Archived { get; set; }
+
+ [Display(Name = "Disable zero amount invoices")]
+ public bool DisableZeroAmountInvoice { get; set; }
}
}
diff --git a/BTCPayServer/Services/Apps/PointOfSaleSettings.cs b/BTCPayServer/Services/Apps/PointOfSaleSettings.cs
index 5862985..ebe8264 100644
--- a/BTCPayServer/Services/Apps/PointOfSaleSettings.cs
+++ b/BTCPayServer/Services/Apps/PointOfSaleSettings.cs
@@ -108,5 +108,6 @@ namespace BTCPayServer.Services.Apps
public string NotificationUrl { get; set; }
public string RedirectUrl { get; set; }
public bool? RedirectAutomatically { get; set; }
+ public bool? DisableZeroAmountInvoice { get; set; }
}
}
diff --git a/BTCPayServer/Views/Shared/PointOfSale/UpdatePointOfSale.cshtml b/BTCPayServer/Views/Shared/PointOfSale/UpdatePointOfSale.cshtml
index dd60132..66a744e 100644
--- a/BTCPayServer/Views/Shared/PointOfSale/UpdatePointOfSale.cshtml
+++ b/BTCPayServer/Views/Shared/PointOfSale/UpdatePointOfSale.cshtml
@@ -203,6 +203,10 @@
<span asp-validation-for="CustomButtonText" class="text-danger"></span>
</div>
</div>
+ <div class="form-group d-flex align-items-center">
+ <input asp-for="DisableZeroAmountInvoice" type="checkbox" class="btcpay-toggle me-3"/>
+ <label asp-for="DisableZeroAmountInvoice" class="form-check-label"></label>
+ </div>
</fieldset>
</div>
</div>
Why this scored 32/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.