forked from CGM_Public/pretix_original
SECURITY: Prevent placeholder injcetion in plaintext emails
This commit is contained in:
committed by
Raphael Michel
parent
d1686df07c
commit
ae6014708b
@@ -256,7 +256,7 @@ def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, La
|
|||||||
_autoextend_context(context, order)
|
_autoextend_context(context, order)
|
||||||
|
|
||||||
# Build raw content
|
# Build raw content
|
||||||
body_plain = render_mail(template, context, placeholder_mode=SafeFormatter.MODE_RICH_TO_PLAIN)
|
body_plain = render_mail(template, context, placeholder_mode=None)
|
||||||
if settings_holder:
|
if settings_holder:
|
||||||
signature = str(settings_holder.settings.get('mail_text_signature'))
|
signature = str(settings_holder.settings.get('mail_text_signature'))
|
||||||
else:
|
else:
|
||||||
@@ -267,7 +267,8 @@ def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, La
|
|||||||
body_plain = format_map(body_plain, context, mode=SafeFormatter.MODE_RICH_TO_PLAIN)
|
body_plain = format_map(body_plain, context, mode=SafeFormatter.MODE_RICH_TO_PLAIN)
|
||||||
|
|
||||||
# Build subject
|
# Build subject
|
||||||
subject = str(subject).format_map(TolerantDict(context))
|
subject = format_map(subject, context)
|
||||||
|
|
||||||
subject = raw_subject = subject.replace('\n', ' ').replace('\r', '')[:900]
|
subject = raw_subject = subject.replace('\n', ' ').replace('\r', '')[:900]
|
||||||
if settings_holder:
|
if settings_holder:
|
||||||
subject = prefix_subject(settings_holder, subject)
|
subject = prefix_subject(settings_holder, subject)
|
||||||
|
|||||||
@@ -32,8 +32,10 @@
|
|||||||
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
# 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.
|
# License for the specific language governing permissions and limitations under the License.
|
||||||
|
|
||||||
|
import datetime
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
from decimal import Decimal
|
||||||
from email.mime.text import MIMEText
|
from email.mime.text import MIMEText
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
@@ -42,11 +44,13 @@ from django.core import mail as djmail
|
|||||||
from django.test import override_settings
|
from django.test import override_settings
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
from django.utils.translation import gettext_lazy as _
|
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 i18nfield.strings import LazyI18nString
|
||||||
|
|
||||||
from pretix.base.email import get_email_context
|
from pretix.base.email import get_email_context
|
||||||
from pretix.base.models import Event, Organizer, OutgoingMail, User
|
from pretix.base.models import (
|
||||||
|
Event, InvoiceAddress, Order, Organizer, OutgoingMail, User,
|
||||||
|
)
|
||||||
from pretix.base.services.mail import mail, mail_send_task
|
from pretix.base.services.mail import mail, mail_send_task
|
||||||
|
|
||||||
|
|
||||||
@@ -68,6 +72,45 @@ def env():
|
|||||||
yield event, user, o
|
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
|
@pytest.mark.django_db
|
||||||
def test_send_mail_with_prefix(env):
|
def test_send_mail_with_prefix(env):
|
||||||
djmail.outbox = []
|
djmail.outbox = []
|
||||||
@@ -377,3 +420,141 @@ def test_placeholder_html_rendering_from_string(env):
|
|||||||
r'style="[^"]+" target="_blank">Link & Text</a>',
|
r'style="[^"]+" target="_blank">Link & Text</a>',
|
||||||
html
|
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
|
||||||
|
|||||||
Reference in New Issue
Block a user