HttpContext.GetStoreData method should be nullable
What changed, and why it matters
This commit is a code-quality and null-safety refactor. It makes the helper that retrieves the current store from an HTTP request return null when no store is set, adds a separate throw-on-null variant, and updates many controllers to use the appropriate variant. It also switches invoice lookups to use a context item set by an authorization filter. The changes reduce the chance of accidental null-reference crashes and make authorization checks more centralized, but the commit itself does not describe a specific security bug being fixed.
Treat this as a routine refactor with potential security-hardening side effects. Review that SetContextFilter correctly validates store-invoice ownership for all routes and that switching invoice lookups to the context item does not bypass repository-level checks. Verify that GetStoreDataOrThrow() is only used on routes where authorization guarantees a store is present. No immediate patching urgency is indicated by the commit itself.
Security signals we found
Centralized authorization context: moving invoice/store ownership checks into a filter can reduce the risk of controllers forgetting to validate ownership.
Nullable annotations added to HttpContext extension methods to prevent null dereferences.
SetContextFilter now fetches invoices including archived ones, which may affect authorization decisions on archived invoices.
Several call sites switched from nullable GetStoreData() to throwing GetStoreDataOrThrow(), which could change failure modes from silent null to exceptions if authorization is misconfigured.
Evidence from the diff
The diff changes HttpContext.GetStoreData() to return nullable StoreData? and introduces GetStoreDataOrThrow() for call sites that assume a store exists after authorization. It also makes GetInvoiceData() nullable and updates Greenfield invoice endpoints to read the invoice from HttpContext (populated by SetContextFilter) rather than querying the repository directly and checking store ownership in the controller. Several controllers are updated to use GetStoreDataOrThrow(), and SetContextFilter now loads invoices with includeArchived: true. The change is primarily a refactor toward nullable reference types and centralized context data; no explicit vulnerability or CVE is mentioned.
Changed components
BTCPayServer/Extensions.csBTCPayServer/Security/SetContextFilter.csBTCPayServer/Controllers/GreenField/GreenfieldInvoiceController.csBTCPayServer/Controllers/UIInvoiceController.UI.csBTCPayServer/Controllers/UIStoresController.csBTCPayServer/Controllers/UIWalletsController.csBTCPayServer/Controllers/GreenField/GreenfieldStorePaymentMethodsController.csBTCPayServer/Controllers/GreenField/GreenfieldStoreRatesController.csBTCPayServer/Controllers/GreenField/GreenfieldStoreRatesConfigurationController.csBTCPayServer/Controllers/GreenField/GreenfieldStoreEmailController.csBTCPayServer/Controllers/GreenField/GreenfieldStoreOnChainWalletsController.csBTCPayServer/Plugins/PayButton/Controllers/UIPayButtonController.csBTCPayServer/Plugins/Subscriptions/Controllers/UIOfferingController.csBTCPayServer/Plugins/Webhooks/Controllers/UIStoreWebhooksController.csBTCPayServer/Components/WalletNav/WalletNav.csBTCPayServer/Services/Stores/ScopeProvider.csBTCPayServer/Security/BuiltInPermissionScopeProvider.csInspect captured patch +106 / −157
diff --git a/BTCPayServer/Components/WalletNav/WalletNav.cs b/BTCPayServer/Components/WalletNav/WalletNav.cs
index 12ae50d..acce5bc 100644
--- a/BTCPayServer/Components/WalletNav/WalletNav.cs
+++ b/BTCPayServer/Components/WalletNav/WalletNav.cs
@@ -45,7 +45,7 @@ namespace BTCPayServer.Components.WalletNav
{
var store = ViewContext.HttpContext.GetStoreData();
var network = _handlers.TryGetNetwork(PaymentTypes.CHAIN.GetPaymentMethodId(walletId.CryptoCode));
- if (network is null)
+ if (network is null || store is null)
return new HtmlContentViewComponentResult(new StringHtmlContent(string.Empty));
var wallet = _walletProvider.GetWallet(network);
var defaultCurrency = store.GetStoreBlob().DefaultCurrency;
diff --git a/BTCPayServer/Controllers/GreenField/GreenfieldApiKeysController.cs b/BTCPayServer/Controllers/GreenField/GreenfieldApiKeysController.cs
index c9f76bb..1773c59 100644
--- a/BTCPayServer/Controllers/GreenField/GreenfieldApiKeysController.cs
+++ b/BTCPayServer/Controllers/GreenField/GreenfieldApiKeysController.cs
@@ -18,17 +18,8 @@ namespace BTCPayServer.Controllers.Greenfield
[ApiController]
[Authorize(AuthenticationSchemes = AuthenticationSchemes.GreenfieldAPIKeys)]
[EnableCors(CorsPolicies.All)]
- public class GreenfieldApiKeysController : ControllerBase
+ public class GreenfieldApiKeysController(APIKeyRepository apiKeyRepository, UserManager<ApplicationUser> userManager) : ControllerBase
{
- private readonly APIKeyRepository _apiKeyRepository;
- private readonly UserManager<ApplicationUser> _userManager;
-
- public GreenfieldApiKeysController(APIKeyRepository apiKeyRepository, UserManager<ApplicationUser> userManager)
- {
- _apiKeyRepository = apiKeyRepository;
- _userManager = userManager;
- }
-
[HttpGet("~/api/v1/api-keys/current")]
public async Task<IActionResult> GetKey()
{
@@ -37,7 +28,7 @@ namespace BTCPayServer.Controllers.Greenfield
return
this.CreateAPIError(404, "api-key-not-found", "The api key was not present.");
}
- var data = await _apiKeyRepository.GetKey(apiKey);
+ var data = await apiKeyRepository.GetKey(apiKey);
return Ok(FromModel(data));
}
@@ -53,7 +44,7 @@ namespace BTCPayServer.Controllers.Greenfield
request ??= new CreateApiKeyRequest();
request.Permissions ??= System.Array.Empty<Permission>();
- var userId = (await _userManager.FindByIdOrEmail(idOrEmail))?.Id;
+ var userId = (await userManager.FindByIdOrEmail(idOrEmail))?.Id;
if (userId is null)
return this.UserNotFound();
var key = new APIKeyData()
@@ -67,7 +58,7 @@ namespace BTCPayServer.Controllers.Greenfield
{
Permissions = request.Permissions.Select(p => p.ToString()).Distinct().ToArray()
});
- await _apiKeyRepository.CreateKey(key);
+ await apiKeyRepository.CreateKey(key);
return Ok(FromModel(key));
}
@@ -91,11 +82,11 @@ namespace BTCPayServer.Controllers.Greenfield
[Authorize(Policy = Policies.CanManageUsers, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
public async Task<IActionResult> RevokeAPIKey(string idOrEmail, string apikey)
{
- var userId = (await _userManager.FindByIdOrEmail(idOrEmail))?.Id;
+ var userId = (await userManager.FindByIdOrEmail(idOrEmail))?.Id;
if (userId is null)
return this.UserNotFound();
if (!string.IsNullOrEmpty(apikey) &&
- await _apiKeyRepository.Remove(apikey, userId))
+ await apiKeyRepository.Remove(apikey, userId))
return Ok();
else
return this.CreateAPIError("apikey-not-found", "This apikey does not exists");
diff --git a/BTCPayServer/Controllers/GreenField/GreenfieldAppsController.cs b/BTCPayServer/Controllers/GreenField/GreenfieldAppsController.cs
index eab6f94..4a05f46 100644
--- a/BTCPayServer/Controllers/GreenField/GreenfieldAppsController.cs
+++ b/BTCPayServer/Controllers/GreenField/GreenfieldAppsController.cs
@@ -19,9 +19,7 @@ using BTCPayServer.Services.Stores;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Http;
-using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
-using Newtonsoft.Json;
using CrowdfundResetEvery = BTCPayServer.Client.Models.CrowdfundResetEvery;
using PosViewType = BTCPayServer.Client.Models.PosViewType;
@@ -117,7 +115,7 @@ namespace BTCPayServer.Controllers.Greenfield
Archived = request.Archived ?? false
};
- var settings = ToPointOfSaleSettings(request, new PointOfSaleSettings { Title = request.Title ?? request.AppName });
+ var settings = ToPointOfSaleSettings(request);
appData.SetSettings(settings);
await _appService.UpdateOrCreateApp(appData);
@@ -158,7 +156,7 @@ namespace BTCPayServer.Controllers.Greenfield
{
app.Archived = request.Archived.Value;
}
- app.SetSettings(ToPointOfSaleSettings(request, settings));
+ app.SetSettings(ToPointOfSaleSettings(request));
await _appService.UpdateOrCreateApp(app);
@@ -343,7 +341,7 @@ namespace BTCPayServer.Controllers.Greenfield
};
}
- private PointOfSaleSettings ToPointOfSaleSettings(PointOfSaleAppRequest request, PointOfSaleSettings settings)
+ private PointOfSaleSettings ToPointOfSaleSettings(PointOfSaleAppRequest request)
{
Enum.TryParse<BTCPayServer.Plugins.PointOfSale.PosViewType>(request.DefaultView.ToString(), true, out var defaultView);
if (request.HtmlMetaTags is not null)
diff --git a/BTCPayServer/Controllers/GreenField/GreenfieldFilesController.cs b/BTCPayServer/Controllers/GreenField/GreenfieldFilesController.cs
index daeebf1..243729f 100644
--- a/BTCPayServer/Controllers/GreenField/GreenfieldFilesController.cs
+++ b/BTCPayServer/Controllers/GreenField/GreenfieldFilesController.cs
@@ -6,12 +6,10 @@ using BTCPayServer.Abstractions.Contracts;
using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Client;
using BTCPayServer.Client.Models;
-using BTCPayServer.Data;
using BTCPayServer.Storage.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Http;
-using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
namespace BTCPayServer.Controllers.Greenfield;
diff --git a/BTCPayServer/Controllers/GreenField/GreenfieldHealthController.cs b/BTCPayServer/Controllers/GreenField/GreenfieldHealthController.cs
index 49f527b..4bcf7ae 100644
--- a/BTCPayServer/Controllers/GreenField/GreenfieldHealthController.cs
+++ b/BTCPayServer/Controllers/GreenField/GreenfieldHealthController.cs
@@ -8,23 +8,14 @@ namespace BTCPayServer.Controllers.Greenfield
{
[Controller]
[EnableCors(CorsPolicies.All)]
- public class GreenfieldHealthController : ControllerBase
+ public class GreenfieldHealthController(NBXplorerDashboard dashBoard) : ControllerBase
{
- private readonly NBXplorerDashboard _dashBoard;
-
- public GreenfieldHealthController(NBXplorerDashboard dashBoard)
- {
- _dashBoard = dashBoard;
- }
[AllowAnonymous]
[HttpGet("~/api/v1/health")]
public ActionResult GetHealth()
+ => Ok( new ApiHealthData()
{
- ApiHealthData model = new ApiHealthData()
- {
- Synchronized = _dashBoard.IsFullySynched()
- };
- return Ok(model);
- }
+ Synchronized = dashBoard.IsFullySynched()
+ });
}
}
diff --git a/BTCPayServer/Controllers/GreenField/GreenfieldInvoiceController.cs b/BTCPayServer/Controllers/GreenField/GreenfieldInvoiceController.cs
index 9a54d02..fd6ba8b 100644
--- a/BTCPayServer/Controllers/GreenField/GreenfieldInvoiceController.cs
+++ b/BTCPayServer/Controllers/GreenField/GreenfieldInvoiceController.cs
@@ -11,7 +11,6 @@ using BTCPayServer.Client;
using BTCPayServer.Client.Models;
using BTCPayServer.Data;
using BTCPayServer.HostedServices;
-using BTCPayServer.Models.InvoicingModels;
using BTCPayServer.Payments;
using BTCPayServer.Payouts;
using BTCPayServer.Rating;
@@ -132,10 +131,9 @@ namespace BTCPayServer.Controllers.Greenfield
[HttpGet("~/api/v1/stores/{storeId}/invoices/{invoiceId}")]
public async Task<IActionResult> GetInvoice(string storeId, string invoiceId)
{
- var invoice = await _invoiceRepository.GetInvoice(invoiceId, true);
- if (!BelongsToThisStore(invoice))
+ var invoice = HttpContext.GetInvoiceData();
+ if (invoice is null)
return InvoiceNotFound();
-
return Ok(ToModel(invoice));
}
@@ -144,8 +142,8 @@ namespace BTCPayServer.Controllers.Greenfield
[HttpDelete("~/api/v1/stores/{storeId}/invoices/{invoiceId}")]
public async Task<IActionResult> ArchiveInvoice(string storeId, string invoiceId)
{
- var invoice = await _invoiceRepository.GetInvoice(invoiceId, true);
- if (!BelongsToThisStore(invoice))
+ var invoice = HttpContext.GetInvoiceData();
+ if (invoice is null)
return InvoiceNotFound();
await _invoiceRepository.ToggleInvoiceArchival(invoiceId, true, storeId);
return Ok();
@@ -156,10 +154,10 @@ namespace BTCPayServer.Controllers.Greenfield
[HttpPut("~/api/v1/stores/{storeId}/invoices/{invoiceId}")]
public async Task<IActionResult> UpdateInvoice(string storeId, string invoiceId, UpdateInvoiceRequest request)
{
- var result = await _invoiceRepository.UpdateInvoiceMetadata(invoiceId, storeId, request.Metadata);
- if (!BelongsToThisStore(result))
+ var invoice = HttpContext.GetInvoiceData();
+ if (invoice is null)
return InvoiceNotFound();
- return Ok(ToModel(result));
+ return Ok(ToModel(invoice));
}
[Authorize(Policy = Policies.CanCreateInvoice,
@@ -241,8 +239,8 @@ namespace BTCPayServer.Controllers.Greenfield
public async Task<IActionResult> MarkInvoiceStatus(string storeId, string invoiceId,
MarkInvoiceStatusRequest request)
{
- var invoice = await _invoiceRepository.GetInvoice(invoiceId, true);
- if (!BelongsToThisStore(invoice))
+ var invoice = HttpContext.GetInvoiceData();
+ if (invoice is null)
return InvoiceNotFound();
if (!await _invoiceRepository.MarkInvoiceStatus(invoice.Id, request.Status))
@@ -262,8 +260,8 @@ namespace BTCPayServer.Controllers.Greenfield
[HttpPost("~/api/v1/stores/{storeId}/invoices/{invoiceId}/unarchive")]
public async Task<IActionResult> UnarchiveInvoice(string storeId, string invoiceId)
{
- var invoice = await _invoiceRepository.GetInvoice(invoiceId, true);
- if (!BelongsToThisStore(invoice))
+ var invoice = HttpContext.GetInvoiceData();
+ if (invoice is null)
return InvoiceNotFound();
if (!invoice.Archived)
@@ -284,8 +282,8 @@ namespace BTCPayServer.Controllers.Greenfield
[HttpGet("~/api/v1/stores/{storeId}/invoices/{invoiceId}/payment-methods")]
public async Task<IActionResult> GetInvoicePaymentMethods(string storeId, string invoiceId, bool onlyAccountedPayments = true, bool includeSensitive = false)
{
- var invoice = await _invoiceRepository.GetInvoice(invoiceId, true);
- if (!BelongsToThisStore(invoice))
+ var invoice = HttpContext.GetInvoiceData();
+ if (invoice is null)
return InvoiceNotFound();
if (includeSensitive && !await _authorizationService.CanModifyStore(User))
@@ -294,22 +292,13 @@ namespace BTCPayServer.Controllers.Greenfield
return Ok(ToPaymentMethodModels(invoice, onlyAccountedPayments, includeSensitive));
}
- bool BelongsToThisStore([NotNullWhen(true)] InvoiceEntity invoice) => BelongsToThisStore(invoice, out _);
- private bool BelongsToThisStore([NotNullWhen(true)] InvoiceEntity invoice, [MaybeNullWhen(false)] out Data.StoreData store)
- {
- store = this.HttpContext.GetStoreData();
- return invoice?.StoreId is not null && store.Id == invoice.StoreId;
- }
-
[Authorize(Policy = Policies.CanViewInvoices,
AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpPost("~/api/v1/stores/{storeId}/invoices/{invoiceId}/payment-methods/{paymentMethod}/activate")]
public async Task<IActionResult> ActivateInvoicePaymentMethod(string storeId, string invoiceId, string paymentMethod)
{
- var invoice = await _invoiceRepository.GetInvoice(invoiceId, true);
- if (!BelongsToThisStore(invoice))
+ if (HttpContext.GetInvoiceData() is null)
return InvoiceNotFound();
-
if (PaymentMethodId.TryParse(paymentMethod, out var paymentMethodId))
{
await _invoiceActivator.ActivateInvoicePaymentMethod(invoiceId, paymentMethodId);
@@ -329,13 +318,13 @@ namespace BTCPayServer.Controllers.Greenfield
CancellationToken cancellationToken = default
)
{
- var invoice = await _invoiceRepository.GetInvoice(invoiceId, true);
- if (!BelongsToThisStore(invoice, out var store))
+ var invoice = HttpContext.GetInvoiceData();
+ var store = HttpContext.GetStoreData();
+ if (invoice is null || store is null)
return InvoiceNotFound();
if (!invoice.GetInvoiceState().CanRefund())
- {
return this.CreateAPIError("non-refundable", "Cannot refund this invoice");
- }
+
PaymentPrompt? paymentPrompt = null;
PayoutMethodId? payoutMethodId = null;
if (request.PayoutMethodId is null)
@@ -472,7 +461,7 @@ namespace BTCPayServer.Controllers.Greenfield
return this.CreateValidationError(ModelState);
}
- // reduce by percentage
+ // reduce it by percentage
if (request.SubtractPercentage is > 0 and <= 100)
{
var reduceByAmount = createPullPayment.Amount * (request.SubtractPercentage / 100);
@@ -500,8 +489,8 @@ namespace BTCPayServer.Controllers.Greenfield
[HttpGet("~/api/v1/stores/{storeId}/invoices/{invoiceId}/refund/{paymentMethodId}")]
public async Task<IActionResult> GetInvoiceRefundTriggerData(string storeId, string invoiceId, string paymentMethodId, CancellationToken cancellationToken)
{
- var invoice = await _invoiceRepository.GetInvoice(invoiceId, true);
- if (!BelongsToThisStore(invoice))
+ var invoice = HttpContext.GetInvoiceData();
+ if (invoice is null)
return InvoiceNotFound();
var pmi = PaymentMethodId.TryParse(paymentMethodId);
if (pmi == null)
@@ -529,7 +518,7 @@ namespace BTCPayServer.Controllers.Greenfield
var cdCurrency = _currencyNameTable.GetCurrencyData(invoice.Currency, true);
var paidAmount = Math.Round(cryptoPaid * paymentPrompt.Rate, cdCurrency.Divisibility);
- var store = this.HttpContext.GetStoreData();
+ var store = this.HttpContext.GetStoreDataOrThrow();
var rules = store.GetStoreBlob().GetRateRules(_defaultRules);
var rateResult = await _rateProvider.FetchRate(
new CurrencyPair(paymentMethodCurrency, invoice.Currency), rules, new StoreIdRateContext(store.Id),
diff --git a/BTCPayServer/Controllers/GreenField/GreenfieldStoreEmailController.cs b/BTCPayServer/Controllers/GreenField/GreenfieldStoreEmailController.cs
index c8ab5c9..41e8143 100644
--- a/BTCPayServer/Controllers/GreenField/GreenfieldStoreEmailController.cs
+++ b/BTCPayServer/Controllers/GreenField/GreenfieldStoreEmailController.cs
@@ -76,7 +76,7 @@ namespace BTCPayServer.Controllers.GreenField
if (!ModelState.IsValid)
return this.CreateValidationError(ModelState);
- var store = HttpContext.GetStoreData();
+ var store = HttpContext.GetStoreDataOrThrow();
var blob = store.GetStoreBlob();
var settings = EmailSettings.FromData(request, blob.EmailSettings?.Password);
blob.EmailSettings = settings;
diff --git a/BTCPayServer/Controllers/GreenField/GreenfieldStoreOnChainWalletsController.cs b/BTCPayServer/Controllers/GreenField/GreenfieldStoreOnChainWalletsController.cs
index b0bdada..c7c3bff 100644
--- a/BTCPayServer/Controllers/GreenField/GreenfieldStoreOnChainWalletsController.cs
+++ b/BTCPayServer/Controllers/GreenField/GreenfieldStoreOnChainWalletsController.cs
@@ -38,7 +38,7 @@ namespace BTCPayServer.Controllers.Greenfield
[EnableCors(CorsPolicies.All)]
public class GreenfieldStoreOnChainWalletsController : ControllerBase
{
- private StoreData Store => HttpContext.GetStoreData();
+ private StoreData Store => HttpContext.GetStoreDataOrThrow();
public PoliciesSettings PoliciesSettings { get; }
diff --git a/BTCPayServer/Controllers/GreenField/GreenfieldStorePaymentMethodsController.cs b/BTCPayServer/Controllers/GreenField/GreenfieldStorePaymentMethodsController.cs
index 4e2f022..dd6d629 100644
--- a/BTCPayServer/Controllers/GreenField/GreenfieldStorePaymentMethodsController.cs
+++ b/BTCPayServer/Controllers/GreenField/GreenfieldStorePaymentMethodsController.cs
@@ -22,23 +22,13 @@ namespace BTCPayServer.Controllers.Greenfield
[ApiController]
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[EnableCors(CorsPolicies.All)]
- public class GreenfieldStorePaymentMethodsController : ControllerBase
+ public class GreenfieldStorePaymentMethodsController(
+ PaymentMethodHandlerDictionary handlers,
+ StoreRepository storeRepository,
+ IAuthorizationService authorizationService)
+ : ControllerBase
{
- private StoreData Store => HttpContext.GetStoreData();
-
- private readonly PaymentMethodHandlerDictionary _handlers;
- private readonly StoreRepository _storeRepository;
- private readonly IAuthorizationService _authorizationService;
-
- public GreenfieldStorePaymentMethodsController(
- PaymentMethodHandlerDictionary handlers,
- StoreRepository storeRepository,
- IAuthorizationService authorizationService)
- {
- _handlers = handlers;
- _storeRepository = storeRepository;
- _authorizationService = authorizationService;
- }
+ private StoreData Store => HttpContext.GetStoreDataOrThrow();
[Authorize(Policy = Policies.CanViewStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpGet("~/api/v1/stores/{storeId}/payment-methods/{paymentMethodId}")]
@@ -68,7 +58,7 @@ namespace BTCPayServer.Controllers.Greenfield
if (Store.GetPaymentMethodConfig(paymentMethodId) is null)
return Ok();
Store.SetPaymentMethodConfig(paymentMethodId, null);
- await _storeRepository.UpdateStore(Store);
+ await storeRepository.UpdateStore(Store);
return Ok();
}
@@ -91,13 +81,11 @@ namespace BTCPayServer.Controllers.Greenfield
{
try
{
- var ctx = new PaymentMethodConfigValidationContext(_authorizationService, ModelState, config, User, Store.GetPaymentMethodConfig(paymentMethodId));
+ var ctx = new PaymentMethodConfigValidationContext(authorizationService, ModelState, config, User, Store.GetPaymentMethodConfig(paymentMethodId));
await handler.ValidatePaymentMethodConfig(ctx);
config = ctx.Config;
if (ctx.MissingPermission is not null)
- {
return this.CreateAPIPermissionError(ctx.MissingPermission.Permission, ctx.MissingPermission.Message);
- }
if (!ModelState.IsValid)
return this.CreateValidationError(ModelState);
if (ctx.StripUnknownProperties)
@@ -116,13 +104,13 @@ namespace BTCPayServer.Controllers.Greenfield
storeBlob.SetExcluded(paymentMethodId, !enabled);
Store.SetStoreBlob(storeBlob);
}
- await _storeRepository.UpdateStore(Store);
+ await storeRepository.UpdateStore(Store);
return await GetStorePaymentMethod(storeId, paymentMethodId, request?.Config is not null);
}
private IPaymentMethodHandler AssertHasHandler(PaymentMethodId paymentMethodId)
{
- if (!_handlers.TryGetValue(paymentMethodId, out var handler))
+ if (!handlers.TryGetValue(paymentMethodId, out var handler))
throw new JsonHttpException(PaymentMethodNotFound());
return handler;
}
@@ -143,17 +131,17 @@ namespace BTCPayServer.Controllers.Greenfield
if (includeConfig is true)
{
- if (!await _authorizationService.CanModifyStore(User))
+ if (!await authorizationService.CanModifyStore(User))
return this.CreateAPIPermissionError(Policies.CanModifyStoreSettings);
}
- return Ok(Store.GetPaymentMethodConfigs(_handlers, onlyEnabled is true)
+ return Ok(Store.GetPaymentMethodConfigs(handlers, onlyEnabled is true)
.Select(
method => new GenericPaymentMethodData()
{
PaymentMethodId = method.Key.ToString(),
Enabled = !excludedPaymentMethods.Match(method.Key),
- Config = includeConfig is true ? JToken.FromObject(method.Value, _handlers[method.Key].Serializer.ForAPI()) : null
+ Config = includeConfig is true ? JToken.FromObject(method.Value, handlers[method.Key].Serializer.ForAPI()) : null
}).ToArray());
}
}
diff --git a/BTCPayServer/Controllers/GreenField/GreenfieldStoreRatesConfigurationController.cs b/BTCPayServer/Controllers/GreenField/GreenfieldStoreRatesConfigurationController.cs
index f928179..9c06894 100644
--- a/BTCPayServer/Controllers/GreenField/GreenfieldStoreRatesConfigurationController.cs
+++ b/BTCPayServer/Controllers/GreenField/GreenfieldStoreRatesConfigurationController.cs
@@ -53,7 +53,7 @@ namespace BTCPayServer.Controllers.GreenField
[NonAction]
private IActionResult GetStoreRateConfigurationCore(bool? fallback)
{
- var data = HttpContext.GetStoreData();
+ var data = HttpContext.GetStoreDataOrThrow();
var storeBlob = data.GetStoreBlob();
var blob = storeBlob.GetRateSettings(fallback ?? false);
if (blob is null)
@@ -92,10 +92,10 @@ namespace BTCPayServer.Controllers.GreenField
[NonAction]
private async Task<IActionResult> UpdateStoreRateConfigurationCore(StoreRateConfiguration configuration, bool fallback)
{
- var storeData = HttpContext.GetStoreData();
+ var storeData = HttpContext.GetStoreDataOrThrow();
var storeBlob = storeData.GetStoreBlob();
var blob = storeBlob.GetRateSettings(fallback);
-
+
// If fallback rates are not enabled but someone is trying to configure them, enable them automatically
if (blob is null && fallback)
{
@@ -105,7 +105,7 @@ namespace BTCPayServer.Controllers.GreenField
{
return this.CreateAPIError(404, "fallback-disabled", "The fallback rates are disabled");
}
-
+
ValidateAndSanitizeConfiguration(configuration, storeBlob, blob);
if (!ModelState.IsValid)
return this.CreateValidationError(ModelState);
@@ -124,7 +124,7 @@ namespace BTCPayServer.Controllers.GreenField
public async Task<IActionResult> PreviewUpdateStoreRateConfiguration(
StoreRateConfiguration configuration, [FromQuery] string[]? currencyPair)
{
- var data = HttpContext.GetStoreData();
+ var data = HttpContext.GetStoreDataOrThrow();
var storeBlob = data.GetStoreBlob();
// Fallback or not, the preview will be the same
var blob = storeBlob.GetOrCreateRateSettings(true);
diff --git a/BTCPayServer/Controllers/GreenField/GreenfieldStoreRatesController.cs b/BTCPayServer/Controllers/GreenField/GreenfieldStoreRatesController.cs
index 639ce87..923b8d7 100644
--- a/BTCPayServer/Controllers/GreenField/GreenfieldStoreRatesController.cs
+++ b/BTCPayServer/Controllers/GreenField/GreenfieldStoreRatesController.cs
@@ -36,7 +36,7 @@ namespace BTCPayServer.Controllers.GreenField
[Authorize(Policy = Policies.CanViewStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
public async Task<IActionResult> GetStoreRates([FromQuery] string[]? currencyPair)
{
- var data = HttpContext.GetStoreData();
+ var data = HttpContext.GetStoreDataOrThrow();
var blob = data.GetStoreBlob();
var parsedCurrencyPairs = new HashSet<CurrencyPair>();
diff --git a/BTCPayServer/Controllers/UIInvoiceController.UI.cs b/BTCPayServer/Controllers/UIInvoiceController.UI.cs
index 782af19..a0c074b 100644
--- a/BTCPayServer/Controllers/UIInvoiceController.UI.cs
+++ b/BTCPayServer/Controllers/UIInvoiceController.UI.cs
@@ -332,9 +332,9 @@ namespace BTCPayServer.Controllers
{
await using var ctx = _dbContextFactory.CreateContext();
- var invoice = GetCurrentInvoice();
+ var invoice = HttpContext.GetInvoiceData();
- if (!invoice.GetInvoiceState().CanRefund())
+ if (invoice?.GetInvoiceState().CanRefund() is not true)
return NotFound();
var store = GetCurrentStore();
@@ -1174,7 +1174,7 @@ namespace BTCPayServer.Controllers
[Authorize(Policy = Policies.CanCreateInvoice, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
public async Task<IActionResult> CreateInvoice(CreateInvoiceModel model, CancellationToken cancellationToken)
{
- var store = HttpContext.GetStoreData();
+ var store = HttpContext.GetStoreDataOrThrow();
if (!store.AnyPaymentMethodAvailable(_handlers))
{
return NoPaymentMethodResult(store.Id);
@@ -1292,9 +1292,7 @@ namespace BTCPayServer.Controllers
public string? StatusString { get; set; }
}
- private StoreData GetCurrentStore() => HttpContext.GetStoreData();
-
- private InvoiceEntity GetCurrentInvoice() => HttpContext.GetInvoiceData();
+ private StoreData GetCurrentStore() => HttpContext.GetStoreDataOrThrow();
// Let server admin lookup invoices from users, see #6489
private string? GetUserIdForInvoiceQuery() => User.IsInRole(Roles.ServerAdmin) ? null : User.GetIdOrNull();
diff --git a/BTCPayServer/Controllers/UIStoresController.cs b/BTCPayServer/Controllers/UIStoresController.cs
index 1492bc5..c5e8d21 100644
--- a/BTCPayServer/Controllers/UIStoresController.cs
+++ b/BTCPayServer/Controllers/UIStoresController.cs
@@ -1,4 +1,5 @@
#nullable enable
+using System;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Constants;
@@ -148,7 +149,7 @@ public partial class UIStoresController : Controller
return Forbid();
}
- public StoreData CurrentStore => HttpContext.GetStoreData();
+ public StoreData CurrentStore => HttpContext.GetStoreDataOrThrow();
public PaymentMethodOptionViewModel.Format[] GetEnabledPaymentMethodChoices(StoreData storeData)
{
diff --git a/BTCPayServer/Controllers/UIWalletsController.cs b/BTCPayServer/Controllers/UIWalletsController.cs
index 80f945b..67d2af6 100644
--- a/BTCPayServer/Controllers/UIWalletsController.cs
+++ b/BTCPayServer/Controllers/UIWalletsController.cs
@@ -1978,7 +1978,7 @@ namespace BTCPayServer.Controllers
private string? GetUserId() => User.GetIdOrNull();
- private StoreData GetCurrentStore() => HttpContext.GetStoreData();
+ private StoreData GetCurrentStore() => HttpContext.GetStoreDataOrThrow();
}
public class WalletReceiveViewModel
diff --git a/BTCPayServer/Extensions.cs b/BTCPayServer/Extensions.cs
index db7a32c..b4ea00e 100644
--- a/BTCPayServer/Extensions.cs
+++ b/BTCPayServer/Extensions.cs
@@ -788,57 +788,58 @@ namespace BTCPayServer
ctx.SetStoreData(storeData);
return new ActionDisposable(() => { ctx.SetStoreData(old); });
}
-#nullable restore
- public static StoreData GetStoreData(this HttpContext ctx)
+ /// <summary>
+ /// Set after authorization succeed. If your route is authorized, this is guaranted to not be null.
+ /// </summary>
+ /// <param name="ctx"></param>
+ /// <returns></returns>
+ public static StoreData? GetStoreData(this HttpContext ctx)
=> ctx.Items.TryGet("BTCPAY.STOREDATA") as StoreData;
- public static void SetStoreData(this HttpContext ctx, StoreData storeData)
+ /// <summary>
+ /// Set after authorization succeed. If your route is authorized, this is guaranted to not throw.
+ /// </summary>
+ /// <param name="ctx"></param>
+ /// <returns></returns>
+ public static StoreData GetStoreDataOrThrow(this HttpContext ctx)
+ => GetStoreData(ctx) ?? throw new InvalidOperationException("StoreData is not set");
+ public static void SetStoreData(this HttpContext ctx, StoreData? storeData)
=> ctx.Items["BTCPAY.STOREDATA"] = storeData;
- public static string GetCurrentStoreId(this HttpContext ctx)
+ public static string? GetCurrentStoreId(this HttpContext ctx)
=> GetStoreData(ctx)?.Id;
public static StoreData[] GetStoresData(this HttpContext ctx)
- => ctx.Items.TryGet("BTCPAY.STORESDATA") as StoreData[];
- public static void SetStoresData(this HttpContext ctx, StoreData[] storeData)
+ => ctx.Items.TryGet("BTCPAY.STORESDATA") as StoreData[] ?? Array.Empty<StoreData>();
+ public static void SetStoresData(this HttpContext ctx, StoreData[]? storeData)
=> ctx.Items["BTCPAY.STORESDATA"] = storeData;
+ /// <summary>
+ /// Set after authorization succeed if invoiceId is present in the route. If not null, the invoice is guaranteed to be from the current store (GetStoreData).
+ /// </summary>
+ /// <param name="ctx"></param>
+ /// <returns></returns>
+ public static InvoiceEntity? GetInvoiceData(this HttpContext ctx)
+ => ctx.Items.TryGet("BTCPAY.INVOICEDATA") as InvoiceEntity;
- public static InvoiceEntity GetInvoiceData(this HttpContext ctx)
- {
- return ctx.Items.TryGet("BTCPAY.INVOICEDATA") as InvoiceEntity;
- }
- public static void SetInvoiceData(this HttpContext ctx, InvoiceEntity invoiceEntity)
- {
- ctx.Items["BTCPAY.INVOICEDATA"] = invoiceEntity;
- }
+ public static void SetInvoiceData(this HttpContext ctx, InvoiceEntity? invoiceEntity)
+ => ctx.Items["BTCPAY.INVOICEDATA"] = invoiceEntity;
- public static PaymentRequestData GetPaymentRequestData(this HttpContext ctx)
- {
- return ctx.Items.TryGet("BTCPAY.PAYMENTREQUESTDATA") as PaymentRequestData;
- }
+ public static PaymentRequestData? GetPaymentRequestData(this HttpContext ctx)
+ => ctx.Items.TryGet("BTCPAY.PAYMENTREQUESTDATA") as PaymentRequestData;
- public static void SetPaymentRequestData(this HttpContext ctx, PaymentRequestData paymentRequestData)
+ public static void SetPaymentRequestData(this HttpContext ctx, PaymentRequestData? paymentRequestData)
{
ctx.Items["BTCPAY.PAYMENTREQUESTDATA"] = paymentRequestData;
}
- public static AppData GetAppData(this HttpContext ctx)
- {
- return ctx.Items.TryGet("BTCPAY.APPDATA") as AppData;
- }
+ public static AppData? GetAppData(this HttpContext ctx)
+ => ctx.Items.TryGet("BTCPAY.APPDATA") as AppData;
- public static void SetAppData(this HttpContext ctx, AppData appData)
+ public static void SetAppData(this HttpContext ctx, AppData? appData)
{
ctx.Items["BTCPAY.APPDATA"] = appData;
}
-
- public static bool SupportChain(this IConfiguration conf, string cryptoCode)
- {
- var supportedChains = conf.GetOrDefault<string>("chains", "btc")
- .Split(',', StringSplitOptions.RemoveEmptyEntries)
- .Select(t => t.ToUpperInvariant()).ToHashSet();
- return supportedChains.Contains(cryptoCode.ToUpperInvariant());
- }
+#nullable restore
class ParameterReplacer : ExpressionVisitor
{
diff --git a/BTCPayServer/Plugins/PayButton/Controllers/UIPayButtonController.cs b/BTCPayServer/Plugins/PayButton/Controllers/UIPayButtonController.cs
index 4316df6..800aad7 100644
--- a/BTCPayServer/Plugins/PayButton/Controllers/UIPayButtonController.cs
+++ b/BTCPayServer/Plugins/PayButton/Controllers/UIPayButtonController.cs
@@ -93,6 +93,6 @@ namespace BTCPayServer.Plugins.PayButton.Controllers
});
}
- private StoreData GetCurrentStore => HttpContext.GetStoreData();
+ private StoreData GetCurrentStore => HttpContext.GetStoreDataOrThrow();
}
}
diff --git a/BTCPayServer/Plugins/Subscriptions/Controllers/UIOfferingController.cs b/BTCPayServer/Plugins/Subscriptions/Controllers/UIOfferingController.cs
index 73c47ca..d6aa2bb 100644
--- a/BTCPayServer/Plugins/Subscriptions/Controllers/UIOfferingController.cs
+++ b/BTCPayServer/Plugins/Subscriptions/Controllers/UIOfferingController.cs
@@ -489,7 +489,7 @@ public partial class UIOfferingController(
OfferingId = offeringId,
PlanId = planId,
OfferingName = offering.App.Name,
- Currency = plan?.Currency ?? this.HttpContext.GetStoreData().GetStoreBlob().DefaultCurrency,
+ Currency = plan?.Currency ?? this.HttpContext.GetStoreDataOrThrow().GetStoreBlob().DefaultCurrency,
Price = plan?.Price ?? 0m,
Name = plan?.Name ?? "",
Description = plan?.Description ?? "",
diff --git a/BTCPayServer/Plugins/Webhooks/Controllers/UIStoreWebhooksController.cs b/BTCPayServer/Plugins/Webhooks/Controllers/UIStoreWebhooksController.cs
index 5ce7aac..f20ec31 100644
--- a/BTCPayServer/Plugins/Webhooks/Controllers/UIStoreWebhooksController.cs
+++ b/BTCPayServer/Plugins/Webhooks/Controllers/UIStoreWebhooksController.cs
@@ -25,7 +25,7 @@ public class UIStoreWebhooksController(
IStringLocalizer stringLocalizer,
WebhookSender webhookSender) : Controller
{
- public Data.StoreData CurrentStore => HttpContext.GetStoreData();
+ public Data.StoreData CurrentStore => HttpContext.GetStoreDataOrThrow();
public IStringLocalizer StringLocalizer { get; set; } = stringLocalizer;
private async Task<Data.WebhookDeliveryData?> LastDeliveryForWebhook(string webhookId)
{
diff --git a/BTCPayServer/Security/BuiltInPermissionScopeProvider.cs b/BTCPayServer/Security/BuiltInPermissionScopeProvider.cs
index 4e4545c..f93f6f8 100644
--- a/BTCPayServer/Security/BuiltInPermissionScopeProvider.cs
+++ b/BTCPayServer/Security/BuiltInPermissionScopeProvider.cs
@@ -103,7 +103,10 @@ public class BuiltInPermissionScopeProvider(
// Consider the route /stores/{storeId}/apps/{appId}
// This check is making sure that the `storeId` is matching the scope resolved from `appId`.
if (storeId2 != storeId)
+ {
storeId2 = null;
+ }
+
if (storeId2 is not null)
additionalScopes.Add(new AdditionalScope(i.RouteValue, id));
}
diff --git a/BTCPayServer/Security/SetContextFilter.cs b/BTCPayServer/Security/SetContextFilter.cs
index 214bb39..8098668 100644
--- a/BTCPayServer/Security/SetContextFilter.cs
+++ b/BTCPayServer/Security/SetContextFilter.cs
@@ -62,7 +62,7 @@ public class SetContextFilter(
httpContext.SetPaymentRequestData(paymentRequest);
break;
case "invoiceId":
- var invoice = await invoiceRepository.GetInvoice(additionalScope.Scope);
+ var invoice = await invoiceRepository.GetInvoice(additionalScope.Scope, true);
if (invoice is not null)
httpContext.SetInvoiceData(invoice);
break;
diff --git a/BTCPayServer/Services/Stores/ScopeProvider.cs b/BTCPayServer/Services/Stores/ScopeProvider.cs
index 5b7b816..3bc1633 100644
--- a/BTCPayServer/Services/Stores/ScopeProvider.cs
+++ b/BTCPayServer/Services/Stores/ScopeProvider.cs
@@ -4,16 +4,7 @@ using Microsoft.AspNetCore.Http;
namespace BTCPayServer.Services.Stores;
-public class ScopeProvider : IScopeProvider
+public class ScopeProvider(IHttpContextAccessor httpContextAccessor) : IScopeProvider
{
- private readonly IHttpContextAccessor _httpContextAccessor;
-
- public ScopeProvider(IHttpContextAccessor httpContextAccessor)
- {
- _httpContextAccessor = httpContextAccessor;
- }
- public string? GetCurrentStoreId()
- {
- return _httpContextAccessor.HttpContext.GetStoreData()?.Id;
- }
+ public string? GetCurrentStoreId() => httpContextAccessor.HttpContext?.GetCurrentStoreId();
}
Why this scored 50/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.