Encode payjoin errors in wallet status messages (#7567)
What changed, and why it matters
This commit fixes a potential cross-site scripting (XSS) issue in BTCPay Server's wallet status messages. When a payjoin transaction fails, the server shows a warning message that includes an error string. Previously, that error string was inserted directly into the HTML of the status message, so if an attacker could influence the error text to contain malicious code (for example, through a script tag), it could run in the administrator's browser. The change now encodes the error text so that any HTML-like characters are displayed as plain text instead of being interpreted as code.
Review whether other status messages in the same controller or elsewhere concatenate raw strings into Html properties. Ensure all dynamic content inserted into StatusMessageModel.Html is encoded unless it is explicitly known-safe HTML. Consider adding a code-analysis rule or helper that requires encoding for Html status messages.
Security signals we found
HTML content constructed from an external error string without encoding
Addition of HtmlEncoder.Default.Encode around user-influenced or third-party error text
Status message rendered as raw Html in the UI
Payjoin error path where attacker-controlled or relayed error content may reach the admin UI
Evidence from the diff
In UIWalletsController.PSBT.cs, the WalletPSBTReady action constructs a StatusMessageModel with an Html property for payjoin failures. The original code concatenated the raw error variable into the HTML string. The patch imports System.Text.Encodings.Web and wraps error with HtmlEncoder.Default.Encode(error), which HTML-encodes special characters. This is a defensive fix against reflected/stored XSS via the payjoin error message content.
Changed components
BTCPayServer/Plugins/Wallets/Controllers/UIWalletsController.PSBT.csWalletPSBTReady actionPayjoin failure status message renderingInspect captured patch +2 / −1
### BTCPayServer/Plugins/Wallets/Controllers/UIWalletsController.PSBT.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using System.Text.Encodings.Web;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Constants;
@@ -544,7 +545,7 @@ public async Task<IActionResult> WalletPSBTReady(
{
Severity = StatusMessageModel.StatusSeverity.Warning,
AllowDismiss = false,
- Html = $"The payjoin transaction could not be created.<br/>The original transaction was broadcasted instead ({psbt.ExtractTransaction().GetHash()})<br/><br/>" + error
+ Html = $"The payjoin transaction could not be created.<br/>The original transaction was broadcasted instead ({psbt.ExtractTransaction().GetHash()})<br/><br/>" + HtmlEncoder.Default.Encode(error)
});
return await WalletPSBTReady(walletId, vm, "broadcast");
case "broadcast" when !psbt.IsAllFinalized() && !psbt.TryFinalize(out var errors):Why this scored 36/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.