CVE-2023-1149: Improper Neutralization of Equivalent Special Elements (CWE-76)
value: Improper Neutralization of Equivalent Special Elements in GitHub repository btcpayserver/btcpayserver prior to 1.8.0.
CWE description: The product correctly neutralizes certain special elements, but it improperly neutralizes equivalent special elements.
CVSS v3 base score: 5.4
Published: 2023-03-02T05:15Z
Repository: https://github.com/btcpayserver/btcpayserver
Language: Other
Files changed: BTCPayServer/Views/Shared/Bitcoin/BitcoinLikeMethodCheckoutNoScript.cshtml, BTCPayServer/Views/Shared/Bitcoin/ViewBitcoinLikePaymentData.cshtml, BTCPayServer/Views/Shared/PosData.cshtml, BTCPayServer/Views/UILNURL/EditLightningAddress.cshtml
Commit date: 2023-02-22 16:35:34 +0100
Commit message: Fix: HTML injection in payment request/posData/receiptData (Close #4678) (#4679)

Co-authored-by: Dennis Reimann <mail@dennisreimann.de>

Diff: diff --git a/BTCPayServer/Views/Shared/Bitcoin/BitcoinLikeMethodCheckoutNoScript.cshtml b/BTCPayServer/Views/Shared/Bitcoin/BitcoinLikeMethodCheckoutNoScript.cshtml
index a374d5cd3..abea18980 100644
--- a/BTCPayServer/Views/Shared/Bitcoin/BitcoinLikeMethodCheckoutNoScript.cshtml
+++ b/BTCPayServer/Views/Shared/Bitcoin/BitcoinLikeMethodCheckoutNoScript.cshtml
@@ -1,8 +1,8 @@
 @model PaymentModel
 <div>
-    <p>To complete payment, please send <b>@Safe.Raw(Model.IsUnsetTopUp? "any amount of": Model.BtcDue) @Model.CryptoCode</b> to <b style="word-break: break-word;">@Model.BtcAddress</b></p>
+    <p>To complete payment, please send <b>@(Model.IsUnsetTopUp ? "any amount of" : Model.BtcDue) @Model.CryptoCode</b> to <b style="word-break: break-word;">@Model.BtcAddress</b></p>
     <p>Time remaining: @Model.TimeLeft</p>
     <p>
         <a href="@Model.InvoiceBitcoinUrl" style="word-break: break-word;" rel="noreferrer noopener">@Model.InvoiceBitcoinUrl</a>
     </p>
 </div>
diff --git a/BTCPayServer/Views/Shared/Bitcoin/ViewBitcoinLikePaymentData.cshtml b/BTCPayServer/Views/Shared/Bitcoin/ViewBitcoinLikePaymentData.cshtml
index 59b1a7b86..3a7f2ff6d 100644
--- a/BTCPayServer/Views/Shared/Bitcoin/ViewBitcoinLikePaymentData.cshtml
+++ b/BTCPayServer/Views/Shared/Bitcoin/ViewBitcoinLikePaymentData.cshtml
@@ -75,11 +75,17 @@
             {
                 <tr style="@(payment.Replaced ? "text-decoration: line-through" : "")">
                     <td>@payment.Crypto</td>
                     <td>@(payment.CryptoPaymentData.KeyPath?.ToString()?? "Unknown")</td>
                     <td style="max-width:300px;" data-bs-toggle="tooltip" class="text-truncate" title="@payment.DepositAddress">@payment.DepositAddress</td>
-                    <td class="payment-value">@payment.CryptoPaymentData.GetValue() @Safe.Raw(payment.AdditionalInformation is string i ? $"<br/>({i})" : string.Empty)</td>
+                    <td class="payment-value">
+                        @payment.CryptoPaymentData.GetValue()
+                        @if (!string.IsNullOrEmpty(payment.AdditionalInformation))
+                        {
+                            <div>(@payment.AdditionalInformation)</div>
+                        }
+                    </td>
                     @if (hasNetworkFee)
                     {
                         <td>@payment.NetworkFee</td>
                     }
                     <td style="max-width:200px;" data-bs-toggle="tooltip" class="text-truncate" title="@payment.TransactionId">
diff --git a/BTCPayServer/Views/Shared/PosData.cshtml b/BTCPayServer/Views/Shared/PosData.cshtml
index 14c84f3bf..b57385823 100644
--- a/BTCPayServer/Views/Shared/PosData.cshtml
+++ b/BTCPayServer/Views/Shared/PosData.cshtml
@@ -14,44 +14,48 @@
         <tr>
             @if (value is string str)
             {
                 if (!string.IsNullOrEmpty(key))
                 {
-                    <th class="w-150px">@Safe.Raw(key)</th>
+                    <th class="w-150px">@key</th>
                 }
                 <td>
                     @if (IsValidURL(str))
                     {
-                        <a href="@Safe.Raw(str)" target="_blank" rel="noreferrer noopener">@Safe.Raw(str)</a>
+                        <a href="@str" target="_blank" rel="noreferrer noopener">@str</a>
                     }
                     else
                     {
-                        @Safe.Raw(value?.ToString())
+                        @value?.ToString()
                     }
                 </td>
             }
             else if (value is Dictionary<string, object>subItems)
             {
                 @* This is the array case *@
                 if (subItems.Count == 1 && subItems.First().Value is string str2)
                 {
-                    <th class="w-150px">@Safe.Raw(key)</th>
+                    <th class="w-150px">@key</th>
                     <td>
                         @if (IsValidURL(str2))
                         {
-                            <a href="@Safe.Raw(str2)" target="_blank" rel="noreferrer noopener">@Safe.Raw(str2)</a>
+                            <a href="@str2" target="_blank" rel="noreferrer noopener">@str2</a>
                         }
                         else
                         {
-                            @Safe.Raw(subItems.First().Value?.ToString())
+                            @subItems.First().Value?.ToString()
                         }
-                    </td>
-                }
-                else
-                {
-                    <td colspan="2" >
-                        @Safe.Raw($"<h{Model.Level + 3} class=\"mt-4 mb-3\">{key}</h{Model.Level + 3}>")
+					</td>
+				}
+				else
+				{
+					<td colspan="2" >
+						@{
+							Write(Html.Raw($"<h{Model.Level + 3} class=\"mt-4 mb-3\">"));
+							Write(key);
+							Write(Html.Raw($"</h{Model.Level + 3}>"));
+						}
                         <partial name="PosData" model="(subItems, Model.Level + 1)"/>
                     </td>
                 }
             }
         </tr>
diff --git a/BTCPayServer/Views/UIInvoice/PosData.cshtml b/BTCPayServer/Views/UIInvoice/PosData.cshtml
deleted file mode 100644
index 14c84f3bf..000000000
--- a/BTCPayServer/Views/UIInvoice/PosData.cshtml
+++ /dev/null
@@ -1,60 +0,0 @@
-@model (Dictionary<string, object> Items, int Level)
-
-@functions {
-    private bool IsValidURL(string source)
-    {
-        return Uri.TryCreate(source, UriKind.Absolute, out var uriResult) && 
-            (uriResult.Scheme == Uri.UriSchemeHttp || uriResult.Scheme == Uri.UriSchemeHttps);
-    }
-}
-
-<table class="table my-0">
-    @foreach (var (key, value) in Model.Items)
-    {
-        <tr>
-            @if (value is string str)
-            {
-                if (!string.IsNullOrEmpty(key))
-                {
-                    <th class="w-150px">@Safe.Raw(key)</th>
-                }
-                <td>
-                    @if (IsValidURL(str))
-                    {
-                        <a href="@Safe.Raw(str)" target="_blank" rel="noreferrer noopener">@Safe.Raw(str)</a>
-                    }
-                    else
-                    {
-                        @Safe.Raw(value?.ToString())
-                    }
-                </td>
-            }
-            else if (value is Dictionary<string, object>subItems)
-            {
-                @* This is the array case *@
-                if (subItems.Count == 1 && subItems.First().Value is string str2)
-                {
-                    <th class="w-150px">@Safe.Raw(key)</th>
-                    <td>
-                        @if (IsValidURL(str2))
-                        {
-                            <a href="@Safe.Raw(str2)" target="_blank" rel="noreferrer noopener">@Safe.Raw(str2)</a>
-                        }
-                        else
-                        {
-                            @Safe.Raw(subItems.First().Value?.ToString())
-                        }
-                    </td>
-                }
-                else
-                {
-                    <td colspan="2" >
-                        @Safe.Raw($"<h{Model.Level + 3} class=\"mt-4 mb-3\">{key}</h{Model.Level + 3}>")
-                        <partial name="PosData" model="(subItems, Model.Level + 1)"/>
-                    </td>
-                }
-            }
-        </tr>
-    }
-</table>
-
diff --git a/BTCPayServer/Views/UILNURL/EditLightningAddress.cshtml b/BTCPayServer/Views/UILNURL/EditLightningAddress.cshtml
index daa0d5b87..4070dfa74 100644
--- a/BTCPayServer/Views/UILNURL/EditLightningAddress.cshtml
+++ b/BTCPayServer/Views/UILNURL/EditLightningAddress.cshtml
@@ -67,11 +67,11 @@
         <div id="AdvancedSettings" class="collapse @(showAdvancedOptions ? "show" : "")">
             <div class="row">
                 <div class="col-12 col-sm-auto">
                     <div class="form-group">
                         <label asp-for="Add.CurrencyCode" class="form-label"></label>
-                        <input asp-for="Add.CurrencyCode" class="form-control" style="max-width:16ch;"/>
+                        <input asp-for="Add.CurrencyCode" class="form-control w-auto" currency-selection style="max-width:16ch;"/>
                         <span asp-validation-for="Add.CurrencyCode" class="text-danger"></span>
                     </div>
                 </div>
                 <div class="col-12 col-sm-auto">
                     <div class="form-group">
@@ -124,19 +124,19 @@
 
                             </td>
                             <td class="settings-holder align-middle">
                                 @if (Model.Items[index].Min.HasValue)
                                 {
-                                    <span>@Safe.Raw($"{Model.Items[index].Min} min sats")</span>
+                                    <span>@Model.Items[index].Min min sats</span>
                                 }
                                 @if (Model.Items[index].Max.HasValue)
                                 {
-                                    <span> @Safe.Raw($"{Model.Items[index].Max} max sats")</span>
+                                    <span>@Model.Items[index].Max max sats</span>
                                 }
                                 @if (!string.IsNullOrEmpty(Model.Items[index].CurrencyCode))
                                 {
-                                    <span> @Safe.Raw($"tracked in {Model.Items[index].CurrencyCode}")</span>
+                                    <span>tracked in @Model.Items[index].CurrencyCode</span>
                                 }
                             </td>
                             <td class="text-end">
                                 <button type="submit" title="Remove" name="command" value="@($"remove:{Model.Items[index].Username}")"
 				class="btn btn-link px-0 remove" data-bs-toggle="modal" data-bs-target="#ConfirmModal" data-description="The Lightning Address <strong>@Html.Encode(address)</strong> will be removed." data-confirm-input="REMOVE">

Vulnerable code:     <p>To complete payment, please send <b>@Safe.Raw(Model.IsUnsetTopUp? "any amount of": Model.BtcDue) @Model.CryptoCode</b> to <b style="word-break: break-word;">@Model.BtcAddress</b></p>
                    <td class="payment-value">@payment.CryptoPaymentData.GetValue() @Safe.Raw(payment.AdditionalInformation is string i ? $"<br/>({i})" : string.Empty)</td>
                    <th class="w-150px">@Safe.Raw(key)</th>
                        <a href="@Safe.Raw(str)" target="_blank" rel="noreferrer noopener">@Safe.Raw(str)</a>
                        @Safe.Raw(value?.ToString())
                    <th class="w-150px">@Safe.Raw(key)</th>
                            <a href="@Safe.Raw(str2)" target="_blank" rel="noreferrer noopener">@Safe.Raw(str2)</a>
                            @Safe.Raw(subItems.First().Value?.ToString())
                    </td>
                }
                else
                {
                    <td colspan="2" >
                        @Safe.Raw($"<h{Model.Level + 3} class=\"mt-4 mb-3\">{key}</h{Model.Level + 3}>")
@model (Dictionary<string, object> Items, int Level)

@functions {
    private bool IsValidURL(string source)
    {
        return Uri.TryCreate(source, UriKind.Absolute, out var uriResult) && 
            (uriResult.Scheme == Uri.UriSchemeHttp || uriResult.Scheme == Uri.UriSchemeHttps);
    }
}

<table class="table my-0">
    @foreach (var (key, value) in Model.Items)
    {
        <tr>
            @if (value is string str)
            {
                if (!string.IsNullOrEmpty(key))
                {
                    <th class="w-150px">@Safe.Raw(key)</th>
                }
                <td>
                    @if (IsValidURL(str))
                    {
                        <a href="@Safe.Raw(str)" target="_blank" rel="noreferrer noopener">@Safe.Raw(str)</a>
                    }
                    else
                    {
                        @Safe.Raw(value?.ToString())
                    }
                </td>
            }
            else if (value is Dictionary<string, object>subItems)
            {
                @* This is the array case *@
                if (subItems.Count == 1 && subItems.First().Value is string str2)
                {
                    <th class="w-150px">@Safe.Raw(key)</th>
                    <td>
                        @if (IsValidURL(str2))
                        {
                            <a href="@Safe.Raw(str2)" target="_blank" rel="noreferrer noopener">@Safe.Raw(str2)</a>
                        }
                        else
                        {
                            @Safe.Raw(subItems.First().Value?.ToString())
                        }
                    </td>
                }
                else
                {
                    <td colspan="2" >
                        @Safe.Raw($"<h{Model.Level + 3} class=\"mt-4 mb-3\">{key}</h{Model.Level + 3}>")
                        <partial name="PosData" model="(subItems, Model.Level + 1)"/>
                    </td>
                }
            }
        </tr>
    }
</table>

                        <input asp-for="Add.CurrencyCode" class="form-control" style="max-width:16ch;"/>
                                    <span>@Safe.Raw($"{Model.Items[index].Min} min sats")</span>
                                    <span> @Safe.Raw($"{Model.Items[index].Max} max sats")</span>
                                    <span> @Safe.Raw($"tracked in {Model.Items[index].CurrencyCode}")</span>
Fixed code:     <p>To complete payment, please send <b>@(Model.IsUnsetTopUp ? "any amount of" : Model.BtcDue) @Model.CryptoCode</b> to <b style="word-break: break-word;">@Model.BtcAddress</b></p>
                    <td class="payment-value">
                        @payment.CryptoPaymentData.GetValue()
                        @if (!string.IsNullOrEmpty(payment.AdditionalInformation))
                        {
                            <div>(@payment.AdditionalInformation)</div>
                        }
                    </td>
                    <th class="w-150px">@key</th>
                        <a href="@str" target="_blank" rel="noreferrer noopener">@str</a>
                        @value?.ToString()
                    <th class="w-150px">@key</th>
                            <a href="@str2" target="_blank" rel="noreferrer noopener">@str2</a>
                            @subItems.First().Value?.ToString()
					</td>
				}
				else
				{
					<td colspan="2" >
						@{
							Write(Html.Raw($"<h{Model.Level + 3} class=\"mt-4 mb-3\">"));
							Write(key);
							Write(Html.Raw($"</h{Model.Level + 3}>"));
						}
                        <input asp-for="Add.CurrencyCode" class="form-control w-auto" currency-selection style="max-width:16ch;"/>
                                    <span>@Model.Items[index].Min min sats</span>
                                    <span>@Model.Items[index].Max max sats</span>
                                    <span>tracked in @Model.Items[index].CurrencyCode</span>