Unsafe variable evaluation in email templates
Description
Emails sent by pretix can utilize placeholders that will be filled with customer data. For example, when {name} is used in an email template, it will be replaced with the buyer's name for the final email. This mechanism contained two security-relevant bugs:
* It was possible to exfiltrate information about the pretix system through specially crafted placeholder names such as {{event.__init__.__code__.co_filename}}. This way, an attacker with the ability to control email templates (usually every user of the pretix backend) could retrieve sensitive information from the system configuration, including even database passwords or API keys. pretix does include mechanisms to prevent the usage of such malicious placeholders, however due to a mistake in the code, they were not fully effective for the email subject.
* Placeholders in subjects and plain text bodies of emails were wrongfully evaluated twice. Therefore, if the first evaluation of a placeholder again contains a placeholder, this second placeholder was rendered. This allows the rendering of placeholders controlled by the ticket buyer, and therefore the exploitation of the first issue as a ticket buyer. Luckily, the only buyer-controlled placeholder available in pretix by default (that is not validated in a way that prevents the issue) is {invoice_company}, which is very unusual (but not impossible) to be contained in an email subject template. In addition to broadening the attack surface of the first issue, this could theoretically also leak information about an order to one of the attendees within that order. However, we also consider this scenario very unlikely under typical conditions.
Out of caution, we recommend that you rotate all passwords and API keys contained in your pretix.cfg https://docs.pretix.eu/self-hosting/config/ file.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
pretixPyPI | >= 2026.1.0, < 2026.1.1 | 2026.1.1 |
pretixPyPI | >= 2025.10.0, < 2025.10.2 | 2025.10.2 |
pretixPyPI | < 2025.9.4 | 2025.9.4 |
Affected products
1Patches
3c85afbc621b5Fix placeholder injection with django templates
4 files changed · +51 −35
src/pretix/base/email.py+9 −7 modified@@ -39,7 +39,7 @@ DEFAULT_CALLBACKS, EMAIL_RE, URL_RE, abslink_callback, markdown_compile_email, truelink_callback, ) -from pretix.helpers.format import SafeFormatter, format_map +from pretix.helpers.format import FormattedString, SafeFormatter, format_map from pretix.base.services.placeholders import ( # noqa get_available_placeholders, PlaceholderContext @@ -141,6 +141,7 @@ def compile_markdown(self, plaintext, context=None): return markdown_compile_email(plaintext, context=context) def render(self, plain_body: str, plain_signature: str, subject: str, order, position, context) -> str: + apply_format_map = not isinstance(plain_body, FormattedString) body_md = self.compile_markdown(plain_body, context) if context: linker = bleach.Linker( @@ -149,12 +150,13 @@ def render(self, plain_body: str, plain_signature: str, subject: str, order, pos callbacks=DEFAULT_CALLBACKS + [truelink_callback, abslink_callback], parse_email=True ) - body_md = format_map( - body_md, - context=context, - mode=SafeFormatter.MODE_RICH_TO_HTML, - linkifier=linker - ) + if apply_format_map: + body_md = format_map( + body_md, + context=context, + mode=SafeFormatter.MODE_RICH_TO_HTML, + linkifier=linker + ) htmlctx = { 'site': settings.PRETIX_INSTANCE_NAME, 'site_url': settings.SITE_URL,
src/pretix/base/services/mail.py+13 −3 modified@@ -75,7 +75,9 @@ from pretix.base.services.tickets import get_tickets_for_order from pretix.base.signals import email_filter, global_email_filter from pretix.celery_app import app -from pretix.helpers.format import FormattedString, SafeFormatter, format_map +from pretix.helpers.format import ( + FormattedString, PlainHtmlAlternativeString, SafeFormatter, format_map, +) from pretix.helpers.hierarkey import clean_filename from pretix.multidomain.urlreverse import build_absolute_uri from pretix.presale.ical import get_private_icals @@ -258,7 +260,10 @@ def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, La else: timezone = ZoneInfo(settings.TIME_ZONE) - body_plain = format_map(content_plain, context, mode=SafeFormatter.MODE_RICH_TO_PLAIN) + if not isinstance(content_plain, FormattedString): + body_plain = format_map(content_plain, context, mode=SafeFormatter.MODE_RICH_TO_PLAIN) + else: + body_plain = content_plain if settings_holder: if settings_holder.settings.mail_bcc: for bcc_mail in settings_holder.settings.mail_bcc.split(','): @@ -760,7 +765,12 @@ def render_mail(template, context, placeholder_mode=SafeFormatter.MODE_RICH_TO_P body = format_map(body, context, mode=placeholder_mode) else: tpl = get_template(template) - body = tpl.render(context) + context = { + # Known bug, should behave differently for plain and HTML but we'll fix after security release + k: v.html if isinstance(v, PlainHtmlAlternativeString) else v + for k, v in context.items() + } + body = FormattedString(tpl.render(context)) return body
src/tests/base/test_mail.py+24 −20 modified@@ -41,6 +41,7 @@ import pytest from django.conf import settings from django.core import mail as djmail +from django.utils.html import escape from django.utils.timezone import now from django.utils.translation import gettext_lazy as _ from django_scopes import scope, scopes_disabled @@ -229,7 +230,7 @@ def _extract_html(mail): def test_placeholder_html_rendering_from_template(env): djmail.outbox = [] event, user, organizer = env - event.name = "<strong>event & co. kg</strong>" + event.name = "<strong>event & co. kg</strong> {currency}" event.save() mail('dummy@dummy.dummy', '{event} Test subject', 'mailtest.txt', get_email_context( event=event, @@ -238,25 +239,26 @@ def test_placeholder_html_rendering_from_template(env): assert len(djmail.outbox) == 1 assert djmail.outbox[0].to == [user.email] - assert 'Event name: <strong>event & co. kg</strong>' in djmail.outbox[0].body - assert '**IBAN**: 123 \n**BIC**: 456' in djmail.outbox[0].body - assert '**Meta**: *Beep*' in djmail.outbox[0].body - assert 'Event website: [<strong>event & co. kg</strong>](https://example.org/dummy)' in djmail.outbox[0].body - assert 'Other website: [<strong>event & co. kg</strong>](https://example.com)' in djmail.outbox[0].body - assert '<' not in djmail.outbox[0].body - assert '&' not in djmail.outbox[0].body + # Known bug for now: These should not have HTML for the plain body, but we'll fix this safter the security release + assert escape('Event name: <strong>event & co. kg</strong> {currency}') in djmail.outbox[0].body + assert '<strong>IBAN</strong>: 123<br>\n<strong>BIC</strong>: 456' in djmail.outbox[0].body + assert '**Meta**: <em>Beep</em>' in djmail.outbox[0].body + assert escape('Event website: [<strong>event & co. kg</strong> {currency}](https://example.org/dummy)') in djmail.outbox[0].body + # todo: assert '<' not in djmail.outbox[0].body + # todo: assert '&' not in djmail.outbox[0].body + assert 'Unevaluated placeholder: {currency}' in djmail.outbox[0].body + assert 'EUR' not in djmail.outbox[0].body html = _extract_html(djmail.outbox[0]) assert '<strong>event' not in html - assert 'Event name: <strong>event & co. kg</strong>' in html + assert 'Event name: <strong>event & co. kg</strong> {currency}' in html assert '<strong>IBAN</strong>: 123<br/>\n<strong>BIC</strong>: 456' in html assert '<strong>Meta</strong>: <em>Beep</em>' in html + assert 'Unevaluated placeholder: {currency}' in html + assert 'EUR' not in html assert re.search( - r'Event website: <a href="https://example.org/dummy" rel="noopener" style="[^"]+" target="_blank"><strong>event & co. kg</strong></a>', - html - ) - assert re.search( - r'Other website: <a href="https://example.com" rel="noopener" style="[^"]+" target="_blank"><strong>event & co. kg</strong></a>', + r'Event website: <a href="https://example.org/dummy" rel="noopener" style="[^"]+" target="_blank">' + r'<strong>event & co. kg</strong> {currency}</a>', html ) @@ -274,7 +276,7 @@ def test_placeholder_html_rendering_from_string(env): }) djmail.outbox = [] event, user, organizer = env - event.name = "<strong>event & co. kg</strong>" + event.name = "<strong>event & co. kg</strong> {currency}" event.save() ctx = get_email_context( event=event, @@ -285,9 +287,9 @@ def test_placeholder_html_rendering_from_string(env): assert len(djmail.outbox) == 1 assert djmail.outbox[0].to == [user.email] - assert 'Event name: <strong>event & co. kg</strong>' in djmail.outbox[0].body - assert 'Event website: [<strong>event & co. kg</strong>](https://example.org/dummy)' in djmail.outbox[0].body - assert 'Other website: [<strong>event & co. kg</strong>](https://example.com)' in djmail.outbox[0].body + assert 'Event name: <strong>event & co. kg</strong> {currency}' in djmail.outbox[0].body + assert 'Event website: [<strong>event & co. kg</strong> {currency}](https://example.org/dummy)' in djmail.outbox[0].body + assert 'Other website: [<strong>event & co. kg</strong> {currency}](https://example.com)' in djmail.outbox[0].body assert '**IBAN**: 123 \n**BIC**: 456' in djmail.outbox[0].body assert '**Meta**: *Beep*' in djmail.outbox[0].body assert 'URL: https://google.com' in djmail.outbox[0].body @@ -302,11 +304,13 @@ def test_placeholder_html_rendering_from_string(env): assert '<strong>IBAN</strong>: 123<br/>\n<strong>BIC</strong>: 456' in html assert '<strong>Meta</strong>: <em>Beep</em>' in html assert re.search( - r'Event website: <a href="https://example.org/dummy" rel="noopener" style="[^"]+" target="_blank"><strong>event & co. kg</strong></a>', + r'Event website: <a href="https://example.org/dummy" rel="noopener" style="[^"]+" target="_blank">' + r'<strong>event & co. kg</strong> {currency}</a>', html ) assert re.search( - r'Other website: <a href="https://example.com" rel="noopener" style="[^"]+" target="_blank"><strong>event & co. kg</strong></a>', + r'Other website: <a href="https://example.com" rel="noopener" style="[^"]+" target="_blank">' + r'<strong>event & co. kg</strong> {currency}</a>', html ) assert re.search(
src/tests/templates/mailtest.txt+5 −5 modified@@ -1,13 +1,13 @@ {% load i18n %} This is a test file for sending mails. -Event name: {event} +Event name: {{ event }} +Unevaluated placeholder: {currency} {% get_current_language as LANGUAGE_CODE %} The language code used for rendering this email is {{ LANGUAGE_CODE }}. Payment info: -{payment_info} +{{ payment_info }} -**Meta**: {meta_Test} +**Meta**: {{ meta_Test }} -Event website: [{event}](https://example.org/{event_slug}) -Other website: [{event}]({meta_Website}) \ No newline at end of file +Event website: [{{event}}](https://example.org/{{event_slug}}) \ No newline at end of file
edac35ed4c54Mark strings as formatted to prevent double-formatting
3 files changed · +30 −8
src/pretix/base/models/orders.py+5 −3 modified@@ -87,7 +87,7 @@ from ...helpers import OF_SELF from ...helpers.countries import CachedCountries, FastCountryField -from ...helpers.format import format_map +from ...helpers.format import FormattedString, format_map from ...helpers.names import build_name from ...testutils.middleware import debugflags_var from ._transactions import ( @@ -1181,7 +1181,8 @@ def send_mail(self, subject: Union[str, LazyI18nString], template: Union[str, La try: email_content = render_mail(template, context) - subject = format_map(subject, context) + if not isinstance(subject, FormattedString): + subject = format_map(subject, context) mail( recipient, subject, template, context, self.event, self.locale, self, headers=headers, sender=sender, @@ -2926,7 +2927,8 @@ def send_mail(self, subject: str, template: Union[str, LazyI18nString], recipient = self.attendee_email try: email_content = render_mail(template, context) - subject = format_map(subject, context) + if not isinstance(subject, FormattedString): + subject = format_map(subject, context) mail( recipient, subject, template, context, self.event, self.order.locale, order=self.order, headers=headers, sender=sender,
src/pretix/base/services/mail.py+7 −3 modified@@ -75,7 +75,7 @@ from pretix.base.services.tickets import get_tickets_for_order from pretix.base.signals import email_filter, global_email_filter from pretix.celery_app import app -from pretix.helpers.format import SafeFormatter, format_map +from pretix.helpers.format import FormattedString, SafeFormatter, format_map from pretix.helpers.hierarkey import clean_filename from pretix.multidomain.urlreverse import build_absolute_uri from pretix.presale.ical import get_private_icals @@ -199,6 +199,9 @@ def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, La if email == INVALID_ADDRESS: return + if isinstance(template, FormattedString): + raise TypeError("Cannot pass an already formatted body template") + if no_order_links and not plain_text_only: raise ValueError('If you set no_order_links, you also need to set plain_text_only.') @@ -222,7 +225,8 @@ def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, La }) renderer = ClassicMailRenderer(None, organizer) content_plain = render_mail(template, context, placeholder_mode=None) - subject = format_map(subject, context) + if not isinstance(subject, FormattedString): + subject = format_map(subject, context) sender = ( sender or (event.settings.get('mail_from') if event else None) or @@ -254,7 +258,7 @@ def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, La else: timezone = ZoneInfo(settings.TIME_ZONE) - body_plain = render_mail(template, context, placeholder_mode=SafeFormatter.MODE_RICH_TO_PLAIN) + body_plain = format_map(content_plain, context, mode=SafeFormatter.MODE_RICH_TO_PLAIN) if settings_holder: if settings_holder.settings.mail_bcc: for bcc_mail in settings_holder.settings.mail_bcc.split(','):
src/pretix/helpers/format.py+18 −2 modified@@ -22,6 +22,7 @@ import logging from string import Formatter +from django.core.exceptions import SuspiciousOperation from django.utils.html import conditional_escape logger = logging.getLogger(__name__) @@ -37,6 +38,17 @@ def __repr__(self): return f"PlainHtmlAlternativeString('{self.plain}', '{self.html}')" +class FormattedString(str): + """ + A str subclass that has been specifically marked as "already formatted" for email rendering + purposes to avoid duplicate formatting. + """ + __slots__ = () + + def __str__(self): + return self + + class SafeFormatter(Formatter): """ Customized version of ``str.format`` that (a) behaves just like ``str.format_map`` and @@ -78,7 +90,11 @@ def format_field(self, value, format_spec): return super().format_field(self._prepare_value(value), '') -def format_map(template, context, raise_on_missing=False, mode=SafeFormatter.MODE_RICH_TO_PLAIN, linkifier=None): +def format_map(template, context, raise_on_missing=False, mode=SafeFormatter.MODE_RICH_TO_PLAIN, linkifier=None) -> FormattedString: + if isinstance(template, FormattedString): + raise SuspiciousOperation("Calling format_map() on an already formatted string is likely unsafe.") if not isinstance(template, str): template = str(template) - return SafeFormatter(context, raise_on_missing, mode=mode, linkifier=linkifier).format(template) + return FormattedString( + SafeFormatter(context, raise_on_missing, mode=mode, linkifier=linkifier).format(template) + )
ba11d24f8dfaSECURITY: Prevent placeholder injcetion in plaintext emails
2 files changed · +187 −10
src/pretix/base/services/mail.py+6 −8 modified@@ -221,8 +221,8 @@ def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, La 'invoice_company': '' }) renderer = ClassicMailRenderer(None, organizer) - body_plain = render_mail(template, context, placeholder_mode=SafeFormatter.MODE_RICH_TO_PLAIN) - subject = str(subject).format_map(TolerantDict(context)) + content_plain = render_mail(template, context, placeholder_mode=None) + subject = format_map(subject, context) sender = ( sender or (event.settings.get('mail_from') if event else None) or @@ -254,6 +254,7 @@ def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, La else: timezone = ZoneInfo(settings.TIME_ZONE) + body_plain = render_mail(template, context, placeholder_mode=SafeFormatter.MODE_RICH_TO_PLAIN) if settings_holder: if settings_holder.settings.mail_bcc: for bcc_mail in settings_holder.settings.mail_bcc.split(','): @@ -269,7 +270,7 @@ def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, La signature = str(settings_holder.settings.get('mail_text_signature')) if signature: - signature = signature.format(event=event.name if event else '') + signature = format_map(signature, {"event": event.name if event else ''}) body_plain += signature body_plain += "\r\n\r\n-- \r\n" @@ -287,7 +288,7 @@ def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, La body_plain += _( "You can view your order details at the following URL:\n{orderurl}." ).replace("\n", "\r\n").format( - event=event.name, orderurl=build_absolute_uri( + orderurl=build_absolute_uri( order.event, 'presale:event.order.position', kwargs={ 'order': order.code, 'secret': position.web_secret, @@ -303,7 +304,7 @@ def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, La body_plain += _( "You can view your order details at the following URL:\n{orderurl}." ).replace("\n", "\r\n").format( - event=event.name, orderurl=build_absolute_uri( + orderurl=build_absolute_uri( order.event, 'presale:event.order.open', kwargs={ 'order': order.code, 'secret': order.secret, @@ -315,7 +316,6 @@ def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, La with override(timezone): try: - content_plain = render_mail(template, context, placeholder_mode=None) if plain_text_only: body_html = None elif 'context' in inspect.signature(renderer.render).parameters: @@ -336,8 +336,6 @@ def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, La logger.exception('Could not render HTML body') body_html = None - body_plain = format_map(body_plain, context, mode=SafeFormatter.MODE_RICH_TO_PLAIN) - send_task = mail_send_task.si( to=[email] if isinstance(email, str) else list(email), cc=cc,
src/tests/base/test_mail.py+181 −2 modified@@ -32,20 +32,22 @@ # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations under the License. +import datetime import os import re +from decimal import Decimal from email.mime.text import MIMEText import pytest from django.conf import settings from django.core import mail as djmail from django.utils.timezone import now from django.utils.translation import gettext_lazy as _ -from django_scopes import scope +from django_scopes import scope, scopes_disabled from i18nfield.strings import LazyI18nString from pretix.base.email import get_email_context -from pretix.base.models import Event, Organizer, User +from pretix.base.models import Event, InvoiceAddress, Order, Organizer, User from pretix.base.services.mail import mail @@ -67,6 +69,45 @@ def env(): yield event, user, o +@pytest.fixture +@scopes_disabled() +def item(env): + return env[0].items.create(name="Budget Ticket", default_price=23) + + +@pytest.fixture +@scopes_disabled() +def order(env, item): + event, _, _ = env + o = Order.objects.create( + code="FOO", + event=event, + email="dummy@dummy.test", + status=Order.STATUS_PENDING, + secret="k24fiuwvu8kxz3y1", + sales_channel=event.organizer.sales_channels.get(identifier="web"), + datetime=datetime.datetime(2017, 12, 1, 10, 0, 0, tzinfo=datetime.UTC), + expires=datetime.datetime(2017, 12, 10, 10, 0, 0, tzinfo=datetime.UTC), + total=23, + locale="en", + ) + o.positions.create( + order=o, + item=item, + variation=None, + price=Decimal("23"), + attendee_email="peter@example.org", + attendee_name_parts={"given_name": "Peter", "family_name": "Miller"}, + secret="z3fsn8jyufm5kpk768q69gkbyr5f4h6w", + pseudonymization_id="ABCDEFGHKL", + ) + InvoiceAddress.objects.create( + order=o, + name_parts={"given_name": "Peter", "family_name": "Miller"}, + ) + return o + + @pytest.mark.django_db def test_send_mail_with_prefix(env): djmail.outbox = [] @@ -286,3 +327,141 @@ def test_placeholder_html_rendering_from_string(env): r'style="[^"]+" target="_blank">Link & Text</a>', html ) + + +@pytest.mark.django_db +def test_nested_placeholder_inclusion_full_process(env, order): + # Test that it is not possible to sneak in a placeholder like {url_cancel} inside a user-controlled + # placeholder value like {invoice_company} + event, user, organizer = env + position = order.positions.get() + order.invoice_address.company = "{url_cancel} Corp" + order.invoice_address.save() + event.settings.mail_text_resend_link = LazyI18nString({"en": "Ticket for {invoice_company}"}) + event.settings.mail_subject_resend_link_attendee = LazyI18nString({"en": "Ticket for {invoice_company}"}) + + djmail.outbox = [] + position.resend_link() + assert len(djmail.outbox) == 1 + assert djmail.outbox[0].to == [position.attendee_email] + assert "Ticket for {url_cancel} Corp" == djmail.outbox[0].subject + assert "/cancel" not in djmail.outbox[0].body + assert "/order" not in djmail.outbox[0].body + html, plain = _extract_html(djmail.outbox[0]), djmail.outbox[0].body + for part in (html, plain): + assert "Ticket for {url_cancel} Corp" in part + assert "/order/" not in part + assert "/cancel" not in part + + +@pytest.mark.django_db +def test_nested_placeholder_inclusion_mail_service(env): + # test that it is not possible to have placeholders within the values of placeholders when + # the mail() function is called directly + template = LazyI18nString("Event name: {event}") + djmail.outbox = [] + event, user, organizer = env + event.name = "event & {currency} co. kg" + event.slug = "event-co-ag-slug" + event.save() + + mail( + "dummy@dummy.dummy", + "{event} Test subject", + template, + get_email_context( + event=event, + payment_info="**IBAN**: 123 \n**BIC**: 456 {event}", + ), + event, + ) + + assert len(djmail.outbox) == 1 + assert djmail.outbox[0].to == [user.email] + html, plain = _extract_html(djmail.outbox[0]), djmail.outbox[0].body + for part in (html, plain, djmail.outbox[0].subject): + assert "event & {currency} co. kg" in part or "event & {currency} co. kg" in part + assert "EUR" not in part + + +@pytest.mark.django_db +@pytest.mark.parametrize("tpl", [ + "Event: {event.__class__}", + "Event: {{event.__class__}}", + "Event: {{{event.__class__}}}", +]) +def test_variable_inclusion_from_string_full_process(env, tpl, order): + # Test that it is not possible to use placeholders that leak system information in templates + # when run through system processes + event, user, organizer = env + event.name = "event & co. kg" + event.save() + position = order.positions.get() + event.settings.mail_text_resend_link = LazyI18nString({"en": tpl}) + event.settings.mail_subject_resend_link_attendee = LazyI18nString({"en": tpl}) + + position.resend_link() + assert len(djmail.outbox) == 1 + html, plain = _extract_html(djmail.outbox[0]), djmail.outbox[0].body + for part in (html, plain, djmail.outbox[0].subject): + assert "{event.__class__}" in part + assert "LazyI18nString" not in part + + +@pytest.mark.django_db +@pytest.mark.parametrize("tpl", [ + "Event: {event.__class__}", + "Event: {{event.__class__}}", + "Event: {{{event.__class__}}}", +]) +def test_variable_inclusion_from_string_mail_service(env, tpl): + # Test that it is not possible to use placeholders that leak system information in templates + # when run through mail() directly + event, user, organizer = env + event.name = "event & co. kg" + event.save() + + djmail.outbox = [] + mail( + "dummy@dummy.dummy", + tpl, + LazyI18nString(tpl), + get_email_context( + event=event, + payment_info="**IBAN**: 123 \n**BIC**: 456\n" + tpl, + ), + event, + ) + assert len(djmail.outbox) == 1 + html, plain = _extract_html(djmail.outbox[0]), djmail.outbox[0].body + for part in (html, plain, djmail.outbox[0].subject): + assert "{event.__class__}" in part + assert "LazyI18nString" not in part + + +@pytest.mark.django_db +def test_escaped_braces_mail_services(env): + # Test that braces can be escaped by doubling + template = LazyI18nString("Event name: -{{currency}}-") + djmail.outbox = [] + event, user, organizer = env + event.name = "event & co. kg" + event.save() + + mail( + "dummy@dummy.dummy", + "-{{currency}}- Test subject", + template, + get_email_context( + event=event, + payment_info="**IBAN**: 123 \n**BIC**: 456 {event}", + ), + event, + ) + + assert len(djmail.outbox) == 1 + assert djmail.outbox[0].to == [user.email] + html, plain = _extract_html(djmail.outbox[0]), djmail.outbox[0].body + for part in (html, plain, djmail.outbox[0].subject): + assert "EUR" not in part + assert "-{currency}-" in part
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
7- github.com/advisories/GHSA-r8p8-qw9w-j9qvghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2026-2415ghsaADVISORY
- pretix.eu/about/en/blog/20260216-release-2026-1-1/mitrevendor-advisory
- github.com/pretix/pretix/commit/ba11d24f8dfa4e9d8f03493e56fd8b43983fe297ghsaWEB
- github.com/pretix/pretix/commit/c85afbc621b5f0b1afa618627c45f89323eb0154ghsaWEB
- github.com/pretix/pretix/commit/edac35ed4c5466eb63a202575c337d117ddf1c8eghsaWEB
- pretix.eu/about/en/blog/20260216-release-2026-1-1ghsaWEB
News mentions
0No linked articles in our index yet.