Compare commits

...

26 Commits

Author SHA1 Message Date
02195ed096 bump 2021-09-26 14:32:16 +09:00
562b96cf1f Update Changelog 2021-09-26 14:22:08 +09:00
f44b54bc59 Sanitize UrlRoot in PayButton 2021-09-26 14:06:52 +09:00
1889dec567 Fix pay button CSP issue when using modal (#2872)
* Fix pay button CSP issue when using modal

Fixes #2864.

* Use event handler, refactor csp tags

* Fix script indentation

* Fix onsubmit event handler integration

Co-authored-by: nicolas.dorier <nicolas.dorier@gmail.com>
2021-09-26 14:06:49 +09:00
aa953ad6a6 bump redoc 2021-09-26 14:05:48 +09:00
6a7d434c89 Make CSP more specific for docs 2021-09-26 14:05:48 +09:00
cc812f96c9 Reset "all stores" values to default 2021-09-26 14:03:02 +09:00
6a4cb0e95c Don't adjust store mode if user is performing an action 2021-09-26 14:02:48 +09:00
4c7149de95 Adjust view 2021-09-26 14:02:36 +09:00
532e847cdd [WIP] Fix issues with Authorization Request page
closes #2858
2021-09-26 14:02:17 +09:00
3e3d4aea03 Fix documentation page broken by CSP 2021-09-26 14:01:54 +09:00
55bfecd4ed Attempt cover scenarios of switching back to Bitcoin only after taint (#2881) 2021-09-26 14:01:12 +09:00
2e4d1f6d37 Should not be able to activate a payment method on an invoice which is not new 2021-09-26 14:01:01 +09:00
a3338b6f80 Fix infinite loop happening if payment method unavailable with on invoices with lazy activation (#2914) 2021-09-26 14:00:45 +09:00
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
39 changed files with 592 additions and 293 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);
}
@ -519,9 +505,11 @@ namespace BTCPayServer.Controllers
var paymentMethodDetails = paymentMethod.GetPaymentMethodDetails();
if (!paymentMethodDetails.Activated)
{
await _InvoiceRepository.ActivateInvoicePaymentMethod(_EventAggregator, _NetworkProvider,
_paymentMethodHandlerDictionary, store, invoice, paymentMethod.GetId());
return await GetInvoiceModel(invoiceId, paymentMethodId, lang);
if (await _InvoiceRepository.ActivateInvoicePaymentMethod(_EventAggregator, _NetworkProvider,
_paymentMethodHandlerDictionary, store, invoice, paymentMethod.GetId()))
{
return await GetInvoiceModel(invoiceId, paymentMethodId, lang);
}
}
var dto = invoice.EntityToDTO();
var storeBlob = store.GetStoreBlob();

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

@ -195,7 +195,25 @@ namespace BTCPayServer.Controllers
private void AdjustVMForAuthorization(AuthorizeApiKeysViewModel vm)
{
var parsedPermissions = Permission.ToPermissions(vm.Permissions?.Split(';')??Array.Empty<string>()).GroupBy(permission => permission.Policy);
var permissions = vm.Permissions?.Split(';') ?? Array.Empty<string>();
var permissionsWithStoreIDs = new List<string>();
/**
* Go over each permission and associated store IDs and
* join them so that permission for a specific store is parsed correctly
*/
for (var i = 0; i < permissions.Length; i++) {
var currPerm = permissions[i];
var storeIds = vm.PermissionValues[i].SpecificStores.ToArray();
if (storeIds.Length > 0) {
for (var x = 0; x < storeIds.Length; x++) {
permissionsWithStoreIDs.Add($"{currPerm}:{storeIds[x]}");
}
} else {
permissionsWithStoreIDs.Add(currPerm);
}
}
var parsedPermissions = Permission.ToPermissions(permissionsWithStoreIDs.ToArray()).GroupBy(permission => permission.Policy);
for (var index = vm.PermissionValues.Count - 1; index >= 0; index--)
{
@ -210,6 +228,14 @@ namespace BTCPayServer.Controllers
}
else if (wanted?.Any() ?? false)
{
var commandParts = vm.Command?.Split(':', StringSplitOptions.RemoveEmptyEntries) ?? Array.Empty<string>();
var command = commandParts.Length > 1 ? commandParts[1] : null;
var isPerformingAnAction = command == "change-store-mode" || command == "add-store";
// Don't want to accidentally change mode for the user if they are explicitly performing some action
if (isPerformingAnAction) {
continue;
}
if (vm.SelectiveStores && Policies.IsStorePolicy(permissionValue.Permission) &&
wanted.Any(permission => !string.IsNullOrEmpty(permission.Scope)))
{
@ -358,6 +384,12 @@ namespace BTCPayServer.Controllers
permissionValueItem.StoreMode = permissionValueItem.StoreMode == AddApiKeyViewModel.ApiKeyStoreMode.Specific
? AddApiKeyViewModel.ApiKeyStoreMode.AllStores
: AddApiKeyViewModel.ApiKeyStoreMode.Specific;
// Reset values for "all stores" option to their original values
if (permissionValueItem.StoreMode == AddApiKeyViewModel.ApiKeyStoreMode.AllStores)
{
permissionValueItem.SpecificStores = new List<string>();
permissionValueItem.Value = true;
}
if (permissionValueItem.StoreMode == AddApiKeyViewModel.ApiKeyStoreMode.Specific &&
!permissionValueItem.SpecificStores.Any() && viewModel.Stores.Any())

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

@ -52,7 +52,7 @@ public class BitcoinLikePayoutHandler : IPayoutHandler
public bool CanHandle(PaymentMethodId paymentMethod)
{
return paymentMethod.PaymentType == BitcoinPaymentType.Instance &&
return paymentMethod?.PaymentType == BitcoinPaymentType.Instance &&
_btcPayNetworkProvider.GetNetwork<BTCPayNetwork>(paymentMethod.CryptoCode)?.ReadonlyWallet is false;
}
@ -89,6 +89,10 @@ public class BitcoinLikePayoutHandler : IPayoutHandler
if (payout?.Proof is null)
return null;
var paymentMethodId = payout.GetPaymentMethodId();
if (paymentMethodId is null)
{
return null;
}
var raw = JObject.Parse(Encoding.UTF8.GetString(payout.Proof));
if (raw.TryGetValue("proofType", StringComparison.InvariantCultureIgnoreCase, out var proofType) &&
proofType.Value<string>() == ManualPayoutProof.Type)
@ -161,7 +165,9 @@ public class BitcoinLikePayoutHandler : IPayoutHandler
.Include(p => p.PullPaymentData.StoreData)
.Where(p => payoutIds.Contains(p.Id))
.Where(p => p.PullPaymentData.StoreId == storeId && !p.PullPaymentData.Archived && p.State == PayoutState.AwaitingPayment)
.ToListAsync()).Where(data => CanHandle(PaymentMethodId.Parse(data.PaymentMethodId)))
.ToListAsync()).Where(data =>
PaymentMethodId.TryParse(data.PaymentMethodId, out var paymentMethodId) &&
CanHandle(paymentMethodId))
.Select(data => (data, ParseProof(data) as PayoutTransactionOnChainBlob)).Where(tuple=> tuple.Item2 != null && tuple.Item2.TransactionId != null && tuple.Item2.Accounted == false);
foreach (var valueTuple in payouts)
{
@ -185,7 +191,9 @@ public class BitcoinLikePayoutHandler : IPayoutHandler
.Include(p => p.PullPaymentData.StoreData)
.Where(p => payoutIds.Contains(p.Id))
.Where(p => p.PullPaymentData.StoreId == storeId && !p.PullPaymentData.Archived && p.State == PayoutState.AwaitingPayment)
.ToListAsync()).Where(data => CanHandle(PaymentMethodId.Parse(data.PaymentMethodId)))
.ToListAsync()).Where(data =>
PaymentMethodId.TryParse(data.PaymentMethodId, out var paymentMethodId) &&
CanHandle(paymentMethodId))
.Select(data => (data, ParseProof(data) as PayoutTransactionOnChainBlob)).Where(tuple=> tuple.Item2 != null && tuple.Item2.TransactionId != null && tuple.Item2.Accounted == true);
foreach (var valueTuple in payouts)
{

View File

@ -28,7 +28,7 @@ namespace BTCPayServer.Data
public static PaymentMethodId GetPaymentMethodId(this PayoutData data)
{
return PaymentMethodId.Parse(data.PaymentMethodId);
return PaymentMethodId.TryParse(data.PaymentMethodId, out var paymentMethodId)? paymentMethodId : null;
}
public static PayoutBlob GetBlob(this PayoutData data, BTCPayNetworkJsonSerializerSettings serializers)
{

View File

@ -171,7 +171,8 @@ namespace BTCPayServer.Data
#pragma warning disable CS0618 // Type or member is obsolete
if (ExcludedPaymentMethods == null || ExcludedPaymentMethods.Length == 0)
return PaymentFilter.Never();
return PaymentFilter.Any(ExcludedPaymentMethods.Select(p => PaymentFilter.WhereIs(PaymentMethodId.Parse(p))).ToArray());
return PaymentFilter.Any(ExcludedPaymentMethods.ParsePaymentMethodIds().Select(PaymentFilter.WhereIs).ToArray());
#pragma warning restore CS0618 // Type or member is obsolete
}

View File

@ -25,7 +25,8 @@ namespace BTCPayServer.Data
public static PaymentMethodId[] GetEnabledPaymentIds(this StoreData storeData, BTCPayNetworkProvider networks)
{
var excludeFilter = storeData.GetStoreBlob().GetExcludedPaymentMethods();
var paymentMethodIds = storeData.GetSupportedPaymentMethods(networks).Select(p => p.PaymentId)
var paymentMethodIds = storeData.GetSupportedPaymentMethods(networks)
.Select(p => p.PaymentId)
.Where(a => !excludeFilter.Match(a))
.OrderByDescending(a => a.CryptoCode == "BTC")
.ThenBy(a => a.CryptoCode)

View File

@ -1,4 +1,5 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
@ -482,6 +483,15 @@ namespace BTCPayServer
"Supported chains: " + String.Join(',', supportedChains.ToArray()));
return result;
}
public static PaymentMethodId[] ParsePaymentMethodIds(this string[] paymentMethods)
{
return paymentMethods.Select(s =>
{
PaymentMethodId.TryParse(s, out var parsed);
return parsed;
}).Where(id => id != null).ToArray();
}
public static DataDirectories Configure(this DataDirectories dataDirectories, IConfiguration configuration)
{

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

@ -286,8 +286,13 @@ namespace BTCPayServer.HostedServices
req.Completion.SetResult(PayoutApproval.Result.OldRevision);
return;
}
if (!PaymentMethodId.TryParse(payout.PaymentMethodId, out var paymentMethod))
{
req.Completion.SetResult(PayoutApproval.Result.NotFound);
return;
}
payout.State = PayoutState.AwaitingPayment;
var paymentMethod = PaymentMethodId.Parse(payout.PaymentMethodId);
if (paymentMethod.CryptoCode == payout.PullPaymentData.GetBlob().Currency)
req.Rate = 1.0m;
var cryptoAmount = payoutBlob.Amount / req.Rate;

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

@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using NBitcoin.Crypto;
namespace BTCPayServer.Security
{
@ -9,6 +10,8 @@ namespace BTCPayServer.Security
{
public ConsentSecurityPolicy(string name, string value)
{
if (value.Contains(';', StringComparison.OrdinalIgnoreCase))
throw new FormatException();
_Value = value;
_Name = name;
}
@ -67,10 +70,41 @@ namespace BTCPayServer.Security
}
readonly HashSet<ConsentSecurityPolicy> _Policies = new HashSet<ConsentSecurityPolicy>();
/// <summary>
/// Allow a specific script as event handler
/// </summary>
/// <param name="script"></param>
public void AllowUnsafeHashes(string script)
{
if (script is null)
throw new ArgumentNullException(nameof(script));
var sha = GetSha256(script);
Add("script-src", $"'unsafe-hashes'");
Add("script-src", $"'sha256-{sha}'");
}
/// <summary>
/// Allow the injection of script tag with the following script
/// </summary>
/// <param name="script"></param>
public void AllowInline(string script)
{
if (script is null)
throw new ArgumentNullException(nameof(script));
var sha = GetSha256(script);
Add("script-src", $"'sha256-{sha}'");
}
static string GetSha256(string script)
{
return Convert.ToBase64String(Hashes.SHA256(Encoding.UTF8.GetBytes(script.Replace("\r\n", "\n", StringComparison.Ordinal))));
}
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 +121,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;
}
}

View File

@ -289,7 +289,10 @@ namespace BTCPayServer.Services.Invoices
JObject strategies = JObject.Parse(DerivationStrategies);
foreach (var strat in strategies.Properties())
{
var paymentMethodId = PaymentMethodId.Parse(strat.Name);
if (!PaymentMethodId.TryParse(strat.Name, out var paymentMethodId))
{
continue;
}
var network = Networks.GetNetwork<BTCPayNetworkBase>(paymentMethodId.CryptoCode);
if (network != null)
{

View File

@ -10,11 +10,13 @@ namespace BTCPayServer.Services.Invoices
{
public static class InvoiceExtensions
{
public static async Task ActivateInvoicePaymentMethod(this InvoiceRepository invoiceRepository,
public static async Task<bool> ActivateInvoicePaymentMethod(this InvoiceRepository invoiceRepository,
EventAggregator eventAggregator, BTCPayNetworkProvider btcPayNetworkProvider, PaymentMethodHandlerDictionary paymentMethodHandlerDictionary,
StoreData store,InvoiceEntity invoice, PaymentMethodId paymentMethodId)
{
if (invoice.GetInvoiceState().Status != InvoiceStatusLegacy.New)
return false;
bool success = false;
var eligibleMethodToActivate = invoice.GetPaymentMethod(paymentMethodId);
if (!eligibleMethodToActivate.GetPaymentMethodDetails().Activated)
{
@ -34,6 +36,8 @@ namespace BTCPayServer.Services.Invoices
eligibleMethodToActivate.SetPaymentMethodDetails(newDetails);
await invoiceRepository.UpdateInvoicePaymentMethod(invoice.Id, eligibleMethodToActivate);
eventAggregator.Publish(new InvoicePaymentMethodActivated(paymentMethodId, invoice));
eventAggregator.Publish(new InvoiceNeedUpdateEvent(invoice.Id));
success = true;
}
catch (PaymentMethodUnavailableException ex)
{
@ -45,8 +49,8 @@ namespace BTCPayServer.Services.Invoices
}
await invoiceRepository.AddInvoiceLogs(invoice.Id, logs);
eventAggregator.Publish(new InvoiceNeedUpdateEvent(invoice.Id));
}
return success;
}
}

144
BTCPayServer/TagHelpers.cs Normal file
View File

@ -0,0 +1,144 @@
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")]
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))
{
_csp.AllowUnsafeHashes(attr.Value.ToString());
}
}
}
}
/// <summary>
/// Add sha256- to allow inline event handlers in CSP
/// </summary>
[HtmlTargetElement("template", Attributes = "csp-allow")]
public class CSPTemplate : TagHelper
{
private readonly ContentSecurityPolicies _csp;
public CSPTemplate(ContentSecurityPolicies csp)
{
_csp = csp;
}
public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
{
output.Attributes.RemoveAll("csp-allow");
var childContent = await output.GetChildContentAsync();
var content = childContent.GetContent();
_csp.AllowInline(content);
}
}
/// <summary>
/// Add sha256- to allow inline event handlers in a:href=javascript:
/// </summary>
[HtmlTargetElement("a", Attributes = "csp-allow")]
public class CSPA : TagHelper
{
private readonly ContentSecurityPolicies _csp;
public CSPA(ContentSecurityPolicies csp)
{
_csp = csp;
}
public override void Process(TagHelperContext context, TagHelperOutput output)
{
output.Attributes.RemoveAll("csp-allow");
if (output.Attributes.TryGetAttribute("href", out var attr))
{
var v = attr.Value.ToString();
if (v.StartsWith("javascript:", StringComparison.OrdinalIgnoreCase))
{
_csp.AllowInline(v);
}
}
}
}
}

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,5 +1,8 @@
@inject BTCPayServer.Security.ContentSecurityPolicies csp
@{
Layout = null;
csp.Add("script-src", "https://cdn.jsdelivr.net");
csp.Add("worker-src", "blob:");
}
<!DOCTYPE html>
<html>
@ -25,6 +28,6 @@
@*Ignore this, this is for making the test ClickOnAllSideMenus happy*@
<div class="navbar-brand" style="visibility:collapse;"></div>
<redoc spec-url="@Url.ActionLink("Swagger")"></redoc>
<script src="https://cdn.jsdelivr.net/npm/redoc@2.0.0-rc.45/bundles/redoc.standalone.js" integrity="sha384-RC31+q3tyqdcilXYaU++ii/FAByqeZ+sjKUHMJ8hMzIY5k4kzNqi4Ett88EZ/4lq" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/redoc@2.0.0-rc.54/bundles/redoc.standalone.js" integrity="sha384-pxWFJkxrlfignEDb+sJ8XrdnJQ+V2bsiRqgPnfmOk1i3KKSubbydbolVZJeKisNY" crossorigin="anonymous"></script>
</body>
</html>

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

@ -64,7 +64,7 @@
@if (Model.PermissionValues[i].StoreMode == ManageController.AddApiKeyViewModel.ApiKeyStoreMode.AllStores)
{
<div class="list-group-item form-group">
<div class="form-check">
<div class="form-check d-flex">
@if (Model.Strict || Model.PermissionValues[i].Forbidden)
{
<input id="@Model.PermissionValues[i].Permission" type="hidden" asp-for="PermissionValues[i].Value"/>
@ -74,10 +74,10 @@
{
<input id="@Model.PermissionValues[i].Permission" type="checkbox" asp-for="PermissionValues[i].Value" class="form-check-input"/>
}
<label for="@Model.PermissionValues[i].Permission" class="h5 form-check-label">@Model.PermissionValues[i].Title</label>
<label for="@Model.PermissionValues[i].Permission" class="h5 form-check-label m-0 me-4 ms-2">@Model.PermissionValues[i].Title</label>
@if (Model.SelectiveStores)
{
<button type="submit" class="btn btn-link" name="command" value="@($"{Model.PermissionValues[i].Permission}:change-store-mode")">select specific stores...</button>
<button type="submit" class="btn btn-link p-0 me-4" name="command" value="@($"{Model.PermissionValues[i].Permission}:change-store-mode")">select specific stores...</button>
}
@if (Model.PermissionValues[i].Forbidden)

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,7 +1,9 @@
@inject BTCPayServer.Security.ContentSecurityPolicies csp
@model PayButtonViewModel
@{
Layout = "../Shared/_NavLayout.cshtml";
ViewData.SetActivePageAndTitle(StoreNavPages.PayButton, "Pay Button", Context.GetStoreData().StoreName);
csp.AllowUnsafeHashes("onBTCPayFormSubmit(event);return false");
}
@section PageHeadContent {
@ -14,6 +16,24 @@
<script src="~/vendor/vuejs-vee-validate/vee-validate.js" asp-append-version="true"></script>
<script src="~/vendor/clipboard.js/clipboard.js" asp-append-version="true"></script>
<script src="~/paybutton/paybutton.js" asp-append-version="true"></script>
<template id="template-get-scripts" csp-allow>
if (!window.btcpay) {
var script = document.createElement('script');
script.src=@(Safe.Json(Model.UrlRoot + "modal/btcpay.js"));
document.getElementsByTagName('head')[0].append(script);
}
function onBTCPayFormSubmit(event) {
event.preventDefault();
var xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function() {
if (this.readyState == 4 && this.status == 200 && this.responseText) {
window.btcpay.showInvoice(JSON.parse(this.responseText).invoiceId);
}
};
xhttp.open('POST', event.target.getAttribute('action'), true);
xhttp.send(new FormData(event.target));
}
</template>
<script>
var srvModel = @Safe.Json(Model);

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

@ -42,33 +42,11 @@ function getStyles (styles) {
}
function getScripts(srvModel) {
return ""+
"<script>" +
"if(!window.btcpay){ " +
" var head = document.getElementsByTagName('head')[0];" +
" var script = document.createElement('script');" +
" script.src='"+esc(srvModel.urlRoot)+"modal/btcpay.js';" +
" script.type = 'text/javascript';" +
" head.append(script);" +
"}" +
"function onBTCPayFormSubmit(event){" +
" var xhttp = new XMLHttpRequest();" +
" xhttp.onreadystatechange = function() {" +
" if (this.readyState == 4 && this.status == 200) {" +
" if(this.status == 200 && this.responseText){" +
" var response = JSON.parse(this.responseText);" +
" window.btcpay.showInvoice(response.invoiceId);" +
" }" +
" }" +
" };" +
" xhttp.open(\"POST\", event.target.getAttribute('action'), true);" +
" xhttp.send(new FormData( event.target ));" +
"}" +
"</script>";
if (!srvModel.useModal) return ''
const template = document.getElementById('template-get-scripts')
return template.innerHTML.replace(/&amp;/g, '&')
}
function inputChanges(event, buttonSize) {
if (buttonSize !== null && buttonSize !== undefined) {
srvModel.buttonSize = buttonSize;
@ -115,12 +93,10 @@ function inputChanges(event, buttonSize) {
}
var html =
//Scripts
(srvModel.useModal? getScripts(srvModel) :"") +
// Styles
getStyles('template-paybutton-styles') + (isSlider ? getStyles('template-slider-styles') : '') +
// Form
'<form method="POST" '+ ( srvModel.useModal? ' onsubmit="onBTCPayFormSubmit(event);return false" ' : '' )+' action="' + esc(srvModel.urlRoot) + actionUrl + '" class="btcpay-form btcpay-form--' + (srvModel.fitButtonInline ? 'inline' : 'block') +'">\n' +
'<form method="POST"' + (srvModel.useModal ? ' onsubmit="onBTCPayFormSubmit(event);return false"' : '') + ' action="' + esc(srvModel.urlRoot) + actionUrl + '" class="btcpay-form btcpay-form--' + (srvModel.fitButtonInline ? 'inline' : 'block') +'">\n' +
addInput("storeId", srvModel.storeId);
if(app){
@ -144,7 +120,6 @@ function inputChanges(event, buttonSize) {
if (srvModel.checkoutQueryString) html += addInput("checkoutQueryString", srvModel.checkoutQueryString);
}
// Fixed amount: Add price and currency as hidden inputs
if (isFixedAmount) {
@ -192,10 +167,21 @@ function inputChanges(event, buttonSize) {
'</button>'
}
html += '</form>';
// Scripts
var scripts = getScripts(srvModel);
var code = html + (scripts ? `\n<script>\n ${scripts.trim()}\n</script>` : '')
$("#mainCode").text(html).html();
$("#preview").html(html);
var form = document.querySelector("#preview form");
$("#mainCode").text(code).html();
var preview = document.getElementById('preview');
preview.innerHTML = html;
if (scripts) {
// script needs to be inserted as node, otherwise it won't get executed
var script = document.createElement('script');
script.innerHTML = scripts
preview.appendChild(script)
}
var form = preview.querySelector("form");
var url = new URL(form.getAttribute("action"));
var formData = new FormData(form);
formData.forEach((value, key) => {

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.4</Version>
</PropertyGroup>
</Project>

View File

@ -1,5 +1,31 @@
# Changelog
## 1.2.4
Minor bug fixes release, update recommended for shared hosting.
### Bug fixes
* If `Only enable the payment method after user explicitly chooses it` is enabled for a store and a payment method is unavailable, the server could become unresponsive. @NicolasDorier
* Authorize API key page was broken when trying to select specific stores (#2858) @ubolator
* The /docs page was broken in 1.2.3 due to CSP @NicolasDorier
* Fixing crashes happening when someone migrate from BTCPay Server altcoins edition back to bitcoin only @Kukks
## 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: