Skip to content

Commit

Permalink
Add CSP at the website level (#2863)
Browse files Browse the repository at this point in the history
  • Loading branch information
NicolasDorier committed Sep 9, 2021
1 parent c39f134 commit fc4e47c
Show file tree
Hide file tree
Showing 9 changed files with 224 additions and 90 deletions.
4 changes: 3 additions & 1 deletion BTCPayServer.Tests/BTCPayServerTester.cs
Expand Up @@ -134,7 +134,7 @@ public async Task StartAsync()
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}");
Expand Down Expand Up @@ -283,6 +283,8 @@ public T GetService<T>()
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();
Expand Down
1 change: 1 addition & 0 deletions BTCPayServer.Tests/SeleniumTester.cs
Expand Up @@ -38,6 +38,7 @@ public class SeleniumTester : IDisposable

public async Task StartAsync()
{
Server.PayTester.NoCSP = true;
await Server.StartAsync();

var windowSize = (Width: 1200, Height: 1000);
Expand Down
Expand Up @@ -67,7 +67,7 @@ else
@if (!disabled)
{

<script type="text/javascript">
<script type="text/javascript" csp-sha256>
var supportsWebSockets = 'WebSocket' in window && window.WebSocket.CLOSING === 2;
Expand Down
1 change: 1 addition & 0 deletions BTCPayServer/Configuration/DefaultConfiguration.cs
Expand Up @@ -28,6 +28,7 @@ protected override CommandLineApplication CreateCommandLineApplicationCore()
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);
Expand Down
141 changes: 78 additions & 63 deletions BTCPayServer/Filters/ContentSecurityPolicyAttribute.cs
Expand Up @@ -7,13 +7,32 @@
namespace BTCPayServer.Filters
{
public interface IContentSecurityPolicy : IFilterMetadata { }
public enum CSPTemplate
{
AntiXSS
}
public class ContentSecurityPolicyAttribute : Attribute, IActionFilter, IContentSecurityPolicy
{
public void OnActionExecuted(ActionExecutedContext context)
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;
Expand All @@ -22,83 +41,79 @@ public void OnActionExecuted(ActionExecutedContext context)
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;
});
}
}
}
10 changes: 2 additions & 8 deletions BTCPayServer/Hosting/Startup.cs
Expand Up @@ -114,14 +114,8 @@ public void ConfigureServices(IServiceCollection services)
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 =>
{
Expand Down
21 changes: 7 additions & 14 deletions BTCPayServer/Security/ContentSecurityPolicies.cs
Expand Up @@ -9,6 +9,8 @@ public class ConsentSecurityPolicy
{
public ConsentSecurityPolicy(string name, string value)
{
if (value.Contains(';', StringComparison.OrdinalIgnoreCase))
throw new FormatException();
_Value = value;
_Name = name;
}
Expand Down Expand Up @@ -67,6 +69,10 @@ public ContentSecurityPolicies()
}

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))
Expand All @@ -87,16 +93,12 @@ public override string ToString()
{
value.Append(';');
}
List<string> values = new List<string>();
HashSet<string> values = new HashSet<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;
}
Expand All @@ -105,16 +107,7 @@ public override string 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;
}
}

0 comments on commit fc4e47c

Please sign in to comment.