Apply csrf protection on all UI**Controller globally (#7199)
What changed, and why it matters
This commit strengthens BTCPay Server's defenses against cross-site request forgery (CSRF) attacks. Instead of manually adding CSRF protection to each web UI controller, the team added a single global filter that automatically checks CSRF tokens for all UI controllers. They also converted several API (Greenfield) controllers from the web-style Controller base class to the API-style ControllerBase so the global filter does not accidentally apply CSRF checks to API endpoints. A few endpoints that legitimately need to skip CSRF (such as the error page and invoice state changes) are explicitly marked to ignore it. The change is a security hardening patch, but the commit message and diff alone do not prove a specific exploitable vulnerability existed before.
Treat this as a security hardening change and include it in the next release. Review any custom or third-party UI controllers not following the UI** naming convention to ensure they are covered by the new global filter, and verify that [IgnoreAntiforgeryToken] is only applied where truly necessary. Run regression tests on Greenfield API endpoints to confirm they remain unaffected by antiforgery validation.
Security signals we found
Global CSRF/antiforgery enforcement added for UI controllers
Removal of per-controller [AutoValidateAntiforgeryToken] attributes
API controllers moved to ControllerBase to avoid UI antiforgery policy
Explicit [IgnoreAntiforgeryToken] on UIErrorController and ChangeInvoiceState
CSRF validation failure message surfaced through UIErrorController.ErrorDetailsKey
Evidence from the diff
The patch removes per-controller [AutoValidateAntiforgeryToken] attributes from many UI** controllers and introduces a new global authorization filter, UIControllerAntiforgeryTokenAttribute, registered in Startup.cs. The filter validates antiforgery tokens for any controller whose name starts with ‘UI’ or inherits from Controller, while excluding GET/HEAD/TRACE/OPTIONS and anything named ‘Greenfield’. Greenfield API controllers are switched from Controller to ControllerBase to keep them out of the UI path. UIErrorController and UIInvoiceController.ChangeInvoiceState are decorated with [IgnoreAntiforgeryToken] to allow error display and a specific state-change action without tokens. A small UI change wires the delete-account confirmation form correctly, and a test expectation is updated from JsonResult to OkObjectResult after a Greenfield controller change.
Changed components
BTCPayServer/Filters/UIControllerAntiforgeryTokenAttribute.csBTCPayServer/Hosting/Startup.csAll UI** controllersGreenfield API controllers (base class change)BTCPayServer/Views/UIError/Handle.cshtmlBTCPayServer/Views/UIManage/Index.cshtmlInspect captured patch +97 / −60
diff --git a/BTCPayServer.Tests/UnitTest1.cs b/BTCPayServer.Tests/UnitTest1.cs
index e041b52..b8c7c2e 100644
--- a/BTCPayServer.Tests/UnitTest1.cs
+++ b/BTCPayServer.Tests/UnitTest1.cs
@@ -2948,7 +2948,7 @@ namespace BTCPayServer.Tests
private async Task<StoreReportResponse> GetReport(TestAccount acc, StoreReportRequest req)
{
var controller = acc.GetController<UIReportsController>();
- return (await controller.StoreReportsJson(acc.StoreId, req)).AssertType<JsonResult>()
+ return (await controller.StoreReportsJson(acc.StoreId, req)).AssertType<OkObjectResult>()
.Value
.AssertType<StoreReportResponse>();
}
diff --git a/BTCPayServer/Controllers/GreenField/GreenfieldFilesController.cs b/BTCPayServer/Controllers/GreenField/GreenfieldFilesController.cs
index c144635..e82fad0 100644
--- a/BTCPayServer/Controllers/GreenField/GreenfieldFilesController.cs
+++ b/BTCPayServer/Controllers/GreenField/GreenfieldFilesController.cs
@@ -23,7 +23,7 @@ public class GreenfieldFilesController(
UserManager<ApplicationUser> userManager,
IFileService fileService,
StoredFileRepository fileRepository)
- : Controller
+ : ControllerBase
{
[HttpGet("~/api/v1/files")]
public async Task<IActionResult> GetFiles()
diff --git a/BTCPayServer/Controllers/GreenField/GreenfieldInvoiceController.cs b/BTCPayServer/Controllers/GreenField/GreenfieldInvoiceController.cs
index 29177f1..d6a64e5 100644
--- a/BTCPayServer/Controllers/GreenField/GreenfieldInvoiceController.cs
+++ b/BTCPayServer/Controllers/GreenField/GreenfieldInvoiceController.cs
@@ -33,7 +33,7 @@ namespace BTCPayServer.Controllers.Greenfield
[ApiController]
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[EnableCors(CorsPolicies.All)]
- public class GreenfieldInvoiceController : Controller
+ public class GreenfieldInvoiceController : ControllerBase
{
private readonly UIInvoiceController _invoiceController;
private readonly InvoiceRepository _invoiceRepository;
diff --git a/BTCPayServer/Controllers/GreenField/GreenfieldLightningNodeApiController.Internal.cs b/BTCPayServer/Controllers/GreenField/GreenfieldLightningNodeApiController.Internal.cs
index 7d53566..b1cdde4 100644
--- a/BTCPayServer/Controllers/GreenField/GreenfieldLightningNodeApiController.Internal.cs
+++ b/BTCPayServer/Controllers/GreenField/GreenfieldLightningNodeApiController.Internal.cs
@@ -20,21 +20,17 @@ namespace BTCPayServer.Controllers.Greenfield
[EnableCors(CorsPolicies.All)]
public class GreenfieldInternalLightningNodeApiController : GreenfieldLightningNodeApiController
{
- private readonly LightningClientFactoryService _lightningClientFactory;
private readonly IOptions<LightningNetworkOptions> _lightningNetworkOptions;
- private readonly PaymentMethodHandlerDictionary _handlers;
public GreenfieldInternalLightningNodeApiController(
- PoliciesSettings policiesSettings, LightningClientFactoryService lightningClientFactory,
+ PoliciesSettings policiesSettings,
IOptions<LightningNetworkOptions> lightningNetworkOptions,
IAuthorizationService authorizationService,
PaymentMethodHandlerDictionary handlers,
LightningHistogramService lnHistogramService
) : base(policiesSettings, authorizationService, handlers, lnHistogramService)
{
- _lightningClientFactory = lightningClientFactory;
_lightningNetworkOptions = lightningNetworkOptions;
- _handlers = handlers;
}
[Authorize(Policy = Policies.CanUseInternalLightningNode,
diff --git a/BTCPayServer/Controllers/GreenField/GreenfieldLightningNodeApiController.cs b/BTCPayServer/Controllers/GreenField/GreenfieldLightningNodeApiController.cs
index 2a48e80..44a2e07 100644
--- a/BTCPayServer/Controllers/GreenField/GreenfieldLightningNodeApiController.cs
+++ b/BTCPayServer/Controllers/GreenField/GreenfieldLightningNodeApiController.cs
@@ -27,7 +27,7 @@ namespace BTCPayServer.Controllers.Greenfield
}
}
- public abstract class GreenfieldLightningNodeApiController : Controller
+ public abstract class GreenfieldLightningNodeApiController : ControllerBase
{
private readonly PoliciesSettings _policiesSettings;
private readonly IAuthorizationService _authorizationService;
@@ -89,7 +89,7 @@ namespace BTCPayServer.Controllers.Greenfield
: null
});
}
-
+
public virtual async Task<IActionResult> GetHistogram(string cryptoCode, HistogramType? type = null, CancellationToken cancellationToken = default)
{
Enum.TryParse<HistogramType>(type.ToString(), true, out var histType);
@@ -361,7 +361,7 @@ namespace BTCPayServer.Controllers.Greenfield
}
}
protected BTCPayNetwork GetNetwork(string cryptoCode)
- => _handlers.TryGetValue(PaymentTypes.LN.GetPaymentMethodId(cryptoCode), out var h)
+ => _handlers.TryGetValue(PaymentTypes.LN.GetPaymentMethodId(cryptoCode), out var h)
&& h is IHasNetwork { Network: var network }
? network
: null;
diff --git a/BTCPayServer/Controllers/GreenField/GreenfieldPaymentRequestsController.cs b/BTCPayServer/Controllers/GreenField/GreenfieldPaymentRequestsController.cs
index 854d851..775310c 100644
--- a/BTCPayServer/Controllers/GreenField/GreenfieldPaymentRequestsController.cs
+++ b/BTCPayServer/Controllers/GreenField/GreenfieldPaymentRequestsController.cs
@@ -17,9 +17,6 @@ using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Routing;
-using TwentyTwenty.Storage;
-using static System.Runtime.InteropServices.JavaScript.JSType;
-using static QRCoder.PayloadGenerator;
using PaymentRequestData = BTCPayServer.Data.PaymentRequestData;
namespace BTCPayServer.Controllers.Greenfield
diff --git a/BTCPayServer/Controllers/GreenField/GreenfieldPayoutProcessorsController.cs b/BTCPayServer/Controllers/GreenField/GreenfieldPayoutProcessorsController.cs
index 594a706..50d9468 100644
--- a/BTCPayServer/Controllers/GreenField/GreenfieldPayoutProcessorsController.cs
+++ b/BTCPayServer/Controllers/GreenField/GreenfieldPayoutProcessorsController.cs
@@ -13,20 +13,13 @@ namespace BTCPayServer.Controllers.Greenfield
[ApiController]
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[EnableCors(CorsPolicies.All)]
- public class GreenfieldPayoutProcessorsController : ControllerBase
+ public class GreenfieldPayoutProcessorsController(IEnumerable<IPayoutProcessorFactory> factories) : ControllerBase
{
- private readonly IEnumerable<IPayoutProcessorFactory> _factories;
-
- public GreenfieldPayoutProcessorsController(IEnumerable<IPayoutProcessorFactory> factories)
- {
- _factories = factories;
- }
-
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpGet("~/api/v1/payout-processors")]
public IActionResult GetPayoutProcessors()
{
- return Ok(_factories.Select(factory => new PayoutProcessorData()
+ return Ok(factories.Select(factory => new PayoutProcessorData()
{
Name = factory.Processor,
FriendlyName = factory.FriendlyName,
diff --git a/BTCPayServer/Controllers/GreenField/GreenfieldReportsController.cs b/BTCPayServer/Controllers/GreenField/GreenfieldReportsController.cs
index ede039f..aa94ce4 100644
--- a/BTCPayServer/Controllers/GreenField/GreenfieldReportsController.cs
+++ b/BTCPayServer/Controllers/GreenField/GreenfieldReportsController.cs
@@ -20,17 +20,12 @@ namespace BTCPayServer.Controllers.GreenField;
[ApiController]
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[EnableCors(CorsPolicies.All)]
-public class GreenfieldReportsController : Controller
+public class GreenfieldReportsController(
+ ApplicationDbContextFactory dbContextFactory,
+ ReportService reportService) : ControllerBase
{
- public GreenfieldReportsController(
- ApplicationDbContextFactory dbContextFactory,
- ReportService reportService)
- {
- DBContextFactory = dbContextFactory;
- ReportService = reportService;
- }
- public ApplicationDbContextFactory DBContextFactory { get; }
- public ReportService ReportService { get; }
+ public ApplicationDbContextFactory DBContextFactory { get; } = dbContextFactory;
+ public ReportService ReportService { get; } = reportService;
public const string DefaultReport = "Invoices";
[Authorize(Policy = Policies.CanViewReports, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
@@ -62,7 +57,7 @@ public class GreenfieldReportsController : Controller
From = from,
To = to
};
- return Json(result);
+ return Ok(result);
}
ModelState.AddModelError(nameof(vm.ViewName), "View doesn't exist");
diff --git a/BTCPayServer/Controllers/GreenField/GreenfieldServerEmailController.cs b/BTCPayServer/Controllers/GreenField/GreenfieldServerEmailController.cs
index 3e73e1d..b37902f 100644
--- a/BTCPayServer/Controllers/GreenField/GreenfieldServerEmailController.cs
+++ b/BTCPayServer/Controllers/GreenField/GreenfieldServerEmailController.cs
@@ -15,7 +15,7 @@ namespace BTCPayServer.Controllers.GreenField
[ApiController]
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[EnableCors(CorsPolicies.All)]
- public class GreenfieldServerEmailController : Controller
+ public class GreenfieldServerEmailController : ControllerBase
{
private readonly EmailSenderFactory _emailSenderFactory;
private readonly PoliciesSettings _policiesSettings;
diff --git a/BTCPayServer/Controllers/GreenField/GreenfieldServerInfoController.cs b/BTCPayServer/Controllers/GreenField/GreenfieldServerInfoController.cs
index 762bd8a..2d99106 100644
--- a/BTCPayServer/Controllers/GreenField/GreenfieldServerInfoController.cs
+++ b/BTCPayServer/Controllers/GreenField/GreenfieldServerInfoController.cs
@@ -13,7 +13,7 @@ namespace BTCPayServer.Controllers.Greenfield
{
[ApiController]
[EnableCors(CorsPolicies.All)]
- public class GreenfieldServerInfoController : Controller
+ public class GreenfieldServerInfoController : ControllerBase
{
private readonly BTCPayServerEnvironment _env;
private readonly PaymentMethodHandlerDictionary _paymentMethodHandlerDictionary;
diff --git a/BTCPayServer/Controllers/GreenField/GreenfieldStoreEmailController.cs b/BTCPayServer/Controllers/GreenField/GreenfieldStoreEmailController.cs
index 02cc52b..c8ab5c9 100644
--- a/BTCPayServer/Controllers/GreenField/GreenfieldStoreEmailController.cs
+++ b/BTCPayServer/Controllers/GreenField/GreenfieldStoreEmailController.cs
@@ -16,7 +16,7 @@ namespace BTCPayServer.Controllers.GreenField
[ApiController]
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[EnableCors(CorsPolicies.All)]
- public class GreenfieldStoreEmailController : Controller
+ public class GreenfieldStoreEmailController : ControllerBase
{
private readonly EmailSenderFactory _emailSenderFactory;
private readonly StoreRepository _storeRepository;
diff --git a/BTCPayServer/Controllers/GreenField/GreenfieldStoreOnChainWalletsController.cs b/BTCPayServer/Controllers/GreenField/GreenfieldStoreOnChainWalletsController.cs
index a09dce7..b0bdada 100644
--- a/BTCPayServer/Controllers/GreenField/GreenfieldStoreOnChainWalletsController.cs
+++ b/BTCPayServer/Controllers/GreenField/GreenfieldStoreOnChainWalletsController.cs
@@ -36,7 +36,7 @@ namespace BTCPayServer.Controllers.Greenfield
[ApiController]
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[EnableCors(CorsPolicies.All)]
- public class GreenfieldStoreOnChainWalletsController : Controller
+ public class GreenfieldStoreOnChainWalletsController : ControllerBase
{
private StoreData Store => HttpContext.GetStoreData();
diff --git a/BTCPayServer/Controllers/UIAppsController.cs b/BTCPayServer/Controllers/UIAppsController.cs
index ae33d2f..f0d78c2 100644
--- a/BTCPayServer/Controllers/UIAppsController.cs
+++ b/BTCPayServer/Controllers/UIAppsController.cs
@@ -21,7 +21,6 @@ using Microsoft.Extensions.Localization;
namespace BTCPayServer.Controllers
{
- [AutoValidateAntiforgeryToken]
[Route("apps")]
public partial class UIAppsController : Controller
{
diff --git a/BTCPayServer/Controllers/UIErrorController.cs b/BTCPayServer/Controllers/UIErrorController.cs
index 73555a4..26e44a0 100644
--- a/BTCPayServer/Controllers/UIErrorController.cs
+++ b/BTCPayServer/Controllers/UIErrorController.cs
@@ -4,8 +4,10 @@ using Microsoft.AspNetCore.Mvc;
namespace BTCPayServer.Controllers
{
+ [IgnoreAntiforgeryToken]
public class UIErrorController : Controller
{
+ public const string ErrorDetailsKey = "ERROR_DETAILS";
[Route("/errors/{statusCode:int}")]
public IActionResult Handle(int? statusCode = null)
{
diff --git a/BTCPayServer/Controllers/UIInvoiceController.UI.cs b/BTCPayServer/Controllers/UIInvoiceController.UI.cs
index 2e0beac..61b625d 100644
--- a/BTCPayServer/Controllers/UIInvoiceController.UI.cs
+++ b/BTCPayServer/Controllers/UIInvoiceController.UI.cs
@@ -1262,6 +1262,7 @@ namespace BTCPayServer.Controllers
[Route("invoices/{invoiceId}/changestate/{newState}")]
[Route("stores/{storeId}/invoices/{invoiceId}/changestate/{newState}")]
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie, Policy = Policies.CanViewInvoices)]
+ [IgnoreAntiforgeryToken]
public async Task<IActionResult> ChangeInvoiceState(string invoiceId, string newState)
{
var invoice = (await _InvoiceRepository.GetInvoices(new InvoiceQuery
diff --git a/BTCPayServer/Controllers/UIReportsController.cs b/BTCPayServer/Controllers/UIReportsController.cs
index ce7f6d3..a7ddee9 100644
--- a/BTCPayServer/Controllers/UIReportsController.cs
+++ b/BTCPayServer/Controllers/UIReportsController.cs
@@ -17,7 +17,6 @@ using Newtonsoft.Json.Linq;
namespace BTCPayServer.Controllers;
[Authorize(Policy = Policies.CanViewReports, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
-[AutoValidateAntiforgeryToken]
public partial class UIReportsController : Controller
{
public UIReportsController(
diff --git a/BTCPayServer/Controllers/UIStorePullPaymentsController.PullPayments.cs b/BTCPayServer/Controllers/UIStorePullPaymentsController.PullPayments.cs
index 7800a3f..927ce92 100644
--- a/BTCPayServer/Controllers/UIStorePullPaymentsController.PullPayments.cs
+++ b/BTCPayServer/Controllers/UIStorePullPaymentsController.PullPayments.cs
@@ -28,7 +28,6 @@ using StoreData = BTCPayServer.Data.StoreData;
namespace BTCPayServer.Controllers
{
[Authorize(Policy = Policies.CanViewPullPayments, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
- [AutoValidateAntiforgeryToken]
public class UIStorePullPaymentsController : Controller
{
private readonly BTCPayNetworkProvider _btcPayNetworkProvider;
diff --git a/BTCPayServer/Controllers/UIStoresController.cs b/BTCPayServer/Controllers/UIStoresController.cs
index ee8d1a6..a4e7bf3 100644
--- a/BTCPayServer/Controllers/UIStoresController.cs
+++ b/BTCPayServer/Controllers/UIStoresController.cs
@@ -29,7 +29,6 @@ namespace BTCPayServer.Controllers;
[Route("stores")]
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[Authorize(Policy = Policies.CanViewStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
-[AutoValidateAntiforgeryToken]
public partial class UIStoresController : Controller
{
public UIStoresController(
diff --git a/BTCPayServer/Controllers/UIUserStoresController.cs b/BTCPayServer/Controllers/UIUserStoresController.cs
index 3f6a106..9b90225 100644
--- a/BTCPayServer/Controllers/UIUserStoresController.cs
+++ b/BTCPayServer/Controllers/UIUserStoresController.cs
@@ -19,7 +19,6 @@ using Microsoft.Extensions.Localization;
namespace BTCPayServer.Controllers
{
[Route("stores")]
- [AutoValidateAntiforgeryToken]
public class UIUserStoresController : Controller
{
private readonly StoreRepository _repo;
diff --git a/BTCPayServer/Controllers/UIWalletsController.cs b/BTCPayServer/Controllers/UIWalletsController.cs
index da07d38..355e463 100644
--- a/BTCPayServer/Controllers/UIWalletsController.cs
+++ b/BTCPayServer/Controllers/UIWalletsController.cs
@@ -50,7 +50,6 @@ namespace BTCPayServer.Controllers
{
[Route("wallets")]
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
- [AutoValidateAntiforgeryToken]
//16mb psbts
[RequestFormLimits(ValueLengthLimit = FormReader.DefaultValueLengthLimit * 4)]
public partial class UIWalletsController : Controller
diff --git a/BTCPayServer/Data/Payouts/LightningLike/UILightningLikePayoutController.cs b/BTCPayServer/Data/Payouts/LightningLike/UILightningLikePayoutController.cs
index a8e5b9c..ccf6e13 100644
--- a/BTCPayServer/Data/Payouts/LightningLike/UILightningLikePayoutController.cs
+++ b/BTCPayServer/Data/Payouts/LightningLike/UILightningLikePayoutController.cs
@@ -24,7 +24,6 @@ using Microsoft.Extensions.Options;
namespace BTCPayServer.Data.Payouts.LightningLike
{
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie)]
- [AutoValidateAntiforgeryToken]
public class UILightningLikePayoutController : Controller
{
private readonly ApplicationDbContextFactory _applicationDbContextFactory;
diff --git a/BTCPayServer/Filters/UIControllerAntiforgeryTokenAttribute.cs b/BTCPayServer/Filters/UIControllerAntiforgeryTokenAttribute.cs
new file mode 100644
index 0000000..1e6357c
--- /dev/null
+++ b/BTCPayServer/Filters/UIControllerAntiforgeryTokenAttribute.cs
@@ -0,0 +1,65 @@
+#nullable enable
+using System;
+using System.Threading.Tasks;
+using BTCPayServer.Controllers;
+using Microsoft.AspNetCore.Antiforgery;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.Controllers;
+using Microsoft.AspNetCore.Mvc.Filters;
+using Microsoft.AspNetCore.Mvc.ViewFeatures;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace BTCPayServer.Filters;
+
+public class UIControllerAntiforgeryTokenAttribute :
+ Attribute,
+ IFilterMetadata,
+ IAntiforgeryPolicy,
+ IAsyncAuthorizationFilter
+{
+ public async Task OnAuthorizationAsync(AuthorizationFilterContext context)
+ {
+ if (context.Result is AntiforgeryValidationFailedResult)
+ AddErrorDetails(context.HttpContext);
+ var antiForgery = context.HttpContext.RequestServices.GetService<IAntiforgery>();
+ if (
+ antiForgery is not null &&
+ context.IsEffectivePolicy<IAntiforgeryPolicy>(this)
+ && this.ShouldValidate(context))
+ {
+ try
+ {
+ await antiForgery.ValidateRequestAsync(context.HttpContext);
+ }
+ catch (AntiforgeryValidationException)
+ {
+ context.Result = new AntiforgeryValidationFailedResult();
+ AddErrorDetails(context.HttpContext);
+ }
+ }
+ }
+
+ private void AddErrorDetails(HttpContext context)
+ => context.Items[UIErrorController.ErrorDetailsKey] = "CSRF token validation failed.";
+
+ private bool ShouldValidate(AuthorizationFilterContext context)
+ {
+ var isUI = IsUI(context);
+ if (isUI is false)
+ return false;
+ var method = context.HttpContext.Request.Method;
+ return !HttpMethods.IsGet(method) && !HttpMethods.IsHead(method) && !HttpMethods.IsTrace(method) && !HttpMethods.IsOptions(method);
+ }
+
+ private bool? IsUI(AuthorizationFilterContext context)
+ {
+ if (context.ActionDescriptor is not ControllerActionDescriptor controllerActionDescriptor)
+ return null;
+ if (controllerActionDescriptor.ControllerName.StartsWith("UI", StringComparison.OrdinalIgnoreCase))
+ return true;
+ if (controllerActionDescriptor.ControllerName.StartsWith("Greenfield", StringComparison.OrdinalIgnoreCase))
+ return false;
+ return typeof(Controller).IsAssignableFrom(controllerActionDescriptor.ControllerTypeInfo);
+ }
+}
diff --git a/BTCPayServer/Hosting/Startup.cs b/BTCPayServer/Hosting/Startup.cs
index 4dc62b7..1cf6a78 100644
--- a/BTCPayServer/Hosting/Startup.cs
+++ b/BTCPayServer/Hosting/Startup.cs
@@ -157,7 +157,7 @@ namespace BTCPayServer.Hosting
services.AddSingleton<LnurlAuthService>();
services.AddSingleton<LightningAddressService>();
var mvcBuilder = services.AddMvc(o =>
- {
+ {
o.Filters.Add(new XFrameOptionsAttribute(XFrameOptionsAttribute.XFrameOptions.Deny));
o.Filters.Add(new XContentTypeOptionsAttribute("nosniff"));
o.Filters.Add(new XXSSProtectionAttribute());
@@ -167,6 +167,7 @@ namespace BTCPayServer.Hosting
o.Filters.Add(new ContentSecurityPolicyAttribute(CSPTemplate.AntiXSS));
o.Filters.Add(new JsonHttpExceptionFilter());
o.Filters.Add(new JsonObjectExceptionFilter());
+ o.Filters.Add(new UIControllerAntiforgeryTokenAttribute());
})
.ConfigureApiBehaviorOptions(options =>
{
diff --git a/BTCPayServer/Plugins/Crowdfund/Controllers/UICrowdfundController.cs b/BTCPayServer/Plugins/Crowdfund/Controllers/UICrowdfundController.cs
index 2124e91..2078b4b 100644
--- a/BTCPayServer/Plugins/Crowdfund/Controllers/UICrowdfundController.cs
+++ b/BTCPayServer/Plugins/Crowdfund/Controllers/UICrowdfundController.cs
@@ -37,7 +37,6 @@ using CrowdfundResetEvery = BTCPayServer.Services.Apps.CrowdfundResetEvery;
namespace BTCPayServer.Plugins.Crowdfund.Controllers
{
- [AutoValidateAntiforgeryToken]
[Route("apps")]
[Area(CrowdfundPlugin.Area)]
public class UICrowdfundController(
diff --git a/BTCPayServer/Plugins/Emails/Controllers/UIServerEmailController.cs b/BTCPayServer/Plugins/Emails/Controllers/UIServerEmailController.cs
index e1e963f..04c4d3e 100644
--- a/BTCPayServer/Plugins/Emails/Controllers/UIServerEmailController.cs
+++ b/BTCPayServer/Plugins/Emails/Controllers/UIServerEmailController.cs
@@ -14,7 +14,6 @@ namespace BTCPayServer.Plugins.Emails.Controllers;
[Area(EmailsPlugin.Area)]
[Authorize(Policy = Client.Policies.CanModifyServerSettings,
AuthenticationSchemes = AuthenticationSchemes.Cookie)]
-[AutoValidateAntiforgeryToken]
public class UIServerEmailController(
EmailSenderFactory emailSenderFactory,
PoliciesSettings policiesSettings,
diff --git a/BTCPayServer/Plugins/Emails/Controllers/UIServerEmailRulesController.cs b/BTCPayServer/Plugins/Emails/Controllers/UIServerEmailRulesController.cs
index 41679b5..b2eb5fa 100644
--- a/BTCPayServer/Plugins/Emails/Controllers/UIServerEmailRulesController.cs
+++ b/BTCPayServer/Plugins/Emails/Controllers/UIServerEmailRulesController.cs
@@ -18,7 +18,6 @@ namespace BTCPayServer.Plugins.Emails.Controllers;
[Route("server/rules")]
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[Authorize(Policy = Policies.CanModifyServerSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
-[AutoValidateAntiforgeryToken]
public class UIServerEmailRulesController(
EmailSenderFactory emailSenderFactory,
EmailTriggerViewModels triggers,
diff --git a/BTCPayServer/Plugins/Emails/Controllers/UIStoreEmailRulesController.cs b/BTCPayServer/Plugins/Emails/Controllers/UIStoreEmailRulesController.cs
index c5b3c6c..ff64e84 100644
--- a/BTCPayServer/Plugins/Emails/Controllers/UIStoreEmailRulesController.cs
+++ b/BTCPayServer/Plugins/Emails/Controllers/UIStoreEmailRulesController.cs
@@ -19,7 +19,6 @@ namespace BTCPayServer.Plugins.Emails.Controllers;
[Route("stores/{storeId}/emails/rules")]
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[Authorize(Policy = Policies.CanViewStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
-[AutoValidateAntiforgeryToken]
public class UIStoreEmailRulesController(
EmailSenderFactory emailSenderFactory,
LinkGenerator linkGenerator,
diff --git a/BTCPayServer/Plugins/Emails/Controllers/UIStoresEmailController.cs b/BTCPayServer/Plugins/Emails/Controllers/UIStoresEmailController.cs
index d409a5b..a313318 100644
--- a/BTCPayServer/Plugins/Emails/Controllers/UIStoresEmailController.cs
+++ b/BTCPayServer/Plugins/Emails/Controllers/UIStoresEmailController.cs
@@ -15,7 +15,6 @@ namespace BTCPayServer.Plugins.Emails.Controllers;
[Route("stores/{storeId}")]
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[Authorize(Policy = Policies.CanViewStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
-[AutoValidateAntiforgeryToken]
public class UIStoresEmailController(
EmailSenderFactory emailSenderFactory,
StoreRepository storeRepository,
diff --git a/BTCPayServer/Plugins/PayButton/Controllers/UIPayButtonController.cs b/BTCPayServer/Plugins/PayButton/Controllers/UIPayButtonController.cs
index 0018459..07f4254 100644
--- a/BTCPayServer/Plugins/PayButton/Controllers/UIPayButtonController.cs
+++ b/BTCPayServer/Plugins/PayButton/Controllers/UIPayButtonController.cs
@@ -19,7 +19,6 @@ namespace BTCPayServer.Plugins.PayButton.Controllers
[Route("stores")]
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
- [AutoValidateAntiforgeryToken]
[Area(PayButtonPlugin.Area)]
public class UIPayButtonController(
StoreRepository repo,
diff --git a/BTCPayServer/Plugins/PointOfSale/Controllers/UIPointOfSaleController.cs b/BTCPayServer/Plugins/PointOfSale/Controllers/UIPointOfSaleController.cs
index 1a96894..98a9b92 100644
--- a/BTCPayServer/Plugins/PointOfSale/Controllers/UIPointOfSaleController.cs
+++ b/BTCPayServer/Plugins/PointOfSale/Controllers/UIPointOfSaleController.cs
@@ -44,7 +44,6 @@ using StoreData = BTCPayServer.Data.StoreData;
namespace BTCPayServer.Plugins.PointOfSale.Controllers
{
- [AutoValidateAntiforgeryToken]
[Route("apps")]
public class UIPointOfSaleController : Controller
{
diff --git a/BTCPayServer/Plugins/Subscriptions/Controllers/UIOfferingController.cs b/BTCPayServer/Plugins/Subscriptions/Controllers/UIOfferingController.cs
index 3787633..782d7bc 100644
--- a/BTCPayServer/Plugins/Subscriptions/Controllers/UIOfferingController.cs
+++ b/BTCPayServer/Plugins/Subscriptions/Controllers/UIOfferingController.cs
@@ -28,7 +28,6 @@ using DisplayFormatter = BTCPayServer.Services.DisplayFormatter;
namespace BTCPayServer.Plugins.Subscriptions.Controllers;
[Authorize(Policy = Policies.CanViewOfferings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
-[AutoValidateAntiforgeryToken]
[Area(SubscriptionsPlugin.Area)]
public partial class UIOfferingController(
ApplicationDbContextFactory dbContextFactory,
diff --git a/BTCPayServer/Plugins/Subscriptions/Controllers/UIPlanCheckoutController.cs b/BTCPayServer/Plugins/Subscriptions/Controllers/UIPlanCheckoutController.cs
index 5de4789..1f69259 100644
--- a/BTCPayServer/Plugins/Subscriptions/Controllers/UIPlanCheckoutController.cs
+++ b/BTCPayServer/Plugins/Subscriptions/Controllers/UIPlanCheckoutController.cs
@@ -15,7 +15,6 @@ namespace BTCPayServer.Plugins.Subscriptions.Controllers;
[AllowAnonymous]
-[AutoValidateAntiforgeryToken]
[Area(SubscriptionsPlugin.Area)]
[Route("plan-checkout/{checkoutId}")]
public class UIPlanCheckoutController(
diff --git a/BTCPayServer/Plugins/Subscriptions/Controllers/UISubscriberPortalController.cs b/BTCPayServer/Plugins/Subscriptions/Controllers/UISubscriberPortalController.cs
index 310a289..0ebbaaa 100644
--- a/BTCPayServer/Plugins/Subscriptions/Controllers/UISubscriberPortalController.cs
+++ b/BTCPayServer/Plugins/Subscriptions/Controllers/UISubscriberPortalController.cs
@@ -23,7 +23,6 @@ using Microsoft.Extensions.Localization;
namespace BTCPayServer.Plugins.Subscriptions.Controllers;
[AllowAnonymous]
-[AutoValidateAntiforgeryToken]
[Area(SubscriptionsPlugin.Area)]
[Route("subscriber-portal/{portalSessionId}")]
public class UISubscriberPortalController(
diff --git a/BTCPayServer/Plugins/Translations/Controllers/UITranslationController.cs b/BTCPayServer/Plugins/Translations/Controllers/UITranslationController.cs
index afa4768..5273219 100644
--- a/BTCPayServer/Plugins/Translations/Controllers/UITranslationController.cs
+++ b/BTCPayServer/Plugins/Translations/Controllers/UITranslationController.cs
@@ -17,7 +17,6 @@ namespace BTCPayServer.Plugins.Translations.Controllers;
[Authorize(Policy = Client.Policies.CanModifyServerSettings,
AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[Area(TranslationsPlugin.Area)]
-[AutoValidateAntiforgeryToken]
public class UITranslationController(
PoliciesSettings policiesSettings,
IStringLocalizer stringLocalizer,
diff --git a/BTCPayServer/Plugins/Webhooks/Controllers/UIStoreWebhooksController.cs b/BTCPayServer/Plugins/Webhooks/Controllers/UIStoreWebhooksController.cs
index ccf085f..5ce7aac 100644
--- a/BTCPayServer/Plugins/Webhooks/Controllers/UIStoreWebhooksController.cs
+++ b/BTCPayServer/Plugins/Webhooks/Controllers/UIStoreWebhooksController.cs
@@ -19,7 +19,6 @@ namespace BTCPayServer.Plugins.Webhooks.Controllers;
[Route("stores")]
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[Authorize(Policy = Policies.CanViewStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
-[AutoValidateAntiforgeryToken]
[Area(WebhooksPlugin.Area)]
public class UIStoreWebhooksController(
StoreRepository storeRepo,
diff --git a/BTCPayServer/Views/UIError/Handle.cshtml b/BTCPayServer/Views/UIError/Handle.cshtml
index 25f4fc5..2ef1cee 100644
--- a/BTCPayServer/Views/UIError/Handle.cshtml
+++ b/BTCPayServer/Views/UIError/Handle.cshtml
@@ -1,5 +1,6 @@
@using System.Net
@using System.Text.RegularExpressions
+@using BTCPayServer.Controllers
@model int?
@{
Layout = "_LayoutError";
@@ -10,8 +11,13 @@
var name = Regex.Replace(httpCode.ToString(), @"(\B[A-Z])", @" $1");
ViewData["Title"] = $"{(int)httpCode} - {name}";
}
+ this.Context.Items.TryGetValue(UIErrorController.ErrorDetailsKey, out var errorDetails);
}
<p class="mt-4">A generic error occurred (HTTP Code: @Model)</p>
+@if (errorDetails != null)
+{
+ <pre>@errorDetails</pre>
+}
<p text-translate="true">Please consult the server log for more details.</p>
<a href="/" text-translate="true">Navigate back to home</a>
diff --git a/BTCPayServer/Views/UIManage/Index.cshtml b/BTCPayServer/Views/UIManage/Index.cshtml
index b4beeb9..793e311 100644
--- a/BTCPayServer/Views/UIManage/Index.cshtml
+++ b/BTCPayServer/Views/UIManage/Index.cshtml
@@ -70,12 +70,12 @@
}
<h3 class="mt-5 mb-4" text-translate="true">Delete Account</h3>
<div id="danger-zone">
- <a id="delete-user" class="btn btn-outline-danger mb-5" data-confirm-input="@StringLocalizer["Delete"]" data-bs-toggle="modal" data-bs-target="#ConfirmModal" asp-action="DeleteUserPost" data-description="@StringLocalizer["This action will also delete all stores, invoices, apps and data associated with the user."]" text-translate="true">Delete Account</a>
+ <a id="delete-user" asp-action="DeleteUserPost" class="btn btn-outline-danger mb-5" data-confirm-input="@StringLocalizer["Delete"]" data-bs-toggle="modal" data-bs-target="#ConfirmModal" text-translate="true">Delete Account</a>
</div>
</div>
</form>
<partial name="_Confirm"
- model="@(new ConfirmModel(StringLocalizer["Delete user"], StringLocalizer["The user will be permanently deleted. This action will also delete all stores, invoices, apps and data associated with your user."], StringLocalizer["Delete"], actionName: nameof(BTCPayServer.Controllers.UIManageController.DeleteUserPost)))"/>
+ model="@(new ConfirmModel(StringLocalizer["Delete user"], StringLocalizer["The user will be permanently deleted. This action will also delete all stores, invoices, apps and data associated with your user."], StringLocalizer["Delete"]))"/>
@section PageFootContent {
<partial name="_ValidationScriptsPartial"/>
Why this scored 83/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.