Fix: When a server has too many apps, the Policies page timeout or crash (#7406)
What changed, and why it matters
This commit fixes a performance problem on the BTCPay Server 'Policies' settings page. Previously, the page tried to load a dropdown list of every app on the server, which could time out or crash when there were many apps. The fix replaces the dropdown with a simple text box where the administrator types an app ID directly. It also adds validation to reject invalid app IDs instead of crashing. The change is primarily a reliability/performance fix, but it slightly changes how app IDs are entered and validated.
Review the new text-input flow for usability and ensure the server-side validation covers all paths where AppId is submitted. Consider whether rate limiting or additional authorization checks are needed now that the UI no longer restricts values to existing apps. No urgent security patch appears required, but administrators should verify that only authorized users can access the Server Policies page.
Security signals we found
Change from bounded select list to free-form text input for app identifiers
Added server-side validation (TryGetValue + ModelState.AddModelError) for RootAppId and DomainToAppMapping AppId values
Removed potentially expensive server-side query that loaded all apps into view state
Fixes a denial-of-service-like symptom (page timeout/crash) for servers with many apps
Evidence from the diff
The patch removes server-side population of ViewBag.AppsList (a SelectList of all apps) from UIServerController.UpdateViewBag and deletes the GetAppSelectList helper. The Policies.cshtml view changes RootAppId and DomainToAppMapping[*].AppId from
Changed components
BTCPayServer/Controllers/UIServerController.csBTCPayServer/Views/UIServer/Policies.cshtmlBTCPayServer.Tests/POSTests.csInspect captured patch +21 / −41
diff --git a/BTCPayServer.Tests/POSTests.cs b/BTCPayServer.Tests/POSTests.cs
index dfcd85e..cb556b4 100644
--- a/BTCPayServer.Tests/POSTests.cs
+++ b/BTCPayServer.Tests/POSTests.cs
@@ -1219,16 +1219,8 @@ goodies:
await s.Page.Locator("#RootAppId").WaitForAsync();
await s.Page.Locator("#RootAppId").ScrollIntoViewIfNeededAsync();
- var options = await s.Page.Locator("#RootAppId option").AllTextContentsAsync();
- var targetOption = options.FirstOrDefault(o => o.Contains("Point of"));
- if (targetOption != null)
- {
- await s.Page.Locator("#RootAppId").SelectOptionAsync(new[] { new SelectOptionValue { Label = targetOption } });
- }
- else
- {
- throw new Exception($"Could not find Point of Sale option. Available options: {string.Join(", ", options)}");
- }
+ await s.Page.Locator("#RootAppId").FillAsync(appId);
+
await s.ClickPagePrimary();
await s.FindAlertMessage();
@@ -1253,21 +1245,13 @@ goodies:
// Let's check with domain mapping as well.
await s.GoToUrl(prevUrl);
await s.GoToServer(ServerNavPages.Policies);
- await s.Page.Locator("#RootAppId").SelectOptionAsync("");
+ await s.Page.Locator("#RootAppId").FillAsync("");
await s.ClickPagePrimary();
await s.Page.ClickAsync("#AddDomainButton");
await s.Page.Locator("#DomainToAppMapping_0__Domain").FillAsync(new Uri(s.Page.Url, UriKind.Absolute).DnsSafeHost);
- var domainOptions = await s.Page.Locator("#DomainToAppMapping_0__AppId option").AllTextContentsAsync();
- var targetDomainOption = domainOptions.FirstOrDefault(o => o.Contains("Point of"));
- if (targetDomainOption != null)
- {
- await s.Page.Locator("#DomainToAppMapping_0__AppId").SelectOptionAsync(new[] { new SelectOptionValue { Label = targetDomainOption } });
- }
- else
- {
- throw new Exception($"Could not find Point of Sale option for domain mapping. Available options: {string.Join(", ", domainOptions)}");
- }
+ await s.Page.Locator("#DomainToAppMapping_0__AppId").FillAsync(appId);
+
await s.ClickPagePrimary();
await s.FindAlertMessage(partialText: "Policies updated successfully");
diff --git a/BTCPayServer/Controllers/UIServerController.cs b/BTCPayServer/Controllers/UIServerController.cs
index 3e00383..4ef7f9a 100644
--- a/BTCPayServer/Controllers/UIServerController.cs
+++ b/BTCPayServer/Controllers/UIServerController.cs
@@ -347,7 +347,6 @@ namespace BTCPayServer.Controllers
private async Task UpdateViewBag()
{
ViewBag.UpdateUrlPresent = _Options.UpdateUrl != null;
- ViewBag.AppsList = await GetAppSelectList();
ViewBag.LangTranslations = await GetLangTranslationsSelectList();
}
@@ -423,15 +422,23 @@ namespace BTCPayServer.Controllers
;
if (!string.IsNullOrEmpty(settings.RootAppId))
{
- settings.RootAppType = apps[settings.RootAppId];
+ if (apps.TryGetValue(settings.RootAppId, out var rootAppType))
+ settings.RootAppType = rootAppType;
+ else
+ this.ModelState.AddModelError(nameof(settings.RootAppId), StringLocalizer["Invalid AppId"]);
}
- foreach (var domainToAppMappingItem in settings.DomainToAppMapping)
+ for (int i =0; i < settings.DomainToAppMapping.Count; i++)
{
- domainToAppMappingItem.AppType = apps[domainToAppMappingItem.AppId];
+ var domainToAppMappingItem = settings.DomainToAppMapping[i];
+ if (apps.TryGetValue(domainToAppMappingItem.AppId, out var rootAppType))
+ domainToAppMappingItem.AppType = rootAppType;
+ else
+ this.ModelState.AddModelError($"DomainToAppMapping[{i}].AppId", StringLocalizer["Invalid AppId"]);
}
}
-
+ if (!this.ModelState.IsValid)
+ return View(settings);
await _SettingsRepository.UpdateSetting(settings);
_ = _transactionLinkProviders.RefreshTransactionLinkTemplates();
@@ -495,16 +502,6 @@ namespace BTCPayServer.Controllers
return View(result);
}
- private async Task<List<SelectListItem>> GetAppSelectList()
- {
- var types = _AppService.GetAvailableAppTypes();
- var apps = (await _AppService.GetAllApps(null, true))
- .Select(a =>
- new SelectListItem($"{types[a.AppType]} - {a.AppName} - {a.StoreName}", a.Id)).ToList();
- apps.Insert(0, new SelectListItem("(None)", null));
- return apps;
- }
-
private async Task<List<SelectListItem>> GetLangTranslationsSelectList()
{
var translations = await this._localizer.GetTranslations();
diff --git a/BTCPayServer/Views/UIServer/Policies.cshtml b/BTCPayServer/Views/UIServer/Policies.cshtml
index 74028ae..42a847c 100644
--- a/BTCPayServer/Views/UIServer/Policies.cshtml
+++ b/BTCPayServer/Views/UIServer/Policies.cshtml
@@ -224,7 +224,8 @@
</div>
<div class="form-group mb-5">
<label asp-for="RootAppId" class="form-label"></label>
- <select asp-for="RootAppId" asp-items="@(new SelectList(ViewBag.AppsList, nameof(SelectListItem.Value), nameof(SelectListItem.Text), Model.RootAppId))" class="form-select"></select>
+ <input type="text" class="form-control" asp-for="RootAppId" placeholder="AppId (3ZUJp4LGqRvQP...)" />
+ <span asp-validation-for="RootAppId" class="text-danger d-block"></span>
@if (!Model.DomainToAppMapping.Any())
{
<button id="AddDomainButton" type="submit" name="command" value="add-domain" class="btn btn-link px-0" text-translate="true">Map specific domains to specific apps</button>
@@ -256,10 +257,7 @@
</div>
<div class="form-group">
<label asp-for="DomainToAppMapping[index].AppId" class="form-label"></label>
- <select asp-for="DomainToAppMapping[index].AppId"
- asp-items="@(new SelectList(ViewBag.AppsList, nameof(SelectListItem.Value), nameof(SelectListItem.Text), Model.DomainToAppMapping[index].AppId))"
- class="form-select">
- </select>
+ <input type="text" class="form-control" asp-for="DomainToAppMapping[index].AppId" placeholder="AppId (3ZUJp4LGqRvQP...)" />
<span asp-validation-for="DomainToAppMapping[index].AppId" class="text-danger"></span>
</div>
</div>
diff --git a/Changelog.md b/Changelog.md
index 5c06cba..897eaea 100644
--- a/Changelog.md
+++ b/Changelog.md
@@ -33,6 +33,7 @@
* Refunds and pull payments were unable to make payments from LND 0.21.0 (https://github.com/btcpayserver/BTCPayServer.Lightning/pull/178) @warioishere
* Uninstall button is missing for language packs (#7390 #7392) @teamssUTXO
* Lightning invoice silently dropped when the node doesn't return amount on reconnection (#7402) @atharrva01
+* The Server Policies page would timeout when the server had too many apps (#7406) @NicolasDorier
### Improvements
Why this scored 37/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.