Compare commits

...

12 Commits

Author SHA1 Message Date
ade11faf94 Fix build 2021-09-10 00:08:38 +09:00
401fbb6d9c Update Changelog 2021-09-09 23:55:20 +09:00
e34f001bf8 Fix Summernote XSS possibility (#2859) 2021-09-09 23:53:52 +09:00
7384cbd42d The page could crash if the user clicks too many time on Notificate 'Mark as Seen' 2021-09-09 23:53:26 +09:00
13e6675a23 Fix CSP when there is a theme 2021-09-09 23:22:49 +09:00
cbabff755f bump 2021-09-09 22:13:01 +09:00
cda46250fb update HWI lib 2021-09-09 22:11:42 +09:00
929b51f814 Changelog 2021-09-09 22:11:21 +09:00
c460d9e5c6 Do not generate payment methods when 0 amount invoice (#2776)
* Do not generate payment methods when 0 amount invoice

* Add test for 0 amoutn invoices
2021-09-09 21:59:53 +09:00
13a4130fde fix broken plugin page 2021-09-09 21:59:36 +09:00
747e14ca74 Fix for crowdfund perk editor
With no title entered, the editor got stuck in an endless loop, because it recursively invoked the save function without checking for the ID fallback being present.

Fixes #2862.
2021-09-09 21:59:28 +09:00
50b297b048 Add CSP at the website level (#2863) 2021-09-09 21:59:14 +09:00
26 changed files with 400 additions and 242 deletions

View File

@ -134,7 +134,7 @@ namespace BTCPayServer.Tests
config.AppendLine($"torrcfile={TestUtils.GetTestDataFullPath("Tor/torrc")}");
config.AppendLine($"socksendpoint={SocksEndpoint}");
config.AppendLine($"debuglog=debug.log");
config.AppendLine($"nocsp={NoCSP.ToString().ToLowerInvariant()}");
if (!string.IsNullOrEmpty(SSHPassword) && string.IsNullOrEmpty(SSHKeyFile))
config.AppendLine($"sshpassword={SSHPassword}");
@ -283,6 +283,8 @@ namespace BTCPayServer.Tests
public string SSHPassword { get; internal set; }
public string SSHKeyFile { get; internal set; }
public string SSHConnection { get; set; }
public bool NoCSP { get; set; }
public T GetController<T>(string userId = null, string storeId = null, bool isAdmin = false) where T : Controller
{
var context = new DefaultHttpContext();

View File

@ -38,6 +38,7 @@ namespace BTCPayServer.Tests
public async Task StartAsync()
{
Server.PayTester.NoCSP = true;
await Server.StartAsync();
var windowSize = (Width: 1200, Height: 1000);

View File

@ -2904,6 +2904,21 @@ namespace BTCPayServer.Tests
});
Assert.Equal(50.51m, invoice5g.Amount);
Assert.Equal(50.51m, (decimal)invoice5g.Metadata["taxIncluded"]);
var zeroInvoice = await greenfield.CreateInvoice(user.StoreId, new CreateInvoiceRequest()
{
Amount = 0m,
Currency = "USD"
});
Assert.Equal(InvoiceStatus.New, zeroInvoice.Status);
await TestUtils.EventuallyAsync(async () =>
{
zeroInvoice = await greenfield.GetInvoice(user.StoreId, zeroInvoice.Id);
Assert.Equal(InvoiceStatus.Settled, zeroInvoice.Status);
});
var zeroInvoicePM = await greenfield.GetInvoicePaymentMethods(user.StoreId, zeroInvoice.Id);
Assert.Empty(zeroInvoicePM);
}
}

View File

@ -1,4 +1,4 @@
 <Project Sdk="Microsoft.NET.Sdk.Web">
<Project Sdk="Microsoft.NET.Sdk.Web">
<Import Project="../Build/Version.csproj" Condition="Exists('../Build/Version.csproj')" />
<Import Project="../Build/Common.csproj" />
@ -45,7 +45,7 @@
<ItemGroup>
<PackageReference Include="BIP78.Sender" Version="0.2.2" />
<PackageReference Include="BTCPayServer.Hwi" Version="2.0.1" />
<PackageReference Include="BTCPayServer.Hwi" Version="2.0.2" />
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.2.12" />
<PackageReference Include="BuildBundlerMinifier" Version="3.2.449" />
<PackageReference Include="BundlerMinifier.Core" Version="3.2.435" />

View File

@ -1,4 +1,3 @@
@inject LinkGenerator linkGenerator
@inject UserManager<ApplicationUser> UserManager
@inject ISettingsRepository SettingsRepository
@using BTCPayServer.HostedServices
@ -56,47 +55,3 @@ else
</a>
</li>
}
@{
var disabled = (await SettingsRepository.GetPolicies()).DisableInstantNotifications;
if (!disabled)
{
var user = await UserManager.GetUserAsync(User);
disabled = user?.DisabledNotifications == "all";
}
}
@if (!disabled)
{
<script type="text/javascript">
var supportsWebSockets = 'WebSocket' in window && window.WebSocket.CLOSING === 2;
if (supportsWebSockets) {
var loc = window.location, ws_uri;
if (loc.protocol === "https:") {
ws_uri = "wss:";
} else {
ws_uri = "ws:";
}
ws_uri += "//" + loc.host;
ws_uri += "@linkGenerator.GetPathByAction("SubscribeUpdates", "Notifications")";
var newDataEndpoint = "@linkGenerator.GetPathByAction("GetNotificationDropdownUI", "Notifications")";
try {
socket = new WebSocket(ws_uri);
socket.onmessage = function (e) {
$.get(newDataEndpoint, function(data){
$("#notifications-nav-item").replaceWith($(data));
});
};
socket.onerror = function (e) {
console.error("Error while connecting to websocket for notifications (callback)", e);
};
}
catch (e) {
console.error("Error while connecting to websocket for notifications", e);
}
}
</script>
}

View File

@ -28,6 +28,7 @@ namespace BTCPayServer.Configuration
app.Option("--chains | -c", $"Chains to support as a comma separated (default: btc; available: {chains})", CommandOptionType.SingleValue);
app.Option("--postgres", $"Connection string to a PostgreSQL database", CommandOptionType.SingleValue);
app.Option("--mysql", $"Connection string to a MySQL database", CommandOptionType.SingleValue);
app.Option("--nocsp", $"Disable CSP (default false)", CommandOptionType.BoolValue);
app.Option("--sqlitefile", $"File name to an SQLite database file inside the data directory", CommandOptionType.SingleValue);
app.Option("--externalservices", $"Links added to external services inside Server Settings / Services under the format service1:path2;service2:path2.(default: empty)", CommandOptionType.SingleValue);
app.Option("--bundlejscss", $"Bundle JavaScript and CSS files for better performance (default: true)", CommandOptionType.SingleValue);

View File

@ -448,20 +448,6 @@ namespace BTCPayServer.Controllers
if (view == "modal")
model.IsModal = true;
_CSP.Add(new ConsentSecurityPolicy("script-src", "'unsafe-eval'")); // Needed by Vue
if (!string.IsNullOrEmpty(model.CustomCSSLink) &&
Uri.TryCreate(model.CustomCSSLink, UriKind.Absolute, out var uri))
{
_CSP.Clear();
}
if (!string.IsNullOrEmpty(model.CustomLogoLink) &&
Uri.TryCreate(model.CustomLogoLink, UriKind.Absolute, out uri))
{
_CSP.Clear();
}
return View(nameof(Checkout), model);
}

View File

@ -32,7 +32,6 @@ namespace BTCPayServer.Controllers
public partial class InvoiceController : Controller
{
readonly InvoiceRepository _InvoiceRepository;
readonly ContentSecurityPolicies _CSP;
readonly RateFetcher _RateProvider;
readonly StoreRepository _StoreRepository;
readonly UserManager<ApplicationUser> _UserManager;
@ -72,7 +71,6 @@ namespace BTCPayServer.Controllers
_dbContextFactory = dbContextFactory;
_paymentHostedService = paymentHostedService;
WebhookNotificationManager = webhookNotificationManager;
_CSP = csp;
_languageService = languageService;
}
@ -257,39 +255,48 @@ namespace BTCPayServer.Controllers
List<ISupportedPaymentMethod> supported = new List<ISupportedPaymentMethod>();
var paymentMethods = new PaymentMethodDictionary();
// This loop ends with .ToList so we are querying all payment methods at once
// instead of sequentially to improve response time
foreach (var o in store.GetSupportedPaymentMethods(_NetworkProvider)
.Where(s => !excludeFilter.Match(s.PaymentId) && _paymentMethodHandlerDictionary.Support(s.PaymentId))
.Select(c =>
(Handler: _paymentMethodHandlerDictionary[c.PaymentId],
SupportedPaymentMethod: c,
Network: _NetworkProvider.GetNetwork<BTCPayNetworkBase>(c.PaymentId.CryptoCode)))
.Where(c => c.Network != null)
.Select(o =>
(SupportedPaymentMethod: o.SupportedPaymentMethod,
PaymentMethod: CreatePaymentMethodAsync(fetchingByCurrencyPair, o.Handler, o.SupportedPaymentMethod, o.Network, entity, store, logs)))
.ToList())
{
var paymentMethod = await o.PaymentMethod;
if (paymentMethod == null)
continue;
supported.Add(o.SupportedPaymentMethod);
paymentMethods.Add(paymentMethod);
}
bool noNeedForMethods = entity.Type != InvoiceType.TopUp && entity.Price == 0m;
if (supported.Count == 0)
if (!noNeedForMethods)
{
StringBuilder errors = new StringBuilder();
if (!store.GetSupportedPaymentMethods(_NetworkProvider).Any())
errors.AppendLine("Warning: No wallet has been linked to your BTCPay Store. See the following link for more information on how to connect your store and wallet. (https://docs.btcpayserver.org/WalletSetup/)");
foreach (var error in logs.ToList())
// This loop ends with .ToList so we are querying all payment methods at once
// instead of sequentially to improve response time
foreach (var o in store.GetSupportedPaymentMethods(_NetworkProvider)
.Where(s => !excludeFilter.Match(s.PaymentId) &&
_paymentMethodHandlerDictionary.Support(s.PaymentId))
.Select(c =>
(Handler: _paymentMethodHandlerDictionary[c.PaymentId],
SupportedPaymentMethod: c,
Network: _NetworkProvider.GetNetwork<BTCPayNetworkBase>(c.PaymentId.CryptoCode)))
.Where(c => c.Network != null)
.Select(o =>
(SupportedPaymentMethod: o.SupportedPaymentMethod,
PaymentMethod: CreatePaymentMethodAsync(fetchingByCurrencyPair, o.Handler,
o.SupportedPaymentMethod, o.Network, entity, store, logs)))
.ToList())
{
errors.AppendLine(error.ToString());
var paymentMethod = await o.PaymentMethod;
if (paymentMethod == null)
continue;
supported.Add(o.SupportedPaymentMethod);
paymentMethods.Add(paymentMethod);
}
throw new BitpayHttpException(400, errors.ToString());
}
if (supported.Count == 0)
{
StringBuilder errors = new StringBuilder();
if (!store.GetSupportedPaymentMethods(_NetworkProvider).Any())
errors.AppendLine(
"Warning: No wallet has been linked to your BTCPay Store. See the following link for more information on how to connect your store and wallet. (https://docs.btcpayserver.org/WalletSetup/)");
foreach (var error in logs.ToList())
{
errors.AppendLine(error.ToString());
}
throw new BitpayHttpException(400, errors.ToString());
}
}
entity.SetSupportedPaymentMethods(supported);
entity.SetPaymentMethods(paymentMethods);
foreach (var app in await getAppsTaggingStore)

View File

@ -364,7 +364,7 @@ askdevice:
private static bool IsTrezorT(HwiEnumerateEntry deviceEntry)
{
return (deviceEntry.Model == HardwareWalletModels.Trezor_T || deviceEntry.Model == HardwareWalletModels.Trezor_T_Simulator);
return deviceEntry.Model.Contains("Trezor_T", StringComparison.OrdinalIgnoreCase);
}
public StoreData CurrentStore

View File

@ -7,13 +7,32 @@ using Microsoft.AspNetCore.Mvc.Filters;
namespace BTCPayServer.Filters
{
public interface IContentSecurityPolicy : IFilterMetadata { }
public enum CSPTemplate
{
AntiXSS
}
public class ContentSecurityPolicyAttribute : Attribute, IActionFilter, IContentSecurityPolicy
{
public ContentSecurityPolicyAttribute()
{
}
public ContentSecurityPolicyAttribute(CSPTemplate template)
{
if (template == CSPTemplate.AntiXSS)
{
AutoSelf = false;
FixWebsocket = false;
UnsafeInline = false;
ScriptSrc = "'self' 'unsafe-eval'"; // unsafe-eval needed for vue
}
}
public void OnActionExecuted(ActionExecutedContext context)
{
}
public bool Enabled { get; set; } = true;
public bool AutoSelf { get; set; } = true;
public bool UnsafeInline { get; set; } = true;
public bool FixWebsocket { get; set; } = true;
@ -22,83 +41,79 @@ namespace BTCPayServer.Filters
public string DefaultSrc { get; set; }
public string StyleSrc { get; set; }
public string ScriptSrc { get; set; }
public string ManifestSrc { get; set; }
public void OnActionExecuting(ActionExecutingContext context)
{
if (context.IsEffectivePolicy<IContentSecurityPolicy>(this))
if (!context.IsEffectivePolicy<IContentSecurityPolicy>(this) || !Enabled)
return;
var policies = context.HttpContext.RequestServices.GetService(typeof(ContentSecurityPolicies)) as ContentSecurityPolicies;
if (policies == null)
return;
if (DefaultSrc != null)
{
var policies = context.HttpContext.RequestServices.GetService(typeof(ContentSecurityPolicies)) as ContentSecurityPolicies;
if (policies == null)
return;
if (DefaultSrc != null)
{
policies.Add(new ConsentSecurityPolicy("default-src", DefaultSrc));
}
if (UnsafeInline)
{
policies.Add(new ConsentSecurityPolicy("script-src", "'unsafe-inline'"));
}
if (!string.IsNullOrEmpty(FontSrc))
{
policies.Add(new ConsentSecurityPolicy("font-src", FontSrc));
}
policies.Add(new ConsentSecurityPolicy("default-src", DefaultSrc));
}
if (UnsafeInline)
{
policies.Add(new ConsentSecurityPolicy("script-src", "'unsafe-inline'"));
}
if (!string.IsNullOrEmpty(FontSrc))
{
policies.Add(new ConsentSecurityPolicy("font-src", FontSrc));
}
if (!string.IsNullOrEmpty(ManifestSrc))
{
policies.Add(new ConsentSecurityPolicy("manifest-src", FontSrc));
}
if (!string.IsNullOrEmpty(ImgSrc))
{
policies.Add(new ConsentSecurityPolicy("img-src", ImgSrc));
}
if (!string.IsNullOrEmpty(ImgSrc))
{
policies.Add(new ConsentSecurityPolicy("img-src", ImgSrc));
}
if (!string.IsNullOrEmpty(StyleSrc))
{
policies.Add(new ConsentSecurityPolicy("style-src", StyleSrc));
}
if (!string.IsNullOrEmpty(StyleSrc))
{
policies.Add(new ConsentSecurityPolicy("style-src", StyleSrc));
}
if (!string.IsNullOrEmpty(ScriptSrc))
{
policies.Add(new ConsentSecurityPolicy("script-src", ScriptSrc));
}
if (!string.IsNullOrEmpty(ScriptSrc))
{
policies.Add(new ConsentSecurityPolicy("script-src", ScriptSrc));
}
if (FixWebsocket && AutoSelf) // Self does not match wss:// and ws:// :(
{
var request = context.HttpContext.Request;
if (FixWebsocket && AutoSelf) // Self does not match wss:// and ws:// :(
{
var request = context.HttpContext.Request;
var url = string.Concat(
request.Scheme.Equals("http", StringComparison.OrdinalIgnoreCase) ? "ws" : "wss",
"://",
request.Host.ToUriComponent(),
request.PathBase.ToUriComponent());
policies.Add(new ConsentSecurityPolicy("connect-src", url));
}
var url = string.Concat(
request.Scheme.Equals("http", StringComparison.OrdinalIgnoreCase) ? "ws" : "wss",
"://",
request.Host.ToUriComponent(),
request.PathBase.ToUriComponent());
policies.Add(new ConsentSecurityPolicy("connect-src", url));
}
context.HttpContext.Response.OnStarting(() =>
context.HttpContext.Response.OnStarting(() =>
{
if (!policies.HasRules)
return Task.CompletedTask;
if (AutoSelf)
{
if (!policies.HasRules)
return Task.CompletedTask;
if (AutoSelf)
bool hasSelf = false;
foreach (var group in policies.Rules.GroupBy(p => p.Name))
{
bool hasSelf = false;
foreach (var group in policies.Rules.GroupBy(p => p.Name))
hasSelf = group.Any(g => g.Value.Contains("'self'", StringComparison.OrdinalIgnoreCase));
if (!hasSelf && !group.Any(g => g.Value.Contains("'none'", StringComparison.OrdinalIgnoreCase) ||
g.Value.Contains("*", StringComparison.OrdinalIgnoreCase)))
{
hasSelf = group.Any(g => g.Value.Contains("'self'", StringComparison.OrdinalIgnoreCase));
if (!hasSelf && !group.Any(g => g.Value.Contains("'none'", StringComparison.OrdinalIgnoreCase) ||
g.Value.Contains("*", StringComparison.OrdinalIgnoreCase)))
{
policies.Add(new ConsentSecurityPolicy(group.Key, "'self'"));
hasSelf = true;
}
if (hasSelf)
{
foreach (var authorized in policies.Authorized)
{
policies.Add(new ConsentSecurityPolicy(group.Key, authorized));
}
}
policies.Add(new ConsentSecurityPolicy(group.Key, "'self'"));
}
}
context.HttpContext.Response.SetHeader("Content-Security-Policy", policies.ToString());
return Task.CompletedTask;
});
}
}
context.HttpContext.Response.SetHeader("Content-Security-Policy", policies.ToString());
return Task.CompletedTask;
});
}
}
}

View File

@ -22,22 +22,6 @@ namespace BTCPayServer.HostedServices
if (policies != null)
{
var theme = settingsRepository.GetTheme().GetAwaiter().GetResult();
if (theme.CreativeStartCssUri != null && Uri.TryCreate(theme.CreativeStartCssUri, UriKind.Absolute, out var uri))
{
policies.Clear();
}
if (theme.BootstrapCssUri != null && Uri.TryCreate(theme.BootstrapCssUri, UriKind.Absolute, out uri))
{
policies.Clear();
}
if (theme.ThemeCssUri != null && Uri.TryCreate(theme.ThemeCssUri, UriKind.Absolute, out uri))
{
policies.Clear();
}
if (theme.CustomThemeCssUri != null && Uri.TryCreate(theme.CustomThemeCssUri, UriKind.Absolute, out uri))
{
policies.Clear();
}
}
}
}

View File

@ -86,8 +86,24 @@ namespace BTCPayServer.HostedServices
}
var allPaymentMethods = invoice.GetPaymentMethods();
var paymentMethod = GetNearestClearedPayment(allPaymentMethods, out var accounting);
if (paymentMethod == null)
if (allPaymentMethods.Any() && paymentMethod == null)
return;
if (accounting is null && invoice.Price is 0m)
{
accounting = new PaymentMethodAccounting()
{
Due = Money.Zero,
Paid = Money.Zero,
CryptoPaid = Money.Zero,
DueUncapped = Money.Zero,
NetworkFee = Money.Zero,
TotalDue = Money.Zero,
TxCount = 0,
TxRequired = 0,
MinimumTotalDue = Money.Zero,
NetworkFeeAlreadyPaid = Money.Zero
};
}
if (invoice.Status == InvoiceStatusLegacy.New || invoice.Status == InvoiceStatusLegacy.Expired)
{
var isPaid = invoice.IsUnsetTopUp() ?
@ -153,7 +169,9 @@ namespace BTCPayServer.HostedServices
if (invoice.Status == InvoiceStatusLegacy.Paid)
{
var confirmedAccounting = paymentMethod.Calculate(p => p.GetCryptoPaymentData().PaymentConfirmed(p, invoice.SpeedPolicy));
var confirmedAccounting =
paymentMethod?.Calculate(p => p.GetCryptoPaymentData().PaymentConfirmed(p, invoice.SpeedPolicy)) ??
accounting;
if (// Is after the monitoring deadline
(invoice.MonitoringExpiration < DateTimeOffset.UtcNow)
@ -177,7 +195,8 @@ namespace BTCPayServer.HostedServices
if (invoice.Status == InvoiceStatusLegacy.Confirmed)
{
var completedAccounting = paymentMethod.Calculate(p => p.GetCryptoPaymentData().PaymentCompleted(p));
var completedAccounting = paymentMethod?.Calculate(p => p.GetCryptoPaymentData().PaymentCompleted(p)) ??
accounting;
if (completedAccounting.Paid >= accounting.MinimumTotalDue)
{
context.Events.Add(new InvoiceEvent(invoice, InvoiceEvent.Completed));

View File

@ -114,14 +114,8 @@ namespace BTCPayServer.Hosting
o.Filters.Add(new XXSSProtectionAttribute());
o.Filters.Add(new ReferrerPolicyAttribute("same-origin"));
o.ModelBinderProviders.Insert(0, new ModelBinders.DefaultModelBinderProvider());
//o.Filters.Add(new ContentSecurityPolicyAttribute()
//{
// FontSrc = "'self' https://fonts.gstatic.com/",
// ImgSrc = "'self' data:",
// DefaultSrc = "'none'",
// StyleSrc = "'self' 'unsafe-inline'",
// ScriptSrc = "'self' 'unsafe-inline'"
//});
if (!Configuration.GetOrDefault<bool>("nocsp", false))
o.Filters.Add(new ContentSecurityPolicyAttribute(CSPTemplate.AntiXSS));
})
.ConfigureApiBehaviorOptions(options =>
{

View File

@ -9,6 +9,8 @@ namespace BTCPayServer.Security
{
public ConsentSecurityPolicy(string name, string value)
{
if (value.Contains(';', StringComparison.OrdinalIgnoreCase))
throw new FormatException();
_Value = value;
_Name = name;
}
@ -67,10 +69,12 @@ namespace BTCPayServer.Security
}
readonly HashSet<ConsentSecurityPolicy> _Policies = new HashSet<ConsentSecurityPolicy>();
public void Add(string name, string value)
{
Add(new ConsentSecurityPolicy(name, value));
}
public void Add(ConsentSecurityPolicy policy)
{
if (_Policies.Any(p => p.Name == policy.Name && p.Value == policy.Name))
return;
_Policies.Add(policy);
}
@ -87,34 +91,19 @@ namespace BTCPayServer.Security
{
value.Append(';');
}
List<string> values = new List<string>();
HashSet<string> values = new HashSet<string>();
List<string> valuesList = new List<string>();
values.Add(group.Key);
valuesList.Add(group.Key);
foreach (var v in group)
{
values.Add(v.Value);
if (values.Add(v.Value))
valuesList.Add(v.Value);
}
foreach (var i in authorized)
{
values.Add(i);
}
value.Append(String.Join(" ", values.OfType<object>().ToArray()));
value.Append(String.Join(" ", valuesList.OfType<object>().ToArray()));
firstGroup = false;
}
return value.ToString();
}
internal void Clear()
{
authorized.Clear();
_Policies.Clear();
}
readonly HashSet<string> authorized = new HashSet<string>();
internal void AddAllAuthorized(string v)
{
authorized.Add(v);
}
public IEnumerable<string> Authorized => authorized;
}
}

119
BTCPayServer/TagHelpers.cs Normal file
View File

@ -0,0 +1,119 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using BTCPayServer.Security;
using BTCPayServer.Services;
using Microsoft.AspNetCore.Razor.TagHelpers;
using NBitcoin;
using NBitcoin.Crypto;
namespace BTCPayServer.TagHelpers
{
[HtmlTargetElement("srv-model")]
public class SrvModel : TagHelper
{
private readonly Safe _safe;
private readonly ContentSecurityPolicies _csp;
public SrvModel(Safe safe, ContentSecurityPolicies csp)
{
_safe = safe;
_csp = csp;
}
public string VarName { get; set; } = "srvModel";
public object Model { get; set; }
public override void Process(TagHelperContext context, TagHelperOutput output)
{
output.TagName = "script";
output.TagMode = TagMode.StartTagAndEndTag;
output.Attributes.Add(new TagHelperAttribute("type", "text/javascript"));
var nonce = RandomUtils.GetUInt256().ToString().Substring(0, 32);
output.Attributes.Add(new TagHelperAttribute("nonce", nonce));
_csp.Add("script-src", $"'nonce-{nonce}'");
output.Content.SetHtmlContent($"var {VarName} = {_safe.Json(Model)};");
}
}
/// <summary>
/// Add a nonce-* so the inline-script can pass CSP rule when they are rendered server-side
/// </summary>
[HtmlTargetElement("script")]
public class CSPInlineScriptTagHelper : TagHelper
{
private readonly ContentSecurityPolicies _csp;
public CSPInlineScriptTagHelper(ContentSecurityPolicies csp)
{
_csp = csp;
}
public override void Process(TagHelperContext context, TagHelperOutput output)
{
if (output.Attributes.ContainsName("src"))
return;
if (output.Attributes.TryGetAttribute("type", out var attr))
{
if (attr.Value?.ToString() != "text/javascript")
return;
}
var nonce = RandomUtils.GetUInt256().ToString().Substring(0, 32);
output.Attributes.Add(new TagHelperAttribute("nonce", nonce));
_csp.Add("script-src", $"'nonce-{nonce}'");
}
}
/// <summary>
/// Add 'unsafe-hashes' and sha256- to allow inline event handlers in CSP
/// </summary>
[HtmlTargetElement(Attributes = "onclick")]
[HtmlTargetElement(Attributes = "onkeypress")]
[HtmlTargetElement(Attributes = "onchange")]
[HtmlTargetElement(Attributes = "onsubmit")]
[HtmlTargetElement(Attributes = "href")]
public class CSPEventTagHelper : TagHelper
{
public const string EventNames = "onclick,onkeypress,onchange,onsubmit";
private readonly ContentSecurityPolicies _csp;
readonly static HashSet<string> EventSet = EventNames.Split(',')
.ToHashSet();
public CSPEventTagHelper(ContentSecurityPolicies csp)
{
_csp = csp;
}
public override void Process(TagHelperContext context, TagHelperOutput output)
{
foreach (var attr in output.Attributes)
{
var n = attr.Name.ToLowerInvariant();
if (EventSet.Contains(n))
{
Allow(attr.Value.ToString());
}
else if (n == "href")
{
var v = attr.Value.ToString();
if (v.StartsWith("javascript:", StringComparison.OrdinalIgnoreCase))
{
Allow(v);
}
}
}
}
private void Allow(string v)
{
var sha = GetSha256(v);
_csp.Add("script-src", $"'unsafe-hashes'");
_csp.Add("script-src", $"'sha256-{sha}'");
}
public static string GetSha256(string script)
{
return Convert.ToBase64String(Hashes.SHA256(Encoding.UTF8.GetBytes(script.Replace("\r\n", "\n", StringComparison.Ordinal))));
}
}
}

View File

@ -340,8 +340,9 @@ document.addEventListener("DOMContentLoaded", function () {
return this.errors.length === 0;
},
saveEditingItem: function(){
if(!this.editingItem.id){
this.editingItem.id = this.editingItem.title.toLowerCase().trim();
const fallbackId = this.editingItem.title.toLowerCase().trim();
if(!this.editingItem.id && fallbackId){
this.editingItem.id = fallbackId;
this.$nextTick(this.saveEditingItem.bind(this));
return;
}

View File

@ -1,41 +1,41 @@
@model (Dictionary<string, object> Items, int Level)
@functions{
void DisplayValue(object value)
{
if (value is string str && str.StartsWith("http"))
{
<a href="@str" target="_blank" rel="noreferrer noopener">@str</a>
}
else
{
@value
}
}
}
<table class="table table-sm table-responsive-md removetopborder">
@foreach (var (key, value) in Model.Items)
{
<tr>
@if (value is string)
@if (value is string str)
{
if (!string.IsNullOrEmpty(key))
{
<th class="w-150px">@key</th>
}
<td>
@{ DisplayValue(value); }
@if (str.StartsWith("http"))
{
<a href="@str" target="_blank" rel="noreferrer noopener">@str</a>
}
else
{
@value
}
</td>
}
else if (value is Dictionary<string, object>subItems)
{
@* This is the array case *@
if (subItems.Count == 1 && subItems.First().Value is string)
if (subItems.Count == 1 && subItems.First().Value is string str2)
{
<th class="w-150px">@key</th>
<td>
@{ DisplayValue(subItems.First().Value); }
@if (str2.StartsWith("http"))
{
<a href="@str2" target="_blank" rel="noreferrer noopener">@str2</a>
}
else
{
@subItems.First().Value
}
</td>
}
else

View File

@ -5,7 +5,10 @@
@{
ViewData.SetActivePageAndTitle(ServerNavPages.Plugins);
var installed = Model.Installed.ToDictionary(plugin => plugin.Identifier.ToLowerInvariant(), plugin => plugin.Version);
var availableAndNotInstalled = Model.Available.Where(plugin => !installed.ContainsKey(plugin.Identifier.ToLowerInvariant())).Select(plugin => (plugin, BTCPayServerOptions.RecommendedPlugins.Contains(plugin.Identifier.ToLowerInvariant()))).OrderBy(tuple => tuple.Item1);
var availableAndNotInstalled = Model.Available
.Where(plugin => !installed.ContainsKey(plugin.Identifier.ToLowerInvariant()))
.OrderBy(plugin => plugin.Identifier)
.ToList();
bool DependentOn(string plugin)
{
@ -236,16 +239,17 @@
{
<h2 class="mb-4">Available Plugins</h2>
<div class="row mb-4">
@foreach (var pluginT in availableAndNotInstalled)
@foreach (var plugin in availableAndNotInstalled)
{
var plugin = pluginT.Item1;
var recommended = BTCPayServerOptions.RecommendedPlugins.Contains(plugin.Identifier.ToLowerInvariant());
<div class="col col-12 col-lg-6 mb-4">
<div class="card h-100">
<div class="card-body">
<h4 class="card-title d-inline-block" data-bs-toggle="tooltip" title="@plugin.Identifier">@plugin.Name</h4>
<h5 class="card-subtitle mb-3 text-muted d-flex align-items-center">
@plugin.Version
@if (pluginT.Item2)
@if (recommended)
{
<div class="badge bg-light ms-2 text-nowrap" data-bs-toggle="tooltip" title="This plugin has been recommended to be installed by your deployment method.">Recommended <span class="fa fa-question-circle-o text-secondary"></span></div>
}

View File

@ -12,6 +12,7 @@
@inject RoleManager<IdentityRole> RoleManager
@inject BTCPayServer.Services.BTCPayServerEnvironment Env
@inject ISettingsRepository SettingsRepository
@inject LinkGenerator linkGenerator
<!DOCTYPE html>
<html lang="en"@(Env.IsDeveloping ? " data-devenv" : "")>
@ -122,9 +123,19 @@
}
<partial name="LayoutFoot" />
@await RenderSectionAsync("PageFootContent", false)
<partial name="LayoutPartials/SyncModal" />
@{
var notificationDisabled = (await SettingsRepository.GetPolicies()).DisableInstantNotifications;
if (!notificationDisabled)
{
var user = await UserManager.GetUserAsync(User);
notificationDisabled = user?.DisabledNotifications == "all";
}
}
<script type="text/javascript">
const expectedDomain = @Safe.Json(Env.ExpectedHost);
const expectedProtocol = @Safe.Json(Env.ExpectedProtocol);
@ -133,5 +144,38 @@
document.getElementById("browserScheme").innerText = window.location.protocol.substr(0, window.location.protocol.length -1);
}
</script>
@if (!notificationDisabled)
{
<script>
var supportsWebSockets = 'WebSocket' in window && window.WebSocket.CLOSING === 2;
if (supportsWebSockets) {
var loc = window.location, ws_uri;
if (loc.protocol === "https:") {
ws_uri = "wss:";
} else {
ws_uri = "ws:";
}
ws_uri += "//" + loc.host;
ws_uri += "@linkGenerator.GetPathByAction("SubscribeUpdates", "Notifications")";
var newDataEndpoint = "@linkGenerator.GetPathByAction("GetNotificationDropdownUI", "Notifications")";
try {
socket = new WebSocket(ws_uri);
socket.onmessage = function (e) {
$.get(newDataEndpoint, function (data) {
$("#notifications-nav-item").replaceWith($(data));
});
};
socket.onerror = function (e) {
console.error("Error while connecting to websocket for notifications (callback)", e);
};
}
catch (e) {
console.error("Error while connecting to websocket for notifications", e);
}
}
</script>
}
</body>
</html>

View File

@ -1,4 +1,4 @@
hljs.initHighlightingOnLoad();
$(document).ready(function () {
$(".richtext").summernote(window.summernoteOptions || {});
$(".richtext").summernote(window.summernoteOptions());
});

View File

@ -1,8 +1,12 @@
window.summernoteOptions = {
minHeight: 300,
tableClassName: 'table table-sm',
insertTableMaxSize: {
col: 5,
row: 10
window.summernoteOptions = function() {
return {
minHeight: 300,
tableClassName: 'table table-sm',
insertTableMaxSize: {
col: 5,
row: 10
},
codeviewFilter: true,
codeviewFilterRegex: new RegExp($.summernote.options.codeviewFilterRegex.source + '|<.*?( on\\w+?=.*?)>', 'gi')
}
};
}

View File

@ -1,3 +1,3 @@
$(document).ready(function() {
$(".richtext").summernote(window.summernoteOptions || {});
$(".richtext").summernote(window.summernoteOptions());
});

View File

@ -1,4 +1,4 @@
hljs.initHighlightingOnLoad();
$(document).ready(function () {
$(".richtext").summernote(window.summernoteOptions || {});
$(".richtext").summernote(window.summernoteOptions());
});

View File

@ -5366,6 +5366,9 @@ var Editor_Editor = /*#__PURE__*/function () {
// if url doesn't have any protocol and not even a relative or a label, use http:// as default
linkUrl = /^([A-Za-z][A-Za-z0-9+-.]*\:|#|\/)/.test(linkUrl) ? linkUrl : _this.options.defaultProtocol + linkUrl;
}
linkUrl = linkUrl.replace(this.options.codeviewFilterRegex, '');
linkText = linkText.replace(this.options.codeviewFilterRegex, '');
var anchors = [];

View File

@ -1,5 +1,5 @@
<Project>
<PropertyGroup>
<Version>1.2.2</Version>
<Version>1.2.3</Version>
</PropertyGroup>
</Project>

View File

@ -1,5 +1,20 @@
# Changelog
## 1.2.3
This release fixes three XSS vulnerabilities. Those vulnerabilities only impacts shared BTCPay instances.
Special thanks to Ajmal "@b3ef" Aboobacker and Abdul "@b1nslashsh" muhaimin for finding them who contacted us through @huntrdev.
See [1](https://huntr.dev/bounties/ffabdac8-7280-4806-b70c-9b0d1aafbb6e/), [2](https://www.huntr.dev/bounties/32e30ecf-31fa-45f6-8552-47250ef0e613/) and [3](https://huntr.dev/bounties/0fcdee5f-1f07-47ce-b650-ea8b4a7d35d8/).
# Bug fixes:
* Use CSP to prevent future XSS vulnerabilities. (#2856, #2863) @NicolasDorier
* Fix XSS vulnerabilities in summernote, the rich text editor (#2859) @dennisreimann
* Fix plugins page crashing @Kukks
* Fix page crash of the perk editor in the crowdfund settings when the title is not set @dennisreimann
* Do not generate payment methods when 0 amount invoice (#2776)
* When using the BTCPay Vault, some hardware wallet types were considered unknown @NicolasDorier
## 1.2.2
# Bug fixes: