Validate antiforgery token on app file uploads
What changed, and why it matters
This commit fixes a security gap in BTCPay Server's app file upload feature. Previously, the upload endpoint ignored anti-forgery tokens, which are a standard defense against cross-site request forgery (CSRF) attacks. The patch removes that exemption and updates the JavaScript uploader to include the token with each upload request. A new test verifies the endpoint now requires anti-forgery validation.
Treat this as a security fix and include it in the next release. Users running affected versions should upgrade promptly, especially if they expose BTCPay Server app editing to multiple administrators or untrusted browser contexts. Review other [IgnoreAntiforgeryToken] usages for similar CSRF exposure.
Security signals we found
Removal of [IgnoreAntiforgeryToken] from a state-changing POST endpoint
Addition of anti-forgery token header in client-side upload code
New test asserting anti-forgery validation is required for FileUpload
Cookie-authenticated, authorization-gated file upload endpoint
Evidence from the diff
The UIAppsController.FileUpload action was decorated with [IgnoreAntiforgeryToken], disabling ASP.NET Core’s automatic validation of the request verification token. Combined with cookie-based authorization (AuthenticationSchemes = Cookie), this made the endpoint susceptible to CSRF: a malicious site could cause an authenticated administrator’s browser to POST a file to /apps/{appId}/upload-file. The patch removes [IgnoreAntiforgeryToken], and template-editor.js now reads the __RequestVerificationToken input from the surrounding form and sends it in the RequestVerificationToken header. A unit test asserts the method no longer carries IgnoreAntiforgeryTokenAttribute.
Changed components
BTCPayServer/Controllers/UIAppsController.csBTCPayServer/wwwroot/js/template-editor.jsBTCPayServer.Tests/AuthorizationPolicyTests.csInspect captured patch +12 / −2
### BTCPayServer.Tests/AuthorizationPolicyTests.cs
@@ -48,4 +48,13 @@ public void ChangeInvoiceStateRequiresAntiforgeryValidation()
foreach (var method in methods)
Assert.Empty(method.GetCustomAttributes<IgnoreAntiforgeryTokenAttribute>(true));
}
+
+ [Fact]
+ [Trait("Fast", "Fast")]
+ public void FileUploadRequiresAntiforgeryValidation()
+ {
+ var method = typeof(UIAppsController).GetMethod(nameof(UIAppsController.FileUpload));
+ Assert.NotNull(method);
+ Assert.Empty(method.GetCustomAttributes<IgnoreAntiforgeryTokenAttribute>(true));
+ }
}
### BTCPayServer/Controllers/UIAppsController.cs
@@ -228,7 +228,6 @@ public async Task<IActionResult> ToggleArchive(string appId)
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[HttpPost("{appId}/upload-file")]
- [IgnoreAntiforgeryToken]
public async Task<IActionResult> FileUpload(IFormFile file)
{
var app = GetCurrentApp();
### BTCPayServer/wwwroot/js/template-editor.js
@@ -58,8 +58,10 @@ document.addEventListener('DOMContentLoaded', () => {
this.$refs.input.classList.remove('is-invalid');
const formData = new FormData();
formData.append('file', file);
+ const tokenInput = this.$el.closest('form').querySelector('input[name="__RequestVerificationToken"]');
+ const headers = { RequestVerificationToken: tokenInput.value };
try {
- const response = await fetch(this.uploadUrl, { method: 'POST', body: formData });
+ const response = await fetch(this.uploadUrl, { method: 'POST', headers, body: formData });
if (response.ok) {
const { error, fileUrl } = await response.json();
if (error) {Why this scored 66/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.