Add Subscriber.Metadata as email placeholder, fix Offering.Metadata and Customer.Metadata placeholder
What changed, and why it matters
This commit fixes a bug in BTCPay Server's subscription email feature where metadata placeholders for offerings and customers didn't work, and adds a new placeholder for subscriber metadata. The metadata strings are now parsed as JSON objects so email templates can correctly read individual metadata fields. There is no clear security vulnerability here; it is a functional bug fix with a minor side effect that malformed metadata could now cause a parsing error when an email is generated.
Treat as a routine bug fix. If reviewing further, verify that Subscriber.Metadata, Offering.Metadata, and Customer.Metadata are validated as valid JSON at input time, or add a try/catch around JObject.Parse to prevent email-sending crashes from malformed metadata.
Security signals we found
Parsing untrusted/variable metadata strings with JObject.Parse without visible exception handling
Potential denial-of-service/crash path if malformed metadata reaches email generation
No injection or privilege-escalation signal in the diff
Evidence from the diff
In BTCPayServer/Plugins/Subscriptions/SubscriberWebhookProvider.cs, the model construction for email/webhook placeholders changed Metadata values from raw JSON strings to parsed JObject instances via JObject.Parse(). SubscriptionsPlugin.cs adds a new documented placeholder {Subscriber.Metadata}*. Changelog.md records this as a bug fix (#7150) for Offering/Customer metadata placeholders and an improvement adding Subscriber metadata. The change enables dot-notation access to metadata properties in templates. If metadata is not valid JSON, JObject.Parse will throw a JsonReaderException at email generation time, which could crash the email sending path. The commit does not add validation or exception handling.
Changed components
BTCPayServer/Plugins/Subscriptions/SubscriberWebhookProvider.csBTCPayServer/Plugins/Subscriptions/SubscriptionsPlugin.csSubscription email/webhook placeholder renderingInspect captured patch +11 / −7
diff --git a/BTCPayServer/Plugins/Subscriptions/SubscriberWebhookProvider.cs b/BTCPayServer/Plugins/Subscriptions/SubscriberWebhookProvider.cs
index 94b7628..afd80df 100644
--- a/BTCPayServer/Plugins/Subscriptions/SubscriberWebhookProvider.cs
+++ b/BTCPayServer/Plugins/Subscriptions/SubscriberWebhookProvider.cs
@@ -25,19 +25,20 @@ public class SubscriberWebhookProvider : WebhookTriggerProvider<SubscriptionEven
["Name"] = evt.Subscriber.Offering.App.Name,
["Id"] = evt.Subscriber.Offering.Id,
["AppId"] = evt.Subscriber.Offering.AppId,
- ["Metadata"] = evt.Subscriber.Offering.Metadata,
+ ["Metadata"] = JObject.Parse(evt.Subscriber.Offering.Metadata),
};
model["Subscriber"] = new JObject()
{
["Phase"] = evt.Subscriber.Phase.ToString(),
// TODO: When the subscriber can customize the email, also check it!
- ["Email"] = evt.Subscriber.Customer.Email.Get()
+ ["Email"] = evt.Subscriber.Customer.Email.Get(),
+ ["Metadata"] = JObject.Parse(evt.Subscriber.Metadata)
};
model["Customer"] = new JObject()
{
["ExternalRef"] = evt.Subscriber.Customer.ExternalRef ?? "",
["Name"] = evt.Subscriber.Customer.Name,
- ["Metadata"] = evt.Subscriber.Customer.Metadata
+ ["Metadata"] = JObject.Parse(evt.Subscriber.Customer.Metadata)
};
return model;
}
diff --git a/BTCPayServer/Plugins/Subscriptions/SubscriptionsPlugin.cs b/BTCPayServer/Plugins/Subscriptions/SubscriptionsPlugin.cs
index 020f254..e0b01db 100644
--- a/BTCPayServer/Plugins/Subscriptions/SubscriptionsPlugin.cs
+++ b/BTCPayServer/Plugins/Subscriptions/SubscriptionsPlugin.cs
@@ -82,6 +82,7 @@ public class SubscriptionsPlugin : BaseBTCPayServerPlugin
new("{Offering.Metadata}*", "Offering metadata"),
new("{Subscriber.Phase}", "Subscriber phase (Trial, Normal, Grace, Expired)"),
new("{Subscriber.Email}", "Subscriber email"),
+ new("{Subscriber.Metadata}*", "Subscriber metadata"),
new("{Customer.ExternalRef}", "Customer external reference"),
new("{Customer.Name}", "Customer name"),
new("{Customer.Metadata}*", "Customer metadata")
diff --git a/Changelog.md b/Changelog.md
index e823f63..783617e 100644
--- a/Changelog.md
+++ b/Changelog.md
@@ -603,18 +603,20 @@ If you are using Boltcards, we advise you to update to this release.
### Bug fixes
-* LNUrl payouts failing due to amount restriction wouldn't be immediately cancelled (#6061) @Kukks
+* Fix: Offering and Customer metadata email placeholders couldn't be used (#7150) @NicolasDorier
+* LNUrl payouts failing due to amount restriction wouldn't be immediately canceled (#6061) @Kukks
* Fix row ordering and display issues in reporting (#6065 #6087, 597e2b0e) @NicolasDorier @dennisreimann
* Parse Timespan strings in the API properly (#6012) @dennisreimann
-* "Return to Store" link in invoice receipt should return to the redirectUrl (#6079) @dennisreimann
+* "Return to Store" link in the invoice receipt should return to the redirectUrl (#6079) @dennisreimann
* Fix crash caused by custom explorer links in some conditions (#6077 #6078) @dennisreimann
* Fix: Can't save email settings on store level (#6076 #6080) @dennisreimann
* Reports: Fix dropdown z-index @dennisreimann
-* Shopify: Properly cancel an order when BTCPay invoice expires, and restock the inventory (#6104 #6107 #6108) @NicolasDorier
+* Shopify: Properly cancel an order when BTCPay invoice expires and restock the inventory (#6104 #6107 #6108) @NicolasDorier
* Shopify: Generate BTCPay invoice as soon as the payment page in shopify opens (#6105) @NicolasDorier
### Improvements
+* Add subscriber's metadata to email placeholders (#7150) @NicolasDorier
* Checkout: Display item description if present (#6082) @dennisreimann
* Disable plugins if they crash the Dashboard page (#6099) @NicolasDorier
* Hide empty values in the receipts (#6079) @dennisreimann
@@ -624,7 +626,7 @@ If you are using Boltcards, we advise you to update to this release.
### Bug fixes
-* Fix potential crash on receipt print page (#6045) @dennisreimann
+* Fix a potential crash on the receipt print page (#6045) @dennisreimann
* Fix invoice paid for topping up a pull payment didn't top up. @NicolasDorier
* Pull payment: Enable CORS for LNURL request (#6044) @dennisreimann
Why this scored 19/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.