fix: standardize paging in WalletTransactions to use ParseListQuery (#7339)
What changed, and why it matters
This commit changes how the wallet transactions page reads its paging settings (skip/count). Instead of taking them directly from the URL, it routes them through a shared helper called ParseListQuery that also reads and saves user preferences in a cookie. The change is a code-quality fix to make paging behave consistently with other pages. There is no direct evidence in the commit that this fixes an active security vulnerability, but it removes a place where paging parameters were handled differently from the rest of the application.
Treat as a routine refactoring/bug-fix commit. Review whether ParseListQuery enforces acceptable max page sizes and sanitizes paging values consistently; if not, harden that shared helper. No urgent action required based solely on this diff.
Security signals we found
Parameter binding change for paging controls
Standardization onto shared preference/cookie helper
No input validation or authorization changes visible in diff
No explicit security framing in commit title or message
Evidence from the diff
UIWalletsController.WalletTransactions previously bound skip and count directly from query-string parameters. The patch removes those direct parameters, accepts a ListTransactionsViewModel, and calls ParseListQuery to populate Skip/Count from the model/query string and persist preferences via UserPrefsCookie. ControllerBaseExtensions and UserPrefsCookie are updated to support ListTransactionsViewModel. The functional behavior is otherwise unchanged: the same Skip/Count values are passed to FetchTransactionHistory and to in-memory filtering.
Changed components
BTCPayServer/Plugins/Wallets/Controllers/UIWalletsController.csBTCPayServer/Extensions/ControllerBaseExtensions.csBTCPayServer/Extensions/UserPrefsCookie.csInspect captured patch +7 / −5
diff --git a/BTCPayServer/Extensions/ControllerBaseExtensions.cs b/BTCPayServer/Extensions/ControllerBaseExtensions.cs
index 73b3da7..9c4f441 100644
--- a/BTCPayServer/Extensions/ControllerBaseExtensions.cs
+++ b/BTCPayServer/Extensions/ControllerBaseExtensions.cs
@@ -26,6 +26,8 @@ namespace BTCPayServer
prop = typeof(UserPrefsCookie).GetProperty(nameof(UserPrefsCookie.PayoutsQuery));
else if (model is PullPaymentsModel)
prop = typeof(UserPrefsCookie).GetProperty(nameof(UserPrefsCookie.PullPaymentsQuery));
+ else if (model is ListTransactionsViewModel)
+ prop = typeof(UserPrefsCookie).GetProperty(nameof(UserPrefsCookie.WalletTransactionsQuery));
else
throw new Exception("Unsupported BasePagingViewModel for cookie user preferences saving");
diff --git a/BTCPayServer/Extensions/UserPrefsCookie.cs b/BTCPayServer/Extensions/UserPrefsCookie.cs
index 465d289..4cafb30 100644
--- a/BTCPayServer/Extensions/UserPrefsCookie.cs
+++ b/BTCPayServer/Extensions/UserPrefsCookie.cs
@@ -7,6 +7,7 @@ namespace BTCPayServer
public ListQueryDataHolder UsersQuery { get; set; }
public ListQueryDataHolder PayoutsQuery { get; set; }
public ListQueryDataHolder PullPaymentsQuery { get; set; }
+ public ListQueryDataHolder WalletTransactionsQuery { get; set; }
public string CurrentStoreId { get; set; }
}
diff --git a/BTCPayServer/Plugins/Wallets/Controllers/UIWalletsController.cs b/BTCPayServer/Plugins/Wallets/Controllers/UIWalletsController.cs
index 71a1b2b..192c740 100644
--- a/BTCPayServer/Plugins/Wallets/Controllers/UIWalletsController.cs
+++ b/BTCPayServer/Plugins/Wallets/Controllers/UIWalletsController.cs
@@ -580,12 +580,12 @@ namespace BTCPayServer.Controllers
[ModelBinder(typeof(WalletIdModelBinder))]
WalletId walletId,
string? labelFilter = null,
- int skip = 0,
- int count = 50,
bool loadTransactions = false,
+ ListTransactionsViewModel? model = null,
CancellationToken cancellationToken = default
)
{
+ model = this.ParseListQuery(model ?? new ListTransactionsViewModel());
var paymentMethod = GetDerivationSchemeSettings(walletId);
if (paymentMethod == null)
return NotFound();
@@ -594,7 +594,6 @@ namespace BTCPayServer.Controllers
// We can't filter at the database level if we need to apply label filter
var preFiltering = string.IsNullOrEmpty(labelFilter);
- var model = new ListTransactionsViewModel { Skip = skip, Count = count };
const int maxVisibleLabels = 20;
model.PendingTransactions = await pendingTransactionService.GetPendingTransactions(walletId.CryptoCode, walletId.StoreId);
@@ -615,7 +614,7 @@ namespace BTCPayServer.Controllers
Dictionary<string, WalletTransactionInfo>? walletTransactionsInfo = null;
if (loadTransactions)
{
- transactions = await wallet.FetchTransactionHistory(paymentMethod.AccountDerivation, preFiltering ? skip : null, preFiltering ? count : null, cancellationToken: cancellationToken);
+ transactions = await wallet.FetchTransactionHistory(paymentMethod.AccountDerivation, preFiltering ? model.Skip : null, preFiltering ? model.Count : null, cancellationToken: cancellationToken);
walletTransactionsInfo = await WalletRepository.GetWalletTransactionsInfo(walletId, transactions.Select(t => t.TransactionId.ToString()).ToArray());
}
if (labelFilter != null)
@@ -684,7 +683,7 @@ namespace BTCPayServer.Controllers
// if we couldn't filter at the db level, we need to apply skip and count
if (!preFiltering)
{
- model.Transactions = model.Transactions.Skip(skip).Take(count).ToList();
+ model.Transactions = model.Transactions.Skip(model.Skip).Take(model.Count).ToList();
}
}
Why this scored 16/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.