Make str.format_map with untrusted input safer (#2931)

This commit is contained in:
Raphael Michel
2022-12-08 13:49:07 +01:00
committed by GitHub
parent 11eecd739d
commit b64c5735a8
12 changed files with 89 additions and 42 deletions

View File

@@ -80,6 +80,7 @@ from pretix.base.settings import PERSON_NAME_SCHEMES
from pretix.base.signals import order_gracefully_delete
from ...helpers.countries import CachedCountries, FastCountryField
from ...helpers.format import format_map
from ._transactions import (
_fail, _transactions_mark_order_clean, _transactions_mark_order_dirty,
)
@@ -996,7 +997,7 @@ class Order(LockModel, LoggedModel):
position and the attendee email will be used if available.
"""
from pretix.base.services.mail import (
SendMailException, TolerantDict, mail, render_mail,
SendMailException, mail, render_mail,
)
if not self.email and not (position and position.attendee_email):
@@ -1012,7 +1013,7 @@ class Order(LockModel, LoggedModel):
try:
email_content = render_mail(template, context)
subject = str(subject).format_map(TolerantDict(context))
subject = format_map(subject, context)
mail(
recipient, subject, template, context,
self.event, self.locale, self, headers=headers, sender=sender,
@@ -2414,7 +2415,7 @@ class OrderPosition(AbstractPosition):
:param attach_ical: Attach relevant ICS files
"""
from pretix.base.services.mail import (
SendMailException, TolerantDict, mail, render_mail,
SendMailException, mail, render_mail,
)
if not self.attendee_email:
@@ -2427,7 +2428,7 @@ class OrderPosition(AbstractPosition):
recipient = self.attendee_email
try:
email_content = render_mail(template, context)
subject = str(subject).format_map(TolerantDict(context))
subject = format_map(subject, context)
mail(
recipient, subject, template, context,
self.event, self.order.locale, order=self.order, headers=headers, sender=sender,

View File

@@ -68,6 +68,7 @@ from pretix.base.signals import register_payment_providers
from pretix.base.templatetags.money import money_filter
from pretix.base.templatetags.rich_text import rich_text
from pretix.helpers.countries import CachedCountries
from pretix.helpers.format import format_map
from pretix.helpers.money import DecimalTextInput
from pretix.multidomain.urlreverse import build_absolute_uri
from pretix.presale.views import get_cart, get_cart_total
@@ -1122,12 +1123,12 @@ class ManualPayment(BasePaymentProvider):
}
def order_pending_mail_render(self, order, payment) -> str:
msg = str(self.settings.get('email_instructions', as_type=LazyI18nString)).format_map(self.format_map(order, payment))
msg = format_map(self.settings.get('email_instructions', as_type=LazyI18nString), self.format_map(order, payment))
return msg
def payment_pending_render(self, request, payment) -> str:
return rich_text(
str(self.settings.get('pending_description', as_type=LazyI18nString)).format_map(self.format_map(payment.order, payment))
format_map(self.settings.get('pending_description', as_type=LazyI18nString), self.format_map(payment.order, payment))
)

View File

@@ -35,12 +35,13 @@ from pretix.base.models import (
SubEvent, User, WaitingListEntry,
)
from pretix.base.services.locking import LockTimeoutException
from pretix.base.services.mail import SendMailException, TolerantDict, mail
from pretix.base.services.mail import SendMailException, mail
from pretix.base.services.orders import (
OrderChangeManager, OrderError, _cancel_order, _try_auto_refund,
)
from pretix.base.services.tasks import ProfiledEventTask
from pretix.celery_app import app
from pretix.helpers.format import format_map
logger = logging.getLogger(__name__)
@@ -51,7 +52,7 @@ def _send_wle_mail(wle: WaitingListEntry, subject: LazyI18nString, message: Lazy
try:
mail(
wle.email,
str(subject).format_map(TolerantDict(email_context)),
format_map(subject, email_context),
message,
email_context,
wle.event,
@@ -71,7 +72,7 @@ def _send_mail(order: Order, subject: LazyI18nString, message: LazyI18nString, s
email_context = get_email_context(event_or_subevent=subevent or order.event, refund_amount=refund_amount,
order=order, position_or_address=ia, event=order.event)
real_subject = str(subject).format_map(TolerantDict(email_context))
real_subject = format_map(subject, email_context)
try:
order.send_mail(
real_subject, message, email_context,
@@ -86,7 +87,7 @@ def _send_mail(order: Order, subject: LazyI18nString, message: LazyI18nString, s
continue
if p.addon_to_id is None and p.attendee_email and p.attendee_email != order.email:
real_subject = str(subject).format_map(TolerantDict(email_context))
real_subject = format_map(subject, email_context)
email_context = get_email_context(event_or_subevent=p.subevent or order.event,
event=order.event,
refund_amount=refund_amount,

View File

@@ -76,6 +76,7 @@ from pretix.base.services.tasks import TransactionAwareTask
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 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
@@ -85,6 +86,7 @@ INVALID_ADDRESS = 'invalid-pretix-mail-address'
class TolerantDict(dict):
# kept for backwards compatibility with plugins
def __missing__(self, key):
return key
@@ -109,7 +111,7 @@ def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, La
:param template: The filename of a template to be used. It will be rendered with the locale given in the locale
argument and the context given in the next argument. Alternatively, you can pass a LazyI18nString and
``context`` will be used as the argument to a Python ``.format_map()`` call on the template.
``context`` will be used as the argument to a ``pretix.helpers.format.format_map(template, context)`` call on the template.
:param context: The context for rendering the template (see ``template`` parameter)
@@ -177,7 +179,7 @@ def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, La
})
renderer = ClassicMailRenderer(None, organizer)
content_plain = body_plain = render_mail(template, context)
subject = str(subject).format_map(TolerantDict(context))
subject = format_map(str(subject), context)
sender = (
sender or
(event.settings.get('mail_from') if event else None) or
@@ -608,7 +610,7 @@ def render_mail(template, context):
if isinstance(template, LazyI18nString):
body = str(template)
if context:
body = body.format_map(TolerantDict(context))
body = format_map(body, context)
else:
tpl = get_template(template)
body = tpl.render(context)

View File

@@ -93,8 +93,8 @@ from ...base.i18n import language
from ...base.models.items import (
Item, ItemCategory, ItemMetaProperty, Question, Quota,
)
from ...base.services.mail import TolerantDict
from ...base.settings import SETTINGS_AFFECTING_CSS, LazyI18nStringList
from ...helpers.format import format_map
from ..logdisplay import OVERVIEW_BANLIST
from . import CreateView, PaginationMixin, UpdateView
@@ -734,10 +734,10 @@ class MailSettingsPreview(EventPermissionRequiredMixin, View):
if idx in self.supported_locale:
with language(self.supported_locale[idx], self.request.event.settings.region):
if k.startswith('mail_subject_'):
msgs[self.supported_locale[idx]] = bleach.clean(v).format_map(self.placeholders(preview_item))
msgs[self.supported_locale[idx]] = format_map(bleach.clean(v), self.placeholders(preview_item))
else:
msgs[self.supported_locale[idx]] = markdown_compile_email(
v.format_map(self.placeholders(preview_item))
format_map(v, self.placeholders(preview_item))
)
return JsonResponse({
@@ -761,7 +761,7 @@ class MailSettingsRendererPreview(MailSettingsPreview):
def get(self, request, *args, **kwargs):
v = str(request.event.settings.mail_text_order_placed)
v = v.format_map(TolerantDict(self.placeholders('mail_text_order_placed')))
v = format_map(v, self.placeholders('mail_text_order_placed'))
renderers = request.event.get_html_mail_renderers()
if request.GET.get('renderer') in renderers:
with rolledback_transaction():

View File

@@ -93,9 +93,7 @@ from pretix.base.services.invoices import (
invoice_qualified, regenerate_invoice,
)
from pretix.base.services.locking import LockTimeoutException
from pretix.base.services.mail import (
SendMailException, TolerantDict, render_mail,
)
from pretix.base.services.mail import SendMailException, render_mail
from pretix.base.services.orders import (
OrderChangeManager, OrderError, approve_order, cancel_order, deny_order,
extend_order, mark_order_expired, mark_order_refunded,
@@ -127,6 +125,7 @@ from pretix.control.forms.orders import (
from pretix.control.permissions import EventPermissionRequiredMixin
from pretix.control.signals import order_search_forms
from pretix.control.views import PaginationMixin
from pretix.helpers.format import format_map
from pretix.helpers.safedownload import check_token
from pretix.presale.signals import question_form_fields
@@ -2032,7 +2031,7 @@ class OrderSendMail(EventPermissionRequiredMixin, OrderViewMixin, FormView):
with language(order.locale, self.request.event.settings.region):
email_context = get_email_context(event=order.event, order=order)
email_template = LazyI18nString(form.cleaned_data['message'])
email_subject = str(form.cleaned_data['subject']).format_map(TolerantDict(email_context))
email_subject = format_map(str(form.cleaned_data['subject']), email_context)
email_content = render_mail(email_template, email_context)
if self.request.POST.get('action') == 'preview':
self.preview_output = {
@@ -2097,7 +2096,7 @@ class OrderPositionSendMail(OrderSendMail):
with language(position.order.locale, self.request.event.settings.region):
email_context = get_email_context(event=position.order.event, order=position.order, position=position)
email_template = LazyI18nString(form.cleaned_data['message'])
email_subject = str(form.cleaned_data['subject']).format_map(TolerantDict(email_context))
email_subject = format_map(str(form.cleaned_data['subject']), email_context)
email_content = render_mail(email_template, email_context)
if self.request.POST.get('action') == 'preview':
self.preview_output = {

View File

@@ -109,6 +109,7 @@ from pretix.control.views import PaginationMixin
from pretix.control.views.mailsetup import MailSettingsSetupView
from pretix.helpers import GroupConcat
from pretix.helpers.dicts import merge_dicts
from pretix.helpers.format import format_map
from pretix.helpers.urls import build_absolute_uri as build_global_uri
from pretix.multidomain.urlreverse import build_absolute_uri
from pretix.presale.forms.customer import TokenGenerator
@@ -335,10 +336,10 @@ class MailSettingsPreview(OrganizerPermissionRequiredMixin, View):
if idx in self.supported_locale:
with language(self.supported_locale[idx], self.request.organizer.settings.region):
if k.startswith('mail_subject_'):
msgs[self.supported_locale[idx]] = bleach.clean(v).format_map(self.placeholders(preview_item))
msgs[self.supported_locale[idx]] = format_map(bleach.clean(v), self.placeholders(preview_item))
else:
msgs[self.supported_locale[idx]] = markdown_compile_email(
v.format_map(self.placeholders(preview_item))
format_map(v, self.placeholders(preview_item))
)
return JsonResponse({

View File

@@ -0,0 +1,34 @@
import logging
from string import Formatter
logger = logging.getLogger(__name__)
class SafeFormatter(Formatter):
"""
Customized version of ``str.format`` that (a) behaves just like ``str.format_map`` and
(b) does not allow any unwanted shenanigans like attribute access or format specifiers.
"""
def __init__(self, context):
self.context = context
def get_field(self, field_name, args, kwargs):
if '.' in field_name or '[' in field_name:
logger.warning(f'Ignored invalid field name "{field_name}"')
return ('{' + str(field_name) + '}', field_name)
return super().get_field(field_name, args, kwargs)
def get_value(self, key, args, kwargs):
if key not in self.context:
return '{' + str(key) + '}'
return self.context[key]
def format_field(self, value, format_spec):
# Ignore format _spec
return super().format_field(value, '')
def format_map(template, context):
if not isinstance(template, str):
template = str(template)
return SafeFormatter(context).format(template)

View File

@@ -40,6 +40,7 @@ from pretix.base.models import Checkin, Event, InvoiceAddress, Order, User
from pretix.base.services.mail import SendMailException, mail
from pretix.base.services.tasks import ProfiledEventTask
from pretix.celery_app import app
from pretix.helpers.format import format_map
@app.task(base=ProfiledEventTask, acks_late=True)
@@ -116,8 +117,8 @@ def send_mails(event: Event, user: int, subject: dict, message: dict, orders: li
user=user,
data={
'position': p.positionid,
'subject': subject.localize(o.locale).format_map(email_context),
'message': message.localize(o.locale).format_map(email_context),
'subject': format_map(subject.localize(o.locale), email_context),
'message': format_map(message.localize(o.locale), email_context),
'recipient': p.attendee_email
}
)
@@ -143,8 +144,8 @@ def send_mails(event: Event, user: int, subject: dict, message: dict, orders: li
'pretix.plugins.sendmail.order.email.sent',
user=user,
data={
'subject': subject.localize(o.locale).format_map(email_context),
'message': message.localize(o.locale).format_map(email_context),
'subject': format_map(subject.localize(o.locale), email_context),
'message': format_map(message.localize(o.locale), email_context),
'recipient': o.email
}
)

View File

@@ -52,12 +52,12 @@ from pretix.base.email import get_available_placeholders
from pretix.base.i18n import LazyI18nString, language
from pretix.base.models import Checkin, LogEntry, Order, OrderPosition
from pretix.base.models.event import SubEvent
from pretix.base.services.mail import TolerantDict
from pretix.base.templatetags.rich_text import markdown_compile_email
from pretix.control.permissions import EventPermissionRequiredMixin
from pretix.control.views import CreateView, PaginationMixin, UpdateView
from pretix.plugins.sendmail.tasks import send_mails
from ...helpers.format import format_map
from . import forms
from .models import Rule, ScheduledMail
@@ -203,7 +203,7 @@ class SenderView(EventPermissionRequiredMixin, FormView):
if self.request.POST.get("action") != "send":
for l in self.request.event.settings.locales:
with language(l, self.request.event.settings.region):
context_dict = TolerantDict()
context_dict = {}
for k, v in get_available_placeholders(self.request.event, ['event', 'order',
'position_or_address']).items():
context_dict[k] = '<span class="placeholder" title="{}">{}</span>'.format(
@@ -212,9 +212,9 @@ class SenderView(EventPermissionRequiredMixin, FormView):
)
subject = bleach.clean(form.cleaned_data['subject'].localize(l), tags=[])
preview_subject = subject.format_map(context_dict)
preview_subject = format_map(subject, context_dict)
message = form.cleaned_data['message'].localize(l)
preview_text = markdown_compile_email(message.format_map(context_dict))
preview_text = markdown_compile_email(format_map(message, context_dict))
self.output[l] = {
'subject': _('Subject: {subject}').format(subject=preview_subject),
@@ -350,7 +350,7 @@ class CreateRule(EventPermissionRequiredMixin, CreateView):
if self.request.POST.get("action") == "preview":
for l in self.request.event.settings.locales:
with language(l, self.request.event.settings.region):
context_dict = TolerantDict()
context_dict = {}
for k, v in get_available_placeholders(self.request.event, ['event', 'order',
'position_or_address']).items():
context_dict[k] = '<span class="placeholder" title="{}">{}</span>'.format(
@@ -359,9 +359,9 @@ class CreateRule(EventPermissionRequiredMixin, CreateView):
)
subject = bleach.clean(form.cleaned_data['subject'].localize(l), tags=[])
preview_subject = subject.format_map(context_dict)
preview_subject = format_map(subject, context_dict)
template = form.cleaned_data['template'].localize(l)
preview_text = markdown_compile_email(template.format_map(context_dict))
preview_text = markdown_compile_email(format_map(template, context_dict))
self.output[l] = {
'subject': _('Subject: {subject}').format(subject=preview_subject),
@@ -427,7 +427,7 @@ class UpdateRule(EventPermissionRequiredMixin, UpdateView):
for lang in self.request.event.settings.locales:
with language(lang, self.request.event.settings.region):
placeholders = TolerantDict()
placeholders = {}
for k, v in get_available_placeholders(self.request.event, ['event', 'order', 'position_or_address']).items():
placeholders[k] = '<span class="placeholder" title="{}">{}</span>'.format(
_('This value will be replaced based on dynamic parameters.'),
@@ -435,9 +435,9 @@ class UpdateRule(EventPermissionRequiredMixin, UpdateView):
)
subject = bleach.clean(self.object.subject.localize(lang), tags=[])
preview_subject = subject.format_map(placeholders)
preview_subject = format_map(subject, placeholders)
template = self.object.template.localize(lang)
preview_text = markdown_compile_email(template.format_map(placeholders))
preview_text = markdown_compile_email(format_map(template, placeholders))
o[lang] = {
'subject': _('Subject: {subject}'.format(subject=preview_subject)),

View File

@@ -30,6 +30,7 @@ from django.utils.translation import gettext as _
from pretix.base.email import get_email_context
from pretix.base.models import Event
from pretix.helpers.format import format_map
from pretix.multidomain.urlreverse import build_absolute_uri
@@ -112,9 +113,6 @@ def get_private_icals(event, positions):
- It would be pretty hard to implement it in a way that doesn't require us to use distinct
settings fields for emails to customers and to attendees, which feels like an overcomplication.
"""
from pretix.base.services.mail import TolerantDict
tz = pytz.timezone(event.settings.timezone)
creation_time = datetime.datetime.now(pytz.utc)
@@ -131,7 +129,7 @@ def get_private_icals(event, positions):
if event.settings.mail_attach_ical_description:
ctx = get_email_context(event=event, event_or_subevent=ev)
description = str(event.settings.mail_attach_ical_description).format_map(TolerantDict(ctx))
description = format_map(str(event.settings.mail_attach_ical_description), ctx)
else:
# Default description
descr = []

View File

@@ -0,0 +1,9 @@
from pretix.helpers.format import format_map
def test_format_map():
assert format_map("Foo {bar}", {"bar": 3}) == "Foo 3"
assert format_map("Foo {baz}", {"bar": 3}) == "Foo {baz}"
assert format_map("Foo {bar.__module__}", {"bar": 3}) == "Foo {bar.__module__}"
assert format_map("Foo {bar!s}", {"bar": 3}) == "Foo 3"
assert format_map("Foo {bar:<20}", {"bar": 3}) == "Foo 3"