VYPR
High severityNVD Advisory· Published Feb 16, 2026· Updated Feb 17, 2026

Unsafe variable evaluation in email templates

CVE-2026-2415

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.

PackageAffected versionsPatched versions
pretixPyPI
>= 2026.1.0, < 2026.1.12026.1.1
pretixPyPI
>= 2025.10.0, < 2025.10.22025.10.2
pretixPyPI
< 2025.9.42025.9.4

Affected products

1

Patches

3
c85afbc621b5

Fix placeholder injection with django templates

https://github.com/pretix/pretixRaphael MichelFeb 13, 2026via ghsa
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 '&lt;' not in djmail.outbox[0].body
    -    assert '&amp;' 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 '&lt;' not in djmail.outbox[0].body
    +    # todo: assert '&amp;' 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: &lt;strong&gt;event &amp; co. kg&lt;/strong&gt;' in html
    +    assert 'Event name: &lt;strong&gt;event &amp; co. kg&lt;/strong&gt; {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">&lt;strong&gt;event &amp; co. kg&lt;/strong&gt;</a>',
    -        html
    -    )
    -    assert re.search(
    -        r'Other website: <a href="https://example.com" rel="noopener" style="[^"]+" target="_blank">&lt;strong&gt;event &amp; co. kg&lt;/strong&gt;</a>',
    +        r'Event website: <a href="https://example.org/dummy" rel="noopener" style="[^"]+" target="_blank">'
    +        r'&lt;strong&gt;event &amp; co. kg&lt;/strong&gt; {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">&lt;strong&gt;event &amp; co. kg&lt;/strong&gt;</a>',
    +        r'Event website: <a href="https://example.org/dummy" rel="noopener" style="[^"]+" target="_blank">'
    +        r'&lt;strong&gt;event &amp; co. kg&lt;/strong&gt; {currency}</a>',
             html
         )
         assert re.search(
    -        r'Other website: <a href="https://example.com" rel="noopener" style="[^"]+" target="_blank">&lt;strong&gt;event &amp; co. kg&lt;/strong&gt;</a>',
    +        r'Other website: <a href="https://example.com" rel="noopener" style="[^"]+" target="_blank">'
    +        r'&lt;strong&gt;event &amp; co. kg&lt;/strong&gt; {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
    
edac35ed4c54

Mark strings as formatted to prevent double-formatting

https://github.com/pretix/pretixRaphael MichelFeb 13, 2026via ghsa
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)
    +    )
    
ba11d24f8dfa

SECURITY: Prevent placeholder injcetion in plaintext emails

https://github.com/pretix/pretixKara EngelhardtFeb 12, 2026via ghsa
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 &amp; 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 &amp; {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

News mentions

0

No linked articles in our index yet.