CVE-2021-3646 (C#, published 2021-09-10T18:15Z)
Description: value: btcpayserver is vulnerable to Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')
Severity: MEDIUM (CVSS v3 base score: 6.1)
Weakness: CWE-79 \u2014 Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')
The product does not neutralize or incorrectly neutralizes user-controllable input before it is placed in output that is used as a web page that is served to other users.
Repository: https://github.com/btcpayserver/btcpayserver
Files changed:
BTCPayServer.Tests/BTCPayServerTester.cs
BTCPayServer.Tests/SeleniumTester.cs
BTCPayServer/Components/NotificationsDropdown/Default.cshtml
BTCPayServer/Configuration/DefaultConfiguration.cs
BTCPayServer/Filters/ContentSecurityPolicyAttribute.cs
BTCPayServer/Hosting/Startup.cs
BTCPayServer/Security/ContentSecurityPolicies.cs
BTCPayServer/TagHelpers.cs
BTCPayServer/Views/Invoice/PosData.cshtml
Commit message: Add CSP at the website level (#2863)



Vulnerable code:

    <script type="text/javascript">
        public void OnActionExecuted(ActionExecutedContext 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;
                });
            }
                //o.Filters.Add(new ContentSecurityPolicyAttribute()
                //{
                //    FontSrc = "'self' https://fonts.gstatic.com/",
                //    ImgSrc = "'self' data:",
                //    DefaultSrc = "'none'",
                //    StyleSrc = "'self' 'unsafe-inline'",
                //    ScriptSrc = "'self' 'unsafe-inline'"
                //});
                List<string> values = new List<string>();
                foreach (var i in authorized)
                {
                    values.Add(i);
                }
            authorized.Clear();

        readonly HashSet<string> authorized = new HashSet<string>();
        internal void AddAllAuthorized(string v)
        {
            authorized.Add(v);
        }

        public IEnumerable<string> Authorized => authorized;
    void DisplayValue(object value)
                    @{ DisplayValue(value); }
                        @{ DisplayValue(subItems.First().Value); }

Fixed code:
            config.AppendLine($"nocsp={NoCSP.ToString().ToLowerInvariant()}");
        public bool NoCSP { get; set; }

            Server.PayTester.NoCSP = true;
    <script type="text/javascript" csp-sha256>
            app.Option("--nocsp", $"Disable CSP (default false)", CommandOptionType.BoolValue);
    public enum CSPTemplate
    {
        AntiXSS
    }
        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 string ManifestSrc { get; set; }
            if (!context.IsEffectivePolicy<IContentSecurityPolicy>(this) || !Enabled)
                return;
            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(ManifestSrc))
            {
                policies.Add(new ConsentSecurityPolicy("manifest-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'"));
                }
                context.HttpContext.Response.SetHeader("Content-Security-Policy", policies.ToString());
                return Task.CompletedTask;
            });
                if (!Configuration.GetOrDefault<bool>("nocsp", false))
                    o.Filters.Add(new ContentSecurityPolicyAttribute(CSPTemplate.AntiXSS));
            if (value.Contains(';', StringComparison.OrdinalIgnoreCase))
                throw new FormatException();
        public void Add(string name, string value)
        {
            Add(new ConsentSecurityPolicy(name, value));
        }
                HashSet<string> values = new HashSet<string>();
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 async Task ProcessAsync(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;
            }
            if (output.Attributes.ContainsName("csp-sha256"))
            {
                var sha = CSPEventTagHelper.GetSha256((await output.GetChildContentAsync(true)).GetContent());
                _csp.Add("script-src", $"'sha256-{sha}'");
                output.Attributes.RemoveAll("csp-sha256");
            }
            else
            {
                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))));
        }
    }
}
    async Task DisplayValue(object value)
                    @{ await DisplayValue(value); }
                        @{ await DisplayValue(subItems.First().Value); }