Add CSP (Disable it if custom theming)

This commit is contained in:
nicolas.dorier 2018-07-12 17:38:21 +09:00
parent 6ea2d9175d
commit 976d9d0cda
12 changed files with 308 additions and 21 deletions

View file

@ -187,6 +187,20 @@ namespace BTCPayServer.Controllers
if (model == null) if (model == null)
return NotFound(); 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); return View(nameof(Checkout), model);
} }

View file

@ -41,12 +41,14 @@ using NBXplorer;
using BTCPayServer.HostedServices; using BTCPayServer.HostedServices;
using BTCPayServer.Payments; using BTCPayServer.Payments;
using BTCPayServer.Rating; using BTCPayServer.Rating;
using BTCPayServer.Security;
namespace BTCPayServer.Controllers namespace BTCPayServer.Controllers
{ {
public partial class InvoiceController : Controller public partial class InvoiceController : Controller
{ {
InvoiceRepository _InvoiceRepository; InvoiceRepository _InvoiceRepository;
ContentSecurityPolicies _CSP;
BTCPayRateProviderFactory _RateProvider; BTCPayRateProviderFactory _RateProvider;
StoreRepository _StoreRepository; StoreRepository _StoreRepository;
UserManager<ApplicationUser> _UserManager; UserManager<ApplicationUser> _UserManager;
@ -64,6 +66,7 @@ namespace BTCPayServer.Controllers
StoreRepository storeRepository, StoreRepository storeRepository,
EventAggregator eventAggregator, EventAggregator eventAggregator,
BTCPayWalletProvider walletProvider, BTCPayWalletProvider walletProvider,
ContentSecurityPolicies csp,
BTCPayNetworkProvider networkProvider) BTCPayNetworkProvider networkProvider)
{ {
_ServiceProvider = serviceProvider; _ServiceProvider = serviceProvider;
@ -75,6 +78,7 @@ namespace BTCPayServer.Controllers
_EventAggregator = eventAggregator; _EventAggregator = eventAggregator;
_NetworkProvider = networkProvider; _NetworkProvider = networkProvider;
_WalletProvider = walletProvider; _WalletProvider = walletProvider;
_CSP = csp;
} }

View file

@ -98,6 +98,26 @@ namespace BTCPayServer
return str + "/"; 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) public static string GetAbsoluteRoot(this HttpRequest request)
{ {
return string.Concat( return string.Concat(

View 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;
});
}
}
}
}

View file

@ -23,11 +23,7 @@ namespace BTCPayServer.Filters
{ {
if (context.IsEffectivePolicy<ReferrerPolicyAttribute>(this)) if (context.IsEffectivePolicy<ReferrerPolicyAttribute>(this))
{ {
var existing = context.HttpContext.Response.Headers["Referrer-Policy"].FirstOrDefault(); context.HttpContext.Response.SetHeaderOnStarting("Referrer-Policy", Value);
if (existing != null && Value == null)
context.HttpContext.Response.Headers.Remove("Referrer-Policy");
else
context.HttpContext.Response.Headers["Referrer-Policy"] = Value;
} }
} }
} }

View file

@ -19,11 +19,7 @@ namespace BTCPayServer.Filters
public string Value { get; set; } public string Value { get; set; }
public void OnActionExecuting(ActionExecutingContext context) public void OnActionExecuting(ActionExecutingContext context)
{ {
var existing = context.HttpContext.Response.Headers["X-Content-Type-Options"].FirstOrDefault(); context.HttpContext.Response.SetHeaderOnStarting("X-Content-Type-Options", Value);
if (existing != null && Value == null)
context.HttpContext.Response.Headers.Remove("X-Content-Type-Options");
else
context.HttpContext.Response.Headers["X-Content-Type-Options"] = Value;
} }
} }
} }

View file

@ -23,11 +23,7 @@ namespace BTCPayServer.Filters
public void OnActionExecuting(ActionExecutingContext context) public void OnActionExecuting(ActionExecutingContext context)
{ {
var existing = context.HttpContext.Response.Headers["X-Frame-Options"].FirstOrDefault(); context.HttpContext.Response.SetHeaderOnStarting("X-Frame-Options", Value);
if (existing != null && Value == null)
context.HttpContext.Response.Headers.Remove("X-Frame-Options");
else
context.HttpContext.Response.Headers["X-Frame-Options"] = Value;
} }
} }
} }

View file

@ -16,11 +16,7 @@ namespace BTCPayServer.Filters
public void OnActionExecuting(ActionExecutingContext context) public void OnActionExecuting(ActionExecutingContext context)
{ {
var existing = context.HttpContext.Response.Headers["X-XSS-Protection"].FirstOrDefault(); context.HttpContext.Response.SetHeaderOnStarting("X-XSS-Protection", "1; mode=block");
if (existing != null)
context.HttpContext.Response.Headers.Remove("X-XSS-Protection");
else
context.HttpContext.Response.Headers["X-XSS-Protection"] = "1; mode=block";
} }
} }

View file

@ -11,6 +11,8 @@ using NBXplorer.Models;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using BTCPayServer.Events; using BTCPayServer.Events;
using BTCPayServer.Services; using BTCPayServer.Services;
using Microsoft.AspNetCore.Mvc.Filters;
using BTCPayServer.Security;
namespace BTCPayServer.HostedServices 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 public class CssThemeManagerHostedService : BaseAsyncService
{ {
private SettingsRepository _SettingsRepository; private SettingsRepository _SettingsRepository;

View file

@ -104,6 +104,7 @@ namespace BTCPayServer.Hosting
}); });
services.AddSingleton<CssThemeManager>(); services.AddSingleton<CssThemeManager>();
services.Configure<MvcOptions>((o) => { o.Filters.Add(new ContentSecurityPolicyCssThemeManager()); });
services.AddSingleton<IHostedService, CssThemeManagerHostedService>(); services.AddSingleton<IHostedService, CssThemeManagerHostedService>();
services.AddSingleton<Payments.IPaymentMethodHandler<DerivationStrategy>, Payments.Bitcoin.BitcoinLikePaymentHandler>(); services.AddSingleton<Payments.IPaymentMethodHandler<DerivationStrategy>, Payments.Bitcoin.BitcoinLikePaymentHandler>();

View file

@ -39,6 +39,7 @@ using Microsoft.AspNetCore.Mvc.Cors.Internal;
using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.AspNetCore.Server.Kestrel.Core;
using System.Net; using System.Net;
using Meziantou.AspNetCore.BundleTagHelpers; using Meziantou.AspNetCore.BundleTagHelpers;
using BTCPayServer.Security;
namespace BTCPayServer.Hosting namespace BTCPayServer.Hosting
{ {
@ -82,8 +83,16 @@ namespace BTCPayServer.Hosting
o.Filters.Add(new XContentTypeOptionsAttribute("nosniff")); o.Filters.Add(new XContentTypeOptionsAttribute("nosniff"));
o.Filters.Add(new XXSSProtectionAttribute()); o.Filters.Add(new XXSSProtectionAttribute());
o.Filters.Add(new ReferrerPolicyAttribute("same-origin")); 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 => services.Configure<IdentityOptions>(options =>
{ {
options.Password.RequireDigit = false; options.Password.RequireDigit = false;

View 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;
}
}