mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2025-03-03 09:29:10 +01:00
Add CSP (Disable it if custom theming)
This commit is contained in:
parent
6ea2d9175d
commit
976d9d0cda
12 changed files with 308 additions and 21 deletions
|
@ -187,6 +187,20 @@ namespace BTCPayServer.Controllers
|
|||
if (model == null)
|
||||
return NotFound();
|
||||
|
||||
|
||||
_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);
|
||||
}
|
||||
|
||||
|
|
|
@ -41,12 +41,14 @@ using NBXplorer;
|
|||
using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Rating;
|
||||
using BTCPayServer.Security;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
public partial class InvoiceController : Controller
|
||||
{
|
||||
InvoiceRepository _InvoiceRepository;
|
||||
ContentSecurityPolicies _CSP;
|
||||
BTCPayRateProviderFactory _RateProvider;
|
||||
StoreRepository _StoreRepository;
|
||||
UserManager<ApplicationUser> _UserManager;
|
||||
|
@ -64,6 +66,7 @@ namespace BTCPayServer.Controllers
|
|||
StoreRepository storeRepository,
|
||||
EventAggregator eventAggregator,
|
||||
BTCPayWalletProvider walletProvider,
|
||||
ContentSecurityPolicies csp,
|
||||
BTCPayNetworkProvider networkProvider)
|
||||
{
|
||||
_ServiceProvider = serviceProvider;
|
||||
|
@ -75,6 +78,7 @@ namespace BTCPayServer.Controllers
|
|||
_EventAggregator = eventAggregator;
|
||||
_NetworkProvider = networkProvider;
|
||||
_WalletProvider = walletProvider;
|
||||
_CSP = csp;
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -98,6 +98,26 @@ namespace BTCPayServer
|
|||
return str + "/";
|
||||
}
|
||||
|
||||
public static void SetHeaderOnStarting(this HttpResponse resp, string name, string value)
|
||||
{
|
||||
if (resp.HasStarted)
|
||||
return;
|
||||
resp.OnStarting(() =>
|
||||
{
|
||||
SetHeader(resp, name, value);
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
}
|
||||
|
||||
public static void SetHeader(this HttpResponse resp, string name, string value)
|
||||
{
|
||||
var existing = resp.Headers[name].FirstOrDefault();
|
||||
if (existing != null && value == null)
|
||||
resp.Headers.Remove(name);
|
||||
else
|
||||
resp.Headers[name] = value;
|
||||
}
|
||||
|
||||
public static string GetAbsoluteRoot(this HttpRequest request)
|
||||
{
|
||||
return string.Concat(
|
||||
|
|
106
BTCPayServer/Filters/ContentSecurityPolicyAttribute.cs
Normal file
106
BTCPayServer/Filters/ContentSecurityPolicyAttribute.cs
Normal file
|
@ -0,0 +1,106 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Security;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
|
||||
namespace BTCPayServer.Filters
|
||||
{
|
||||
public interface IContentSecurityPolicy : IFilterMetadata { }
|
||||
public class ContentSecurityPolicyAttribute : Attribute, IActionFilter, IContentSecurityPolicy
|
||||
{
|
||||
public void OnActionExecuted(ActionExecutedContext context)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public bool AutoSelf { get; set; } = true;
|
||||
public bool UnsafeInline { get; set; } = true;
|
||||
public bool FixWebsocket { get; set; } = true;
|
||||
public string FontSrc { get; set; } = null;
|
||||
public string ImgSrc { get; set; } = null;
|
||||
public string DefaultSrc { get; set; }
|
||||
public string StyleSrc { get; set; }
|
||||
public string ScriptSrc { get; set; }
|
||||
|
||||
public void OnActionExecuting(ActionExecutingContext context)
|
||||
{
|
||||
if (context.IsEffectivePolicy<IContentSecurityPolicy>(this))
|
||||
{
|
||||
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));
|
||||
}
|
||||
|
||||
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(ScriptSrc))
|
||||
{
|
||||
policies.Add(new ConsentSecurityPolicy("script-src", ScriptSrc));
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
context.HttpContext.Response.OnStarting(() =>
|
||||
{
|
||||
if (!policies.HasRules)
|
||||
return Task.CompletedTask;
|
||||
if (AutoSelf)
|
||||
{
|
||||
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)))
|
||||
{
|
||||
policies.Add(new ConsentSecurityPolicy(group.Key, "'self'"));
|
||||
hasSelf = true;
|
||||
}
|
||||
if (hasSelf)
|
||||
{
|
||||
foreach (var authorized in policies.Authorized)
|
||||
{
|
||||
policies.Add(new ConsentSecurityPolicy(group.Key, authorized));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
context.HttpContext.Response.SetHeader("Content-Security-Policy", policies.ToString());
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -23,11 +23,7 @@ namespace BTCPayServer.Filters
|
|||
{
|
||||
if (context.IsEffectivePolicy<ReferrerPolicyAttribute>(this))
|
||||
{
|
||||
var existing = context.HttpContext.Response.Headers["Referrer-Policy"].FirstOrDefault();
|
||||
if (existing != null && Value == null)
|
||||
context.HttpContext.Response.Headers.Remove("Referrer-Policy");
|
||||
else
|
||||
context.HttpContext.Response.Headers["Referrer-Policy"] = Value;
|
||||
context.HttpContext.Response.SetHeaderOnStarting("Referrer-Policy", Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,11 +19,7 @@ namespace BTCPayServer.Filters
|
|||
public string Value { get; set; }
|
||||
public void OnActionExecuting(ActionExecutingContext context)
|
||||
{
|
||||
var existing = context.HttpContext.Response.Headers["X-Content-Type-Options"].FirstOrDefault();
|
||||
if (existing != null && Value == null)
|
||||
context.HttpContext.Response.Headers.Remove("X-Content-Type-Options");
|
||||
else
|
||||
context.HttpContext.Response.Headers["X-Content-Type-Options"] = Value;
|
||||
context.HttpContext.Response.SetHeaderOnStarting("X-Content-Type-Options", Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,11 +23,7 @@ namespace BTCPayServer.Filters
|
|||
|
||||
public void OnActionExecuting(ActionExecutingContext context)
|
||||
{
|
||||
var existing = context.HttpContext.Response.Headers["X-Frame-Options"].FirstOrDefault();
|
||||
if (existing != null && Value == null)
|
||||
context.HttpContext.Response.Headers.Remove("X-Frame-Options");
|
||||
else
|
||||
context.HttpContext.Response.Headers["X-Frame-Options"] = Value;
|
||||
context.HttpContext.Response.SetHeaderOnStarting("X-Frame-Options", Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,11 +16,7 @@ namespace BTCPayServer.Filters
|
|||
|
||||
public void OnActionExecuting(ActionExecutingContext context)
|
||||
{
|
||||
var existing = context.HttpContext.Response.Headers["X-XSS-Protection"].FirstOrDefault();
|
||||
if (existing != null)
|
||||
context.HttpContext.Response.Headers.Remove("X-XSS-Protection");
|
||||
else
|
||||
context.HttpContext.Response.Headers["X-XSS-Protection"] = "1; mode=block";
|
||||
context.HttpContext.Response.SetHeaderOnStarting("X-XSS-Protection", "1; mode=block");
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -11,6 +11,8 @@ using NBXplorer.Models;
|
|||
using System.Collections.Concurrent;
|
||||
using BTCPayServer.Events;
|
||||
using BTCPayServer.Services;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
using BTCPayServer.Security;
|
||||
|
||||
namespace BTCPayServer.HostedServices
|
||||
{
|
||||
|
@ -50,6 +52,33 @@ namespace BTCPayServer.HostedServices
|
|||
}
|
||||
}
|
||||
|
||||
public class ContentSecurityPolicyCssThemeManager : Attribute, IActionFilter, IOrderedFilter
|
||||
{
|
||||
public int Order => 1001;
|
||||
|
||||
public void OnActionExecuted(ActionExecutedContext context)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public void OnActionExecuting(ActionExecutingContext context)
|
||||
{
|
||||
var manager = context.HttpContext.RequestServices.GetService(typeof(CssThemeManager)) as CssThemeManager;
|
||||
var policies = context.HttpContext.RequestServices.GetService(typeof(ContentSecurityPolicies)) as ContentSecurityPolicies;
|
||||
if (manager != null && policies != null)
|
||||
{
|
||||
if(manager.CreativeStartUri != null && Uri.TryCreate(manager.CreativeStartUri, UriKind.Absolute, out var uri))
|
||||
{
|
||||
policies.Clear();
|
||||
}
|
||||
if (manager.BootstrapUri != null && Uri.TryCreate(manager.BootstrapUri, UriKind.Absolute, out uri))
|
||||
{
|
||||
policies.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class CssThemeManagerHostedService : BaseAsyncService
|
||||
{
|
||||
private SettingsRepository _SettingsRepository;
|
||||
|
|
|
@ -104,6 +104,7 @@ namespace BTCPayServer.Hosting
|
|||
});
|
||||
|
||||
services.AddSingleton<CssThemeManager>();
|
||||
services.Configure<MvcOptions>((o) => { o.Filters.Add(new ContentSecurityPolicyCssThemeManager()); });
|
||||
services.AddSingleton<IHostedService, CssThemeManagerHostedService>();
|
||||
|
||||
services.AddSingleton<Payments.IPaymentMethodHandler<DerivationStrategy>, Payments.Bitcoin.BitcoinLikePaymentHandler>();
|
||||
|
|
|
@ -39,6 +39,7 @@ using Microsoft.AspNetCore.Mvc.Cors.Internal;
|
|||
using Microsoft.AspNetCore.Server.Kestrel.Core;
|
||||
using System.Net;
|
||||
using Meziantou.AspNetCore.BundleTagHelpers;
|
||||
using BTCPayServer.Security;
|
||||
|
||||
namespace BTCPayServer.Hosting
|
||||
{
|
||||
|
@ -82,8 +83,16 @@ namespace BTCPayServer.Hosting
|
|||
o.Filters.Add(new XContentTypeOptionsAttribute("nosniff"));
|
||||
o.Filters.Add(new XXSSProtectionAttribute());
|
||||
o.Filters.Add(new ReferrerPolicyAttribute("same-origin"));
|
||||
o.Filters.Add(new ContentSecurityPolicyAttribute()
|
||||
{
|
||||
FontSrc = "'self' https://fonts.gstatic.com/",
|
||||
ImgSrc = "'self' data:",
|
||||
DefaultSrc = "'none'",
|
||||
StyleSrc = "'self' 'unsafe-inline'",
|
||||
ScriptSrc = "'self' 'unsafe-inline'"
|
||||
});
|
||||
});
|
||||
|
||||
services.TryAddScoped<ContentSecurityPolicies>();
|
||||
services.Configure<IdentityOptions>(options =>
|
||||
{
|
||||
options.Password.RequireDigit = false;
|
||||
|
|
120
BTCPayServer/Security/ContentSecurityPolicies.cs
Normal file
120
BTCPayServer/Security/ContentSecurityPolicies.cs
Normal file
|
@ -0,0 +1,120 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Security
|
||||
{
|
||||
public class ConsentSecurityPolicy
|
||||
{
|
||||
public ConsentSecurityPolicy(string name, string value)
|
||||
{
|
||||
_Value = value;
|
||||
_Name = name;
|
||||
}
|
||||
|
||||
|
||||
private readonly string _Name;
|
||||
public string Name
|
||||
{
|
||||
get
|
||||
{
|
||||
return _Name;
|
||||
}
|
||||
}
|
||||
|
||||
private readonly string _Value;
|
||||
public string Value
|
||||
{
|
||||
get
|
||||
{
|
||||
return _Value;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
ConsentSecurityPolicy item = obj as ConsentSecurityPolicy;
|
||||
if (item == null)
|
||||
return false;
|
||||
return GetHashCode().Equals(item.GetHashCode());
|
||||
}
|
||||
public static bool operator ==(ConsentSecurityPolicy a, ConsentSecurityPolicy b)
|
||||
{
|
||||
if (System.Object.ReferenceEquals(a, b))
|
||||
return true;
|
||||
if (((object)a == null) || ((object)b == null))
|
||||
return false;
|
||||
return a.GetHashCode() == b.GetHashCode();
|
||||
}
|
||||
|
||||
public static bool operator !=(ConsentSecurityPolicy a, ConsentSecurityPolicy b)
|
||||
{
|
||||
return !(a == b);
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return HashCode.Combine(Name, Value);
|
||||
}
|
||||
}
|
||||
public class ContentSecurityPolicies
|
||||
{
|
||||
public ContentSecurityPolicies()
|
||||
{
|
||||
|
||||
}
|
||||
HashSet<ConsentSecurityPolicy> _Policies = new HashSet<ConsentSecurityPolicy>();
|
||||
public void Add(ConsentSecurityPolicy policy)
|
||||
{
|
||||
if (_Policies.Any(p => p.Name == policy.Name && p.Value == policy.Name))
|
||||
return;
|
||||
_Policies.Add(policy);
|
||||
}
|
||||
|
||||
public IEnumerable<ConsentSecurityPolicy> Rules => _Policies;
|
||||
public bool HasRules => _Policies.Count != 0;
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
StringBuilder value = new StringBuilder();
|
||||
bool firstGroup = true;
|
||||
foreach(var group in Rules.GroupBy(r => r.Name))
|
||||
{
|
||||
if (!firstGroup)
|
||||
{
|
||||
value.Append(';');
|
||||
}
|
||||
List<string> values = new List<string>();
|
||||
values.Add(group.Key);
|
||||
foreach (var v in group)
|
||||
{
|
||||
values.Add(v.Value);
|
||||
}
|
||||
foreach(var i in authorized)
|
||||
{
|
||||
values.Add(i);
|
||||
}
|
||||
value.Append(String.Join(" ", values.OfType<object>().ToArray()));
|
||||
firstGroup = false;
|
||||
}
|
||||
return value.ToString();
|
||||
}
|
||||
|
||||
internal void Clear()
|
||||
{
|
||||
authorized.Clear();
|
||||
_Policies.Clear();
|
||||
}
|
||||
|
||||
HashSet<string> authorized = new HashSet<string>();
|
||||
internal void AddAllAuthorized(string v)
|
||||
{
|
||||
authorized.Add(v);
|
||||
}
|
||||
|
||||
public IEnumerable<string> Authorized => authorized;
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue