mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2024-11-19 09:54:30 +01:00
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:
parent
a8995d2bed
commit
aac87539ae
@ -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));
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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(/&/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) => {
|
||||
|
Loading…
Reference in New Issue
Block a user