Do not show Server service links on the Dashboard if insuffient permissions
What changed, and why it matters
This change hides a dashboard widget called 'Lightning Services' from users who do not have permission to change server settings. Previously, the widget may have been visible to users who lacked those rights, potentially exposing links or information about server-level Lightning services. The fix adds a permission check directly to the widget's HTML wrapper so it only renders for authorized users.
Verify that the `permission` tag helper is evaluated server-side before any sensitive URLs or service metadata are emitted, and that no other dashboard widgets expose server-level service links without equivalent authorization checks. Consider adding automated tests for widget visibility across role boundaries.
Security signals we found
UI element rendered without proper authorization check
Information disclosure via dashboard widget visible to lower-privileged users
Permission tag helper added to enforce server-settings policy
Evidence from the diff
The commit modifies the StoreLightningServices view component and its Razor view. The key change is adding permission="@Policies.CanModifyServerSettings" to the outer <div> in Default.cshtml, which causes BTCPay Server’s permission tag helper to suppress the entire widget unless the current user holds the CanModifyServerSettings policy. The C# component is refactored to primary-constructor syntax and removes unused dependencies, but the substantive security change is the view-level permission gating. A second widget, StoreLightningBalance, is refactored similarly but does not appear to receive a new permission gate.
Changed components
BTCPayServer/Components/StoreLightningServices/Default.cshtmlBTCPayServer/Components/StoreLightningServices/StoreLightningServices.csBTCPayServer/Components/StoreLightningServices/StoreLightningServicesViewModel.csBTCPayServer/Components/StoreLightningBalance/StoreLightningBalance.csBTCPayServer/Components/StoreLightningBalance/Default.cshtmlInspect captured patch +35 / −84
diff --git a/BTCPayServer/Components/StoreLightningBalance/Default.cshtml b/BTCPayServer/Components/StoreLightningBalance/Default.cshtml
index 6b6db92..2c295ca 100644
--- a/BTCPayServer/Components/StoreLightningBalance/Default.cshtml
+++ b/BTCPayServer/Components/StoreLightningBalance/Default.cshtml
@@ -1,7 +1,4 @@
-@using BTCPayServer.Abstractions.TagHelpers
@using BTCPayServer.Client.Models
-@using BTCPayServer.TagHelpers
-@using Microsoft.AspNetCore.Mvc.TagHelpers
@model BTCPayServer.Components.StoreLightningBalance.StoreLightningBalanceViewModel
@if (!Model.InitialRendering && Model.Balance == null)
{
diff --git a/BTCPayServer/Components/StoreLightningBalance/StoreLightningBalance.cs b/BTCPayServer/Components/StoreLightningBalance/StoreLightningBalance.cs
index f1d1c31..b9f2d4d 100644
--- a/BTCPayServer/Components/StoreLightningBalance/StoreLightningBalance.cs
+++ b/BTCPayServer/Components/StoreLightningBalance/StoreLightningBalance.cs
@@ -12,7 +12,6 @@ using BTCPayServer.Security;
using BTCPayServer.Services;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Rates;
-using BTCPayServer.Services.Stores;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
@@ -20,45 +19,18 @@ using StoreData = BTCPayServer.Data.StoreData;
namespace BTCPayServer.Components.StoreLightningBalance;
-public class StoreLightningBalance : ViewComponent
+public class StoreLightningBalance(
+ CurrencyNameTable currencies,
+ BTCPayNetworkProvider networkProvider,
+ LightningClientFactoryService lightningClientFactory,
+ IOptions<LightningNetworkOptions> lightningNetworkOptions,
+ IAuthorizationService authorizationService,
+ PaymentMethodHandlerDictionary handlers,
+ LightningHistogramService lnHistogramService)
+ : ViewComponent
{
private const HistogramType DefaultType = HistogramType.Week;
- private readonly StoreRepository _storeRepo;
- private readonly CurrencyNameTable _currencies;
- private readonly BTCPayServerOptions _btcpayServerOptions;
- private readonly BTCPayNetworkProvider _networkProvider;
- private readonly LightningClientFactoryService _lightningClientFactory;
- private readonly IOptions<LightningNetworkOptions> _lightningNetworkOptions;
- private readonly IOptions<ExternalServicesOptions> _externalServiceOptions;
- private readonly IAuthorizationService _authorizationService;
- private readonly PaymentMethodHandlerDictionary _handlers;
- private readonly LightningHistogramService _lnHistogramService;
-
- public StoreLightningBalance(
- StoreRepository storeRepo,
- CurrencyNameTable currencies,
- BTCPayNetworkProvider networkProvider,
- BTCPayServerOptions btcpayServerOptions,
- LightningClientFactoryService lightningClientFactory,
- IOptions<LightningNetworkOptions> lightningNetworkOptions,
- IOptions<ExternalServicesOptions> externalServiceOptions,
- IAuthorizationService authorizationService,
- PaymentMethodHandlerDictionary handlers,
- LightningHistogramService lnHistogramService)
- {
- _storeRepo = storeRepo;
- _currencies = currencies;
- _networkProvider = networkProvider;
- _btcpayServerOptions = btcpayServerOptions;
- _externalServiceOptions = externalServiceOptions;
- _authorizationService = authorizationService;
- _handlers = handlers;
- _lightningClientFactory = lightningClientFactory;
- _lightningNetworkOptions = lightningNetworkOptions;
- _lnHistogramService = lnHistogramService;
- }
-
public async Task<IViewComponentResult> InvokeAsync(StoreData store, string cryptoCode, bool initialRendering)
{
var defaultCurrency = store.GetStoreBlob().DefaultCurrency;
@@ -68,17 +40,17 @@ public class StoreLightningBalance : ViewComponent
CryptoCode = cryptoCode,
InitialRendering = initialRendering,
DefaultCurrency = defaultCurrency,
- CurrencyData = _currencies.GetCurrencyData(defaultCurrency, true),
+ CurrencyData = currencies.GetCurrencyData(defaultCurrency, true),
DataUrl = Url.Action("LightningBalanceDashboard", "UIStores", new { storeId = store.Id, cryptoCode })
};
if (vm.InitialRendering)
return View(vm);
-
+
try
{
var lightningClient = await GetLightningClient(store, vm.CryptoCode);
-
+
// balance
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
var balance = await lightningClient.GetBalance(cts.Token);
@@ -91,9 +63,9 @@ public class StoreLightningBalance : ViewComponent
? (balance.OffchainBalance.Opening ?? 0) + (balance.OffchainBalance.Local ?? 0) +
(balance.OffchainBalance.Closing ?? 0)
: null;
-
+
// histogram
- var data = await _lnHistogramService.GetHistogram(lightningClient, DefaultType, cts.Token);
+ var data = await lnHistogramService.GetHistogram(lightningClient, DefaultType, cts.Token);
if (data != null)
{
vm.Type = data.Type;
@@ -116,19 +88,19 @@ public class StoreLightningBalance : ViewComponent
private async Task<ILightningClient> GetLightningClient(StoreData store, string cryptoCode)
{
- var network = _networkProvider.GetNetwork<BTCPayNetwork>(cryptoCode);
+ var network = networkProvider.GetNetwork<BTCPayNetwork>(cryptoCode);
var id = PaymentTypes.LN.GetPaymentMethodId(cryptoCode);
- var existing = store.GetPaymentMethodConfig<LightningPaymentMethodConfig>(id, _handlers);
+ var existing = store.GetPaymentMethodConfig<LightningPaymentMethodConfig>(id, handlers);
if (existing == null)
return null;
if (existing.GetExternalLightningUrl() is { } connectionString)
{
- return _lightningClientFactory.Create(connectionString, network);
+ return lightningClientFactory.Create(connectionString, network);
}
- if (existing.IsInternalNode && _lightningNetworkOptions.Value.InternalLightningByCryptoCode.TryGetValue(cryptoCode, out var internalLightningNode))
+ if (existing.IsInternalNode && lightningNetworkOptions.Value.InternalLightningByCryptoCode.TryGetValue(cryptoCode, out var internalLightningNode))
{
- var result = await _authorizationService.AuthorizeAsync(HttpContext.User, null,
+ var result = await authorizationService.AuthorizeAsync(HttpContext.User, null,
new PolicyRequirement(Policies.CanUseInternalLightningNode));
return result.Succeeded ? internalLightningNode : null;
}
diff --git a/BTCPayServer/Components/StoreLightningServices/Default.cshtml b/BTCPayServer/Components/StoreLightningServices/Default.cshtml
index 7cc0755..0ed2e45 100644
--- a/BTCPayServer/Components/StoreLightningServices/Default.cshtml
+++ b/BTCPayServer/Components/StoreLightningServices/Default.cshtml
@@ -1,13 +1,14 @@
+@using BTCPayServer.Client
@model BTCPayServer.Components.StoreLightningServices.StoreLightningServicesViewModel
@if (Model.Services != null && Model.Services.Any())
{
- <div id="StoreLightningServices-@Model.StoreId" class="widget store-lightning-services">
+ <div permission="@Policies.CanModifyServerSettings" id="StoreLightningServices-@Model.StoreId" class="widget store-lightning-services">
<header class="mb-4">
<h6 text-translate="true">Lightning Services</h6>
<a
asp-controller="UIPublicLightningNodeInfo"
- asp-action="ShowLightningNodeInfo"app-top-items
+ asp-action="ShowLightningNodeInfo"
asp-route-cryptoCode="@Model.CryptoCode"
asp-route-storeId="@Model.StoreId"
target="_blank"
diff --git a/BTCPayServer/Components/StoreLightningServices/StoreLightningServices.cs b/BTCPayServer/Components/StoreLightningServices/StoreLightningServices.cs
index b324766..830518b 100644
--- a/BTCPayServer/Components/StoreLightningServices/StoreLightningServices.cs
+++ b/BTCPayServer/Components/StoreLightningServices/StoreLightningServices.cs
@@ -17,39 +17,22 @@ using Microsoft.Extensions.Options;
namespace BTCPayServer.Components.StoreLightningServices;
-public class StoreLightningServices : ViewComponent
+public class StoreLightningServices(
+ BTCPayServerOptions btcpayServerOptions,
+ IAuthorizationService authorizationService,
+ PaymentMethodHandlerDictionary handlers,
+ IOptions<LightningNetworkOptions> lightningNetworkOptions,
+ IOptions<ExternalServicesOptions> externalServiceOptions)
+ : ViewComponent
{
- private readonly BTCPayServerOptions _btcpayServerOptions;
- private readonly BTCPayNetworkProvider _networkProvider;
- private readonly IAuthorizationService _authorizationService;
- private readonly PaymentMethodHandlerDictionary _handlers;
- private readonly IOptions<LightningNetworkOptions> _lightningNetworkOptions;
- private readonly IOptions<ExternalServicesOptions> _externalServiceOptions;
-
- public StoreLightningServices(
- BTCPayNetworkProvider networkProvider,
- BTCPayServerOptions btcpayServerOptions,
- IAuthorizationService authorizationService,
- PaymentMethodHandlerDictionary handlers,
- IOptions<LightningNetworkOptions> lightningNetworkOptions,
- IOptions<ExternalServicesOptions> externalServiceOptions)
- {
- _networkProvider = networkProvider;
- _btcpayServerOptions = btcpayServerOptions;
- _lightningNetworkOptions = lightningNetworkOptions;
- _externalServiceOptions = externalServiceOptions;
- _authorizationService = authorizationService;
- _handlers = handlers;
- }
-
public async Task<IViewComponentResult> InvokeAsync(StoreData store, string cryptoCode)
{
var vm = new StoreLightningServicesViewModel { StoreId = store.Id, CryptoCode = cryptoCode };
var id = PaymentTypes.LN.GetPaymentMethodId(cryptoCode);
- var existing = store.GetPaymentMethodConfig<LightningPaymentMethodConfig>(id, _handlers);
- if (existing?.IsInternalNode is true && _lightningNetworkOptions.Value.InternalLightningByCryptoCode.TryGetValue(cryptoCode, out _))
+ var existing = store.GetPaymentMethodConfig<LightningPaymentMethodConfig>(id, handlers);
+ if (existing?.IsInternalNode is true && lightningNetworkOptions.Value.InternalLightningByCryptoCode.TryGetValue(cryptoCode, out _))
{
- var result = await _authorizationService.AuthorizeAsync(HttpContext.User, null, new PolicyRequirement(Policies.CanUseInternalLightningNode));
+ var result = await authorizationService.AuthorizeAsync(HttpContext.User, null, new PolicyRequirement(Policies.CanUseInternalLightningNode));
vm.LightningNodeType = result.Succeeded ? LightningNodeType.Internal : null;
}
@@ -58,7 +41,7 @@ public class StoreLightningServices : ViewComponent
if (!User.IsInRole(Roles.ServerAdmin))
return View(vm);
- var services = _externalServiceOptions.Value.ExternalServices.ToList()
+ var services = externalServiceOptions.Value.ExternalServices.ToList()
.Where(service => ExternalServices.LightningServiceTypes.Contains(service.Type))
.Select(async service =>
{
@@ -71,7 +54,7 @@ public class StoreLightningServices : ViewComponent
};
try
{
- model.Link = await service.GetLink(Request.GetAbsoluteUriNoPathBase(), _btcpayServerOptions.NetworkType);
+ model.Link = await service.GetLink(Request.GetAbsoluteUriNoPathBase(), btcpayServerOptions.NetworkType);
}
catch (Exception exception)
{
@@ -83,7 +66,7 @@ public class StoreLightningServices : ViewComponent
.ToList();
// other services
- foreach ((string key, Uri value) in _externalServiceOptions.Value.OtherExternalServices)
+ foreach (var (key, value) in externalServiceOptions.Value.OtherExternalServices)
{
if (ExternalServices.LightningServiceNames.Contains(key))
{
diff --git a/BTCPayServer/Components/StoreLightningServices/StoreLightningServicesViewModel.cs b/BTCPayServer/Components/StoreLightningServices/StoreLightningServicesViewModel.cs
index 33dd368..be05b69 100644
--- a/BTCPayServer/Components/StoreLightningServices/StoreLightningServicesViewModel.cs
+++ b/BTCPayServer/Components/StoreLightningServices/StoreLightningServicesViewModel.cs
@@ -1,6 +1,4 @@
using System.Collections.Generic;
-using BTCPayServer.Data;
-using BTCPayServer.Lightning;
using BTCPayServer.Models;
using BTCPayServer.Models.StoreViewModels;
Why this scored 34/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.