Compare commits
12 Commits
v1.11.7-af
...
v1.2.3
Author | SHA1 | Date | |
---|---|---|---|
ade11faf94 | |||
401fbb6d9c | |||
e34f001bf8 | |||
7384cbd42d | |||
13e6675a23 | |||
cbabff755f | |||
cda46250fb | |||
929b51f814 | |||
c460d9e5c6 | |||
13a4130fde | |||
747e14ca74 | |||
50b297b048 |
@ -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();
|
||||
|
@ -38,6 +38,7 @@ namespace BTCPayServer.Tests
|
||||
|
||||
public async Task StartAsync()
|
||||
{
|
||||
Server.PayTester.NoCSP = true;
|
||||
await Server.StartAsync();
|
||||
|
||||
var windowSize = (Width: 1200, Height: 1000);
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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" />
|
||||
|
@ -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>
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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));
|
||||
|
@ -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 =>
|
||||
{
|
||||
|
@ -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
119
BTCPayServer/TagHelpers.cs
Normal 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))));
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -1,4 +1,4 @@
|
||||
hljs.initHighlightingOnLoad();
|
||||
$(document).ready(function () {
|
||||
$(".richtext").summernote(window.summernoteOptions || {});
|
||||
$(".richtext").summernote(window.summernoteOptions());
|
||||
});
|
||||
|
@ -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')
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -1,3 +1,3 @@
|
||||
$(document).ready(function() {
|
||||
$(".richtext").summernote(window.summernoteOptions || {});
|
||||
$(".richtext").summernote(window.summernoteOptions());
|
||||
});
|
||||
|
@ -1,4 +1,4 @@
|
||||
hljs.initHighlightingOnLoad();
|
||||
$(document).ready(function () {
|
||||
$(".richtext").summernote(window.summernoteOptions || {});
|
||||
$(".richtext").summernote(window.summernoteOptions());
|
||||
});
|
||||
|
@ -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 = [];
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<Version>1.2.2</Version>
|
||||
<Version>1.2.3</Version>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
|
15
Changelog.md
15
Changelog.md
@ -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:
|
||||
|
Reference in New Issue
Block a user