Compare commits

...

5 Commits

Author SHA1 Message Date
Raphael Michel
013da0a6aa Bump to 4.6.1 2022-02-28 16:10:23 +01:00
Raphael Michel
a7c59374db Pin django-countries to 7.2 2022-02-28 16:10:18 +01:00
Raphael Michel
23e1dac5da [SECURITY] Fix stored XSS in help texts 2022-02-28 16:10:18 +01:00
Raphael Michel
5cd8845728 [SECURITY] Fix stored XSS in question errors 2022-02-28 16:10:18 +01:00
Raphael Michel
34dfc35032 [SECURITY] Prevent untrusted values from creating Excel formulas 2022-02-28 16:10:18 +01:00
9 changed files with 171 additions and 30 deletions

View File

@@ -19,4 +19,4 @@
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
__version__ = "4.6.0"
__version__ = "4.6.1"

View File

@@ -33,7 +33,6 @@
# License for the specific language governing permissions and limitations under the License.
import io
import re
import tempfile
from collections import OrderedDict, namedtuple
from decimal import Decimal
@@ -46,26 +45,13 @@ from django.conf import settings
from django.db.models import QuerySet
from django.utils.formats import localize
from django.utils.translation import gettext, gettext_lazy as _
from openpyxl import Workbook
from openpyxl.cell.cell import ILLEGAL_CHARACTERS_RE, KNOWN_TYPES, Cell
from pretix.base.models import Event
from pretix.helpers.safe_openpyxl import ( # NOQA: backwards compatibility for plugins using excel_safe
SafeWorkbook, remove_invalid_excel_chars as excel_safe,
)
def excel_safe(val):
if isinstance(val, Cell):
return val
if not isinstance(val, KNOWN_TYPES):
val = str(val)
if isinstance(val, bytes):
val = val.decode("utf-8", errors="ignore")
if isinstance(val, str):
val = re.sub(ILLEGAL_CHARACTERS_RE, '', val)
return val
__ = excel_safe # just so the compatbility import above is "used" and doesn't get removed by linter
class BaseExporter:
@@ -228,7 +214,7 @@ class ListExporter(BaseExporter):
pass
def _render_xlsx(self, form_data, output_file=None):
wb = Workbook(write_only=True)
wb = SafeWorkbook(write_only=True)
ws = wb.create_sheet()
self.prepare_xlsx_sheet(ws)
try:
@@ -242,7 +228,7 @@ class ListExporter(BaseExporter):
total = line.total
continue
ws.append([
excel_safe(val) for val in line
val for val in line
])
if total:
counter += 1
@@ -347,7 +333,7 @@ class MultiSheetListExporter(ListExporter):
return self.get_filename() + '.csv', 'text/csv', output.getvalue().encode("utf-8")
def _render_xlsx(self, form_data, output_file=None):
wb = Workbook(write_only=True)
wb = SafeWorkbook(write_only=True)
n_sheets = len(self.sheets)
for i_sheet, (s, l) in enumerate(self.sheets):
ws = wb.create_sheet(str(l))
@@ -361,8 +347,7 @@ class MultiSheetListExporter(ListExporter):
total = line.total
continue
ws.append([
excel_safe(val)
for val in line
val for val in line
])
if total:
counter += 1

View File

@@ -692,7 +692,7 @@ class BaseQuestionsForm(forms.Form):
label=label, required=required,
min_value=q.valid_number_min or Decimal('0.00'),
max_value=q.valid_number_max,
help_text=q.help_text,
help_text=help_text,
initial=initial.answer if initial else None,
)
elif q.type == Question.TYPE_STRING:

View File

@@ -0,0 +1,113 @@
import re
from inspect import isgenerator
from openpyxl import Workbook
from openpyxl.cell.cell import (
ILLEGAL_CHARACTERS_RE, KNOWN_TYPES, TIME_TYPES, TYPE_FORMULA, TYPE_STRING,
Cell,
)
from openpyxl.compat import NUMERIC_TYPES
from openpyxl.utils import column_index_from_string
from openpyxl.utils.exceptions import ReadOnlyWorkbookException
from openpyxl.worksheet._write_only import WriteOnlyWorksheet
from openpyxl.worksheet.worksheet import Worksheet
SAFE_TYPES = NUMERIC_TYPES + TIME_TYPES + (bool, type(None))
"""
This module provides a safer version of openpyxl's `Workbook` class to generate XLSX files from
user-generated data using `WriteOnlyWorksheet` and `ws.append()`. We commonly use these methods
to output e.g. order data, which contains data from untrusted sources such as attendee names.
There are mainly two problems this solves:
- It makes sure strings starting with = are treated as text, not as a formula, as openpyxl will
otherwise assume, which can be used for remote code execution.
- It removes characters considered invalid by Excel to avoid exporter crashes.
"""
def remove_invalid_excel_chars(val):
if isinstance(val, Cell):
return val
if not isinstance(val, KNOWN_TYPES):
val = str(val)
if isinstance(val, bytes):
val = val.decode("utf-8", errors="ignore")
if isinstance(val, str):
val = re.sub(ILLEGAL_CHARACTERS_RE, '', val)
return val
def SafeCell(*args, value=None, **kwargs):
value = remove_invalid_excel_chars(value)
c = Cell(*args, value=value, **kwargs)
if c.data_type == TYPE_FORMULA:
c.data_type = TYPE_STRING
return c
class SafeAppendMixin:
def append(self, iterable):
row_idx = self._current_row + 1
if isinstance(iterable, (list, tuple, range)) or isgenerator(iterable):
for col_idx, content in enumerate(iterable, 1):
if isinstance(content, Cell):
# compatible with write-only mode
cell = content
if cell.parent and cell.parent != self:
raise ValueError("Cells cannot be copied from other worksheets")
cell.parent = self
cell.column = col_idx
cell.row = row_idx
else:
cell = SafeCell(self, row=row_idx, column=col_idx, value=remove_invalid_excel_chars(content))
self._cells[(row_idx, col_idx)] = cell
elif isinstance(iterable, dict):
for col_idx, content in iterable.items():
if isinstance(col_idx, str):
col_idx = column_index_from_string(col_idx)
cell = SafeCell(self, row=row_idx, column=col_idx, value=content)
self._cells[(row_idx, col_idx)] = cell
else:
self._invalid_row(iterable)
self._current_row = row_idx
class SafeWriteOnlyWorksheet(SafeAppendMixin, WriteOnlyWorksheet):
pass
class SafeWorksheet(SafeAppendMixin, Worksheet):
pass
class SafeWorkbook(Workbook):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if self._sheets:
# monkeypatch existing sheets
for s in self._sheets:
s.append = SafeAppendMixin.append
def create_sheet(self, title=None, index=None):
if self.read_only:
raise ReadOnlyWorkbookException('Cannot create new sheet in a read-only workbook')
if self.write_only:
new_ws = SafeWriteOnlyWorksheet(parent=self, title=title)
else:
new_ws = SafeWorksheet(parent=self, title=title)
self._add_sheet(sheet=new_ws, index=index)
return new_ws

View File

@@ -47,6 +47,7 @@ from pretix.base.forms.questions import (
BaseInvoiceAddressForm, BaseQuestionsForm, WrappedPhoneNumberPrefixWidget,
guess_phone_prefix,
)
from pretix.base.templatetags.rich_text import rich_text
from pretix.base.validators import EmailBanlistValidator
from pretix.presale.signals import contact_form_fields
@@ -82,7 +83,7 @@ class ContactForm(forms.Form):
self.fields['phone'] = PhoneNumberField(
label=_('Phone number'),
required=self.event.settings.order_phone_required,
help_text=self.event.settings.checkout_phone_helptext,
help_text=rich_text(self.event.settings.checkout_phone_helptext),
widget=WrappedPhoneNumberPrefixWidget()
)
@@ -91,7 +92,7 @@ class ContactForm(forms.Form):
# is an autofocus field. Who would have thought… See e.g. here:
# https://floatboxjs.com/forum/topic.php?post=8440&usebb_sid=2e116486a9ec6b7070e045aea8cded5b#post8440
self.fields['email'].widget.attrs['autofocus'] = 'autofocus'
self.fields['email'].help_text = self.event.settings.checkout_email_helptext
self.fields['email'].help_text = rich_text(self.event.settings.checkout_email_helptext)
responses = contact_form_fields.send(self.event, request=self.request)
for r, response in responses:

View File

@@ -28,6 +28,7 @@ from pretix.base.forms.questions import (
NamePartsFormField, WrappedPhoneNumberPrefixWidget, guess_phone_prefix,
)
from pretix.base.models import Quota, WaitingListEntry
from pretix.base.templatetags.rich_text import rich_text
from pretix.presale.views.event import get_grouped_items
@@ -99,7 +100,7 @@ class WaitingListForm(forms.ModelForm):
self.fields['phone'] = PhoneNumberField(
label=_("Phone number"),
required=event.settings.waiting_list_phones_required,
help_text=event.settings.waiting_list_phones_explanation_text,
help_text=rich_text(event.settings.waiting_list_phones_explanation_text),
widget=WrappedPhoneNumberPrefixWidget()
)
else:

View File

@@ -219,7 +219,10 @@ $(function () {
// multi-input fields have a role=group with aria-labelledby
var label = this.hasAttribute("aria-labelledby") ? $("#" + this.getAttribute("aria-labelledby")) : $("[for="+target.attr("id")+"]");
content.append("<li><a href='#" + target.attr("id") + "'>" + label.get(0).childNodes[0].nodeValue + "</a>: "+desc.text()+"</li>");
var $li = $("<li>");
$li.text(": " + desc.text())
$li.prepend($("<a>").attr("href", "#" + target.attr("id")).text(label.get(0).childNodes[0].nodeValue))
content.append($li);
});
$(this).append(content);
});

View File

@@ -172,7 +172,7 @@ setup(
'Django==3.2.*',
'django-bootstrap3==15.0.*',
'django-compressor==2.4.*',
'django-countries>=7.2',
'django-countries==7.2.*',
'django-filter==21.1',
'django-formset-js-improved==0.5.0.2',
'django-formtools==2.3',

View File

@@ -0,0 +1,38 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-2021 rami.io GmbH and contributors
#
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
# Public License as published by the Free Software Foundation in version 3 of the License.
#
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
# this file, see <https://pretix.eu/about/en/license>.
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
from openpyxl.cell.cell import TYPE_STRING
from pretix.helpers.safe_openpyxl import SafeWorkbook
def test_nullbyte_removed():
wb = SafeWorkbook()
ws = wb.create_sheet()
ws.append(["foo\u0000bar"])
assert ws.cell(1, 1).value == "foobar"
def test_no_formulas():
wb = SafeWorkbook()
ws = wb.create_sheet()
ws.append(["=1+1"])
assert ws.cell(1, 1).data_type == TYPE_STRING