Fix pay button CSP issue when using modal (#2872)

* Fix pay button CSP issue when using modal

Fixes #2864.

* Use event handler, refactor csp tags

* Fix script indentation

* Fix onsubmit event handler integration

Co-authored-by: nicolas.dorier <nicolas.dorier@gmail.com>
This commit is contained in:
d11n 2021-09-12 13:31:35 +02:00 committed by GitHub
parent a8995d2bed
commit aac87539ae
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 110 additions and 50 deletions

View File

@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using NBitcoin.Crypto;
namespace BTCPayServer.Security
{
@ -69,6 +70,35 @@ namespace BTCPayServer.Security
}
readonly HashSet<ConsentSecurityPolicy> _Policies = new HashSet<ConsentSecurityPolicy>();
/// <summary>
/// Allow a specific script as event handler
/// </summary>
/// <param name="script"></param>
public void AllowUnsafeHashes(string script)
{
if (script is null)
throw new ArgumentNullException(nameof(script));
var sha = GetSha256(script);
Add("script-src", $"'unsafe-hashes'");
Add("script-src", $"'sha256-{sha}'");
}
/// <summary>
/// Allow the injection of script tag with the following script
/// </summary>
/// <param name="script"></param>
public void AllowInline(string script)
{
if (script is null)
throw new ArgumentNullException(nameof(script));
var sha = GetSha256(script);
Add("script-src", $"'sha256-{sha}'");
}
static string GetSha256(string script)
{
return Convert.ToBase64String(Hashes.SHA256(Encoding.UTF8.GetBytes(script.Replace("\r\n", "\n", StringComparison.Ordinal))));
}
public void Add(string name, string value)
{
Add(new ConsentSecurityPolicy(name, value));

View File

@ -72,7 +72,6 @@ namespace BTCPayServer.TagHelpers
[HtmlTargetElement(Attributes = "onkeypress")]
[HtmlTargetElement(Attributes = "onchange")]
[HtmlTargetElement(Attributes = "onsubmit")]
[HtmlTargetElement(Attributes = "href")]
public class CSPEventTagHelper : TagHelper
{
public const string EventNames = "onclick,onkeypress,onchange,onsubmit";
@ -86,35 +85,60 @@ namespace BTCPayServer.TagHelpers
}
public override void Process(TagHelperContext context, TagHelperOutput output)
{
bool cspAllow = output.Attributes.RemoveAll("csp-allow");
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) && cspAllow)
{
Allow(v);
}
_csp.AllowUnsafeHashes(attr.Value.ToString());
}
}
}
}
private void Allow(string v)
/// <summary>
/// Add sha256- to allow inline event handlers in CSP
/// </summary>
[HtmlTargetElement("template", Attributes = "csp-allow")]
public class CSPTemplate : TagHelper
{
private readonly ContentSecurityPolicies _csp;
public CSPTemplate(ContentSecurityPolicies csp)
{
var sha = GetSha256(v);
_csp.Add("script-src", $"'unsafe-hashes'");
_csp.Add("script-src", $"'sha256-{sha}'");
_csp = csp;
}
public static string GetSha256(string script)
public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
{
return Convert.ToBase64String(Hashes.SHA256(Encoding.UTF8.GetBytes(script.Replace("\r\n", "\n", StringComparison.Ordinal))));
output.Attributes.RemoveAll("csp-allow");
var childContent = await output.GetChildContentAsync();
var content = childContent.GetContent();
_csp.AllowInline(content);
}
}
/// <summary>
/// Add sha256- to allow inline event handlers in a:href=javascript:
/// </summary>
[HtmlTargetElement("a", Attributes = "csp-allow")]
public class CSPA : TagHelper
{
private readonly ContentSecurityPolicies _csp;
public CSPA(ContentSecurityPolicies csp)
{
_csp = csp;
}
public override void Process(TagHelperContext context, TagHelperOutput output)
{
output.Attributes.RemoveAll("csp-allow");
if (output.Attributes.TryGetAttribute("href", out var attr))
{
var v = attr.Value.ToString();
if (v.StartsWith("javascript:", StringComparison.OrdinalIgnoreCase))
{
_csp.AllowInline(v);
}
}
}
}
}

View File

@ -1,7 +1,9 @@
@inject BTCPayServer.Security.ContentSecurityPolicies csp
@model PayButtonViewModel
@{
Layout = "../Shared/_NavLayout.cshtml";
ViewData.SetActivePageAndTitle(StoreNavPages.PayButton, "Pay Button", Context.GetStoreData().StoreName);
csp.AllowUnsafeHashes("onBTCPayFormSubmit(event);return false");
}
@section PageHeadContent {
@ -14,6 +16,24 @@
<script src="~/vendor/vuejs-vee-validate/vee-validate.js" asp-append-version="true"></script>
<script src="~/vendor/clipboard.js/clipboard.js" asp-append-version="true"></script>
<script src="~/paybutton/paybutton.js" asp-append-version="true"></script>
<template id="template-get-scripts" csp-allow>
if (!window.btcpay) {
var script = document.createElement('script');
script.src='@(Model.UrlRoot)modal/btcpay.js';
document.getElementsByTagName('head')[0].append(script);
}
function onBTCPayFormSubmit(event) {
event.preventDefault();
var xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function() {
if (this.readyState == 4 && this.status == 200 && this.responseText) {
window.btcpay.showInvoice(JSON.parse(this.responseText).invoiceId);
}
};
xhttp.open('POST', event.target.getAttribute('action'), true);
xhttp.send(new FormData(event.target));
}
</template>
<script>
var srvModel = @Safe.Json(Model);

View File

@ -42,33 +42,11 @@ function getStyles (styles) {
}
function getScripts(srvModel) {
return ""+
"<script>" +
"if(!window.btcpay){ " +
" var head = document.getElementsByTagName('head')[0];" +
" var script = document.createElement('script');" +
" script.src='"+esc(srvModel.urlRoot)+"modal/btcpay.js';" +
" script.type = 'text/javascript';" +
" head.append(script);" +
"}" +
"function onBTCPayFormSubmit(event){" +
" var xhttp = new XMLHttpRequest();" +
" xhttp.onreadystatechange = function() {" +
" if (this.readyState == 4 && this.status == 200) {" +
" if(this.status == 200 && this.responseText){" +
" var response = JSON.parse(this.responseText);" +
" window.btcpay.showInvoice(response.invoiceId);" +
" }" +
" }" +
" };" +
" xhttp.open(\"POST\", event.target.getAttribute('action'), true);" +
" xhttp.send(new FormData( event.target ));" +
"}" +
"</script>";
if (!srvModel.useModal) return ''
const template = document.getElementById('template-get-scripts')
return template.innerHTML.replace(/&amp;/g, '&')
}
function inputChanges(event, buttonSize) {
if (buttonSize !== null && buttonSize !== undefined) {
srvModel.buttonSize = buttonSize;
@ -115,12 +93,10 @@ function inputChanges(event, buttonSize) {
}
var html =
//Scripts
(srvModel.useModal? getScripts(srvModel) :"") +
// Styles
getStyles('template-paybutton-styles') + (isSlider ? getStyles('template-slider-styles') : '') +
// Form
'<form method="POST" '+ ( srvModel.useModal? ' onsubmit="onBTCPayFormSubmit(event);return false" ' : '' )+' action="' + esc(srvModel.urlRoot) + actionUrl + '" class="btcpay-form btcpay-form--' + (srvModel.fitButtonInline ? 'inline' : 'block') +'">\n' +
'<form method="POST"' + (srvModel.useModal ? ' onsubmit="onBTCPayFormSubmit(event);return false"' : '') + ' action="' + esc(srvModel.urlRoot) + actionUrl + '" class="btcpay-form btcpay-form--' + (srvModel.fitButtonInline ? 'inline' : 'block') +'">\n' +
addInput("storeId", srvModel.storeId);
if(app){
@ -144,7 +120,6 @@ function inputChanges(event, buttonSize) {
if (srvModel.checkoutQueryString) html += addInput("checkoutQueryString", srvModel.checkoutQueryString);
}
// Fixed amount: Add price and currency as hidden inputs
if (isFixedAmount) {
@ -192,10 +167,21 @@ function inputChanges(event, buttonSize) {
'</button>'
}
html += '</form>';
// Scripts
var scripts = getScripts(srvModel);
var code = html + (scripts ? `\n<script>\n ${scripts.trim()}\n</script>` : '')
$("#mainCode").text(html).html();
$("#preview").html(html);
var form = document.querySelector("#preview form");
$("#mainCode").text(code).html();
var preview = document.getElementById('preview');
preview.innerHTML = html;
if (scripts) {
// script needs to be inserted as node, otherwise it won't get executed
var script = document.createElement('script');
script.innerHTML = scripts
preview.appendChild(script)
}
var form = preview.querySelector("form");
var url = new URL(form.getAttribute("action"));
var formData = new FormData(form);
formData.forEach((value, key) => {