Compare commits

..

1 Commits

Author SHA1 Message Date
Raphael Michel
614408f5ae Add default protection for SSRF 2026-03-26 14:05:41 +01:00
75 changed files with 16286 additions and 17456 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__ = "2026.4.0.dev0"
__version__ = "2026.3.0.dev0"

View File

@@ -196,7 +196,8 @@ class RegistrationForm(forms.Form):
def clean_password(self):
password1 = self.cleaned_data.get('password', '')
user = User(email=self.cleaned_data.get('email'))
validate_password(password1, user=user)
if validate_password(password1, user=user) is not None:
raise forms.ValidationError(_(password_validators_help_texts()), code='pw_invalid')
return password1
def clean_email(self):

View File

@@ -411,7 +411,7 @@ def mail_send_task(self, **kwargs) -> bool:
try:
outgoing_mail = OutgoingMail.objects.select_for_update(of=OF_SELF).get(pk=outgoing_mail)
except OutgoingMail.DoesNotExist:
logger.info(f"Ignoring job for non existing email {outgoing_mail}")
logger.info(f"Ignoring job for non existing email {outgoing_mail.guid}")
return False
if outgoing_mail.status == OutgoingMail.STATUS_INFLIGHT:
logger.info(f"Ignoring job for inflight email {outgoing_mail.guid}")

View File

@@ -331,10 +331,6 @@ class OtherOperationsForm(forms.Form):
class OrderPositionAddForm(forms.Form):
count = forms.IntegerField(
label=_('Number of products to add'),
initial=1,
)
itemvar = forms.ChoiceField(
label=_('Product')
)
@@ -436,10 +432,6 @@ class OrderPositionAddForm(forms.Form):
d['used_membership'] = [m for m in self.memberships if str(m.pk) == d['used_membership']][0]
else:
d['used_membership'] = None
if d.get("count", 1) and d.get("seat"):
raise ValidationError({
"seat": _("You can not choose a seat when adding multiple products at once.")
})
return d

View File

@@ -329,7 +329,6 @@
{{ add_form.custom_error }}
</div>
{% endif %}
{% bootstrap_field add_form.count layout="control" %}
{% bootstrap_field add_form.itemvar layout="control" %}
{% bootstrap_field add_form.price addon_after=request.event.currency layout="control" %}
{% if add_form.addon_to %}
@@ -365,7 +364,6 @@
</div>
<div class="panel-body">
<div class="form-horizontal">
{% bootstrap_field add_position_formset.empty_form.count layout="control" %}
{% bootstrap_field add_position_formset.empty_form.itemvar layout="control" %}
{% bootstrap_field add_position_formset.empty_form.price addon_after=request.event.currency layout="control" %}
{% if add_position_formset.empty_form.addon_to %}

View File

@@ -8,45 +8,7 @@
{% csrf_token %}
{% bootstrap_form_errors form %}
{% bootstrap_field form.name layout='horizontal' %}
<div class="form-group{% if form.devicetype.errors %} has-error{% endif %}">
<label class="col-md-3 control-label">{% trans "Device type" %}</label>
<div class="col-md-9">
<div>
<div class="big-radio radio">
<label>
<input type="radio" required value="totp" name="{{ form.devicetype.html_name }}" {% if form.devicetype.value == "totp" %}checked{% endif %}>
<strong>{% trans "Smartphone with Authenticator app" %}</strong><br>
<div class="help-block">
{% blocktrans trimmed %}
Use your smartphone with any Time-based One-Time-Password app like freeOTP, Google Authenticator or Proton Authenticator.
{% endblocktrans %}
</div>
</label>
</div>
<div class="big-radio radio">
<label>
<input type="radio" required value="webauthn" name="{{ form.devicetype.html_name }}" {% if form.devicetype.value == "webauthn" %}checked{% endif %}>
<strong>{% trans "WebAuthn-compatible hardware token" %}</strong><br>
<div class="help-block">
{% blocktrans trimmed %}
Use a hardware token like the Yubikey, or other biometric authentication like fingerprint or face recognition.
{% endblocktrans %}
</div>
</label>
</div>
</div>
{% if form.devicetype.errors %}
<div class="help-block">
{% for error in form.devicetype.errors %}
<p>{{ error|escape }}</p>
{% endfor %}
</div>
{% endif %}
</div>
</div>
{% bootstrap_field form.devicetype layout='horizontal' %}
<div class="form-group submit-group">
<button type="submit" class="btn btn-primary btn-save">
{% trans "Continue" %}

View File

@@ -28,6 +28,11 @@
{% trans "iOS (iTunes)" %}
</a>
</li>
<li>
<a href="https://m.google.com/authenticator">
{% trans "Blackberry (Link via Google)" %}
</a>
</li>
</ul>
</li>
<li>

View File

@@ -2059,13 +2059,12 @@ class OrderChange(OrderView):
else:
variation = None
try:
for i in range(f.cleaned_data.get("count", 1)):
ocm.add_position(item, variation,
f.cleaned_data['price'],
f.cleaned_data.get('addon_to'),
f.cleaned_data.get('subevent'),
f.cleaned_data.get('seat'),
f.cleaned_data.get('used_membership'))
ocm.add_position(item, variation,
f.cleaned_data['price'],
f.cleaned_data.get('addon_to'),
f.cleaned_data.get('subevent'),
f.cleaned_data.get('seat'),
f.cleaned_data.get('used_membership'))
except OrderError as e:
f.custom_error = str(e)
return False

View File

@@ -19,12 +19,26 @@
# 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/>.
#
import ipaddress
import socket
import sys
import types
from datetime import datetime
from http import cookies
from django.conf import settings
from PIL import Image
from requests.adapters import HTTPAdapter
from urllib3.connection import HTTPConnection, HTTPSConnection
from urllib3.connectionpool import HTTPConnectionPool, HTTPSConnectionPool
from urllib3.exceptions import (
ConnectTimeoutError, HTTPError, LocationParseError, NameResolutionError,
NewConnectionError,
)
from urllib3.util.connection import (
_TYPE_SOCKET_OPTIONS, _set_socket_options, allowed_gai_family,
)
from urllib3.util.timeout import _DEFAULT_TIMEOUT
def monkeypatch_vobject_performance():
@@ -89,6 +103,123 @@ def monkeypatch_requests_timeout():
HTTPAdapter.send = httpadapter_send
def monkeypatch_urllib3_ssrf_protection():
"""
pretix allows HTTP requests to untrusted URLs, e.g. through webhooks or external API URLs. This is dangerous since
it can allow access to private networks that should not be reachable by users ("server-side request forgery", SSRF).
Validating URLs at submission is not sufficient, since with DNS rebinding an attacker can make a domain name pass
validation and then resolve to a private IP address on actual execution. Unfortunately, there seems no clean solution
to this in Python land, so we monkeypatch urllib3's connection management to check the IP address to be external
*after* the DNS resolution.
This does not work when a global http(s) proxy is used, but in that scenario the proxy can perform the validation.
"""
if settings.ALLOW_HTTP_TO_PRIVATE_NETWORKS:
# Settings are not supposed to change during runtime, so we can optimize performance and complexity by skipping
# this if not needed.
return
def create_connection(
address: tuple[str, int],
timeout=_DEFAULT_TIMEOUT,
source_address: tuple[str, int] | None = None,
socket_options: _TYPE_SOCKET_OPTIONS | None = None,
) -> socket.socket:
# This is copied from urllib3.util.connection v2.3.0
host, port = address
if host.startswith("["):
host = host.strip("[]")
err = None
# Using the value from allowed_gai_family() in the context of getaddrinfo lets
# us select whether to work with IPv4 DNS records, IPv6 records, or both.
# The original create_connection function always returns all records.
family = allowed_gai_family()
try:
host.encode("idna")
except UnicodeError:
raise LocationParseError(f"'{host}', label empty or too long") from None
for res in socket.getaddrinfo(host, port, family, socket.SOCK_STREAM):
af, socktype, proto, canonname, sa = res
if not settings.ALLOW_HTTP_TO_PRIVATE_NETWORKS:
ip_addr = ipaddress.ip_address(sa[0])
if ip_addr.is_multicast:
raise HTTPError(f"Request to multicast address {sa[0]} blocked")
if ip_addr.is_loopback or ip_addr.is_link_local:
raise HTTPError(f"Request to local address {sa[0]} blocked")
if ip_addr.is_private:
raise HTTPError(f"Request to private address {sa[0]} blocked")
sock = None
try:
sock = socket.socket(af, socktype, proto)
# If provided, set socket level options before connecting.
_set_socket_options(sock, socket_options)
if timeout is not _DEFAULT_TIMEOUT:
sock.settimeout(timeout)
if source_address:
sock.bind(source_address)
sock.connect(sa)
# Break explicitly a reference cycle
err = None
return sock
except OSError as _:
err = _
if sock is not None:
sock.close()
if err is not None:
try:
raise err
finally:
# Break explicitly a reference cycle
err = None
else:
raise OSError("getaddrinfo returns an empty list")
class ProtectionMixin:
def _new_conn(self) -> socket.socket:
# This is 1:1 the version from urllib3.connection.HTTPConnection._new_conn v2.3.0
# just with a call to our own create_connection
try:
sock = create_connection(
(self._dns_host, self.port),
self.timeout,
source_address=self.source_address,
socket_options=self.socket_options,
)
except socket.gaierror as e:
raise NameResolutionError(self.host, self, e) from e
except socket.timeout as e:
raise ConnectTimeoutError(
self,
f"Connection to {self.host} timed out. (connect timeout={self.timeout})",
) from e
except OSError as e:
raise NewConnectionError(
self, f"Failed to establish a new connection: {e}"
) from e
sys.audit("http.client.connect", self, self.host, self.port)
return sock
class ProtectedHTTPConnection(ProtectionMixin, HTTPConnection):
pass
class ProtectedHTTPSConnection(ProtectionMixin, HTTPSConnection):
pass
HTTPConnectionPool.ConnectionCls = ProtectedHTTPConnection
HTTPSConnectionPool.ConnectionCls = ProtectedHTTPSConnection
def monkeypatch_cookie_morsel():
# See https://code.djangoproject.com/ticket/34613
cookies.Morsel._flags.add("partitioned")
@@ -99,4 +230,5 @@ def monkeypatch_all_at_ready():
monkeypatch_vobject_performance()
monkeypatch_pillow_safer()
monkeypatch_requests_timeout()
monkeypatch_urllib3_ssrf_protection()
monkeypatch_cookie_morsel()

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -32,7 +32,6 @@ ausgecheckt
ausgeklappt
auswahl
Authentication
Authenticator
Authenticator-App
Autorisierungscode
Autorisierungs-Endpunktes
@@ -131,7 +130,6 @@ Eingangsscan
Einlassbuchung
Einlassdatum
Einlasskontrolle
Einmalpasswörter
einzuchecken
email
E-Mail-Renderer
@@ -165,7 +163,6 @@ Explorer
FA
Favicon
F-Droid
freeOTP
Footer
Footer-Link
Footer-Text
@@ -560,7 +557,6 @@ Zahlungs-ID
Zahlungspflichtig
Zehnerkarten
Zeitbasiert
zeitbasierte
Zeitslotbuchung
Zimpler
ZIP-Datei

File diff suppressed because it is too large Load Diff

View File

@@ -32,7 +32,6 @@ ausgecheckt
ausgeklappt
auswahl
Authentication
Authenticator
Authenticator-App
Autorisierungscode
Autorisierungs-Endpunktes
@@ -131,7 +130,6 @@ Eingangsscan
Einlassbuchung
Einlassdatum
Einlasskontrolle
Einmalpasswörter
einzuchecken
email
E-Mail-Renderer
@@ -165,7 +163,6 @@ Explorer
FA
Favicon
F-Droid
freeOTP
Footer
Footer-Link
Footer-Text
@@ -560,7 +557,6 @@ Zahlungs-ID
Zahlungspflichtig
Zehnerkarten
Zeitbasiert
zeitbasierte
Zeitslotbuchung
Zimpler
ZIP-Datei

File diff suppressed because it is too large Load Diff

View File

@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-03-30 11:25+0000\n"
"POT-Creation-Date: 2026-03-17 14:06+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -8,7 +8,7 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-03-17 14:06+0000\n"
"PO-Revision-Date: 2026-03-30 03:00+0000\n"
"PO-Revision-Date: 2026-03-18 12:23+0000\n"
"Last-Translator: CVZ-es <damien.bremont@casadevelazquez.org>\n"
"Language-Team: Spanish <https://translate.pretix.eu/projects/pretix/pretix-"
"js/es/>\n"
@@ -329,7 +329,7 @@ msgstr "Pedido no aprobado"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:68
msgid "Checked-in Tickets"
msgstr "Billetes registrados"
msgstr "Registro de código QR"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:69
msgid "Valid Tickets"

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -8,8 +8,8 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-03-17 14:06+0000\n"
"PO-Revision-Date: 2026-03-25 14:14+0000\n"
"Last-Translator: Pietro Isotti <isottipietro@gmail.com>\n"
"PO-Revision-Date: 2026-02-10 16:49+0000\n"
"Last-Translator: Raffaele Doretto <ced@comune.portogruaro.ve.it>\n"
"Language-Team: Italian <https://translate.pretix.eu/projects/pretix/pretix-"
"js/it/>\n"
"Language: it\n"
@@ -17,7 +17,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 5.16.2\n"
"X-Generator: Weblate 5.15.2\n"
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:56
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:62
@@ -310,8 +310,9 @@ msgid "Ticket code revoked/changed"
msgstr "Codice biglietto annullato/modificato"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:63
#, fuzzy
msgid "Ticket blocked"
msgstr "Biglietto bloccato"
msgstr "Biglietto non pagato"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:64
msgid "Ticket not valid at this time"
@@ -428,7 +429,7 @@ msgstr ""
#: pretix/static/pretixbase/js/asynctask.js:276
msgid "If this takes longer than a few minutes, please contact us."
msgstr "Se questa operazione richiede alcuni minuti, si prega di contattarci."
msgstr ""
#: pretix/static/pretixbase/js/asynctask.js:331
msgid "Close message"

File diff suppressed because it is too large Load Diff

View File

@@ -8,7 +8,7 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-03-17 14:06+0000\n"
"PO-Revision-Date: 2026-03-23 21:00+0000\n"
"PO-Revision-Date: 2026-02-23 10:00+0000\n"
"Last-Translator: Hijiri Umemoto <hijiri@umemoto.org>\n"
"Language-Team: Japanese <https://translate.pretix.eu/projects/pretix/pretix-"
"js/ja/>\n"
@@ -17,7 +17,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=1; plural=0;\n"
"X-Generator: Weblate 5.16.2\n"
"X-Generator: Weblate 5.16\n"
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:56
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:62
@@ -60,7 +60,7 @@ msgstr "PayPal後払い"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:41
msgid "iDEAL | Wero"
msgstr "iDEAL | Wero"
msgstr ""
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:42
msgid "SEPA Direct Debit"

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -8,7 +8,7 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-03-17 14:06+0000\n"
"PO-Revision-Date: 2026-03-25 08:00+0000\n"
"PO-Revision-Date: 2026-01-26 22:00+0000\n"
"Last-Translator: Renne Rocha <renne@rocha.dev.br>\n"
"Language-Team: Portuguese (Brazil) <https://translate.pretix.eu/projects/"
"pretix/pretix-js/pt_BR/>\n"
@@ -17,7 +17,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n > 1;\n"
"X-Generator: Weblate 5.16.2\n"
"X-Generator: Weblate 5.15.2\n"
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:56
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:62
@@ -60,7 +60,7 @@ msgstr "PayPal Pay Later"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:41
msgid "iDEAL | Wero"
msgstr "iDEAL | Wero"
msgstr ""
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:42
msgid "SEPA Direct Debit"

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -8,7 +8,7 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-03-17 14:06+0000\n"
"PO-Revision-Date: 2026-03-26 14:29+0000\n"
"PO-Revision-Date: 2025-10-10 17:00+0000\n"
"Last-Translator: Linnea Thelander <linnea@coeo.events>\n"
"Language-Team: Swedish <https://translate.pretix.eu/projects/pretix/pretix-"
"js/sv/>\n"
@@ -17,7 +17,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 5.16.2\n"
"X-Generator: Weblate 5.13.3\n"
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:56
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:62
@@ -1023,6 +1023,7 @@ msgid "Waiting list"
msgstr "Väntelista"
#: pretix/static/pretixpresale/js/widget/widget.js:55
#, fuzzy
msgctxt "widget"
msgid ""
"You currently have an active cart for this event. If you select more "

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -12,7 +12,6 @@ anonymized
Auth
authentification
authenticator
Authenticator
automatical
availabilities
backend
@@ -23,7 +22,6 @@ barcodes
Bcc
BCC
BezahlCode
biometric
BLIK
blocklist
BN
@@ -58,7 +56,6 @@ EPS
eps
favicon
filetype
freeOTP
frontend
frontpage
Galician

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -28,7 +28,7 @@ from django import forms
from django.conf import settings
from django.contrib.auth.hashers import check_password
from django.contrib.auth.password_validation import (
MinimumLengthValidator, get_password_validators, validate_password,
get_password_validators, password_validators_help_texts, validate_password,
)
from django.contrib.auth.tokens import PasswordResetTokenGenerator
from django.core import signing
@@ -300,12 +300,13 @@ class SetPasswordForm(forms.Form):
)
password = forms.CharField(
label=_('Password'),
widget=forms.PasswordInput(attrs={'autocomplete': 'new-password'}),
widget=forms.PasswordInput(attrs={'minlength': '8', 'autocomplete': 'new-password'}),
max_length=4096,
required=True
)
password_repeat = forms.CharField(
label=_('Repeat password'),
widget=forms.PasswordInput(attrs={'autocomplete': 'new-password'}),
widget=forms.PasswordInput(attrs={'minlength': '8', 'autocomplete': 'new-password'}),
max_length=4096,
)
@@ -315,14 +316,6 @@ class SetPasswordForm(forms.Form):
kwargs['initial']['email'] = self.customer.email
super().__init__(*args, **kwargs)
pw_min_len_validators = [v for v in get_customer_password_validators() if isinstance(v, MinimumLengthValidator)]
if pw_min_len_validators:
self.fields['password'].widget.attrs['minlength'] = max(v.min_length for v in pw_min_len_validators)
self.fields['password_repeat'].widget.attrs['minlength'] = max(v.min_length for v in pw_min_len_validators)
if 'password' not in self.data:
self.fields['password'].help_text = ' '.join(v.get_help_text() for v in pw_min_len_validators)
def clean(self):
password1 = self.cleaned_data.get('password', '')
password2 = self.cleaned_data.get('password_repeat')
@@ -336,7 +329,8 @@ class SetPasswordForm(forms.Form):
def clean_password(self):
password1 = self.cleaned_data.get('password', '')
validate_password(password1, user=self.customer, password_validators=get_customer_password_validators())
if validate_password(password1, user=self.customer, password_validators=get_customer_password_validators()) is not None:
raise forms.ValidationError(_(password_validators_help_texts()), code='pw_invalid')
return password1
@@ -401,13 +395,13 @@ class ChangePasswordForm(forms.Form):
)
password = forms.CharField(
label=_('New password'),
widget=forms.PasswordInput(attrs={'autocomplete': 'new-password'}),
widget=forms.PasswordInput(attrs={'minlength': '8', 'autocomplete': 'new-password'}),
max_length=4096,
required=True
)
password_repeat = forms.CharField(
label=_('Repeat password'),
widget=forms.PasswordInput(attrs={'autocomplete': 'new-password'}),
widget=forms.PasswordInput(attrs={'minlength': '8', 'autocomplete': 'new-password'}),
max_length=4096,
)
@@ -417,14 +411,6 @@ class ChangePasswordForm(forms.Form):
kwargs['initial']['email'] = self.customer.email
super().__init__(*args, **kwargs)
pw_min_len_validators = [v for v in get_customer_password_validators() if isinstance(v, MinimumLengthValidator)]
if pw_min_len_validators:
self.fields['password'].widget.attrs['minlength'] = max(v.min_length for v in pw_min_len_validators)
self.fields['password_repeat'].widget.attrs['minlength'] = max(v.min_length for v in pw_min_len_validators)
if 'password' not in self.data:
self.fields['password'].help_text = ' '.join(v.get_help_text() for v in pw_min_len_validators)
def clean(self):
password1 = self.cleaned_data.get('password', '')
password2 = self.cleaned_data.get('password_repeat')
@@ -438,7 +424,8 @@ class ChangePasswordForm(forms.Form):
def clean_password(self):
password1 = self.cleaned_data.get('password', '')
validate_password(password1, user=self.customer, password_validators=get_customer_password_validators())
if validate_password(password1, user=self.customer, password_validators=get_customer_password_validators()) is not None:
raise forms.ValidationError(_(password_validators_help_texts()), code='pw_invalid')
return password1
def clean_password_current(self):

View File

@@ -208,6 +208,7 @@ CSRF_TRUSTED_ORIGINS = [urlparse(SITE_URL).scheme + '://' + urlparse(SITE_URL).h
TRUST_X_FORWARDED_FOR = config.getboolean('pretix', 'trust_x_forwarded_for', fallback=False)
USE_X_FORWARDED_HOST = config.getboolean('pretix', 'trust_x_forwarded_host', fallback=False)
ALLOW_HTTP_TO_PRIVATE_NETWORKS = config.getboolean('pretix', 'allow_http_to_private_networks', fallback=False)
REQUEST_ID_HEADER = config.get('pretix', 'request_id_header', fallback=False)

View File

@@ -0,0 +1,93 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-today pretix 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 socket import AF_INET, SOCK_STREAM
from unittest import mock
import pytest
import requests
from django.test import override_settings
from dns.inet import AF_INET6
from urllib3.exceptions import HTTPError
def test_local_blocked():
with pytest.raises(HTTPError, match="Request to local address.*"):
requests.get("http://localhost", timeout=0.1)
with pytest.raises(HTTPError, match="Request to local address.*"):
requests.get("https://localhost", timeout=0.1)
def test_private_ip_blocked():
with pytest.raises(HTTPError, match="Request to private address.*"):
requests.get("http://10.0.0.1", timeout=0.1)
with pytest.raises(HTTPError, match="Request to private address.*"):
requests.get("https://10.0.0.1", timeout=0.1)
@pytest.mark.django_db
@pytest.mark.parametrize("res", [
[(AF_INET, SOCK_STREAM, 6, '', ('10.0.0.3', 443))],
[(AF_INET, SOCK_STREAM, 6, '', ('0.0.0.0', 443))],
[(AF_INET, SOCK_STREAM, 6, '', ('127.1.1.1', 443))],
[(AF_INET, SOCK_STREAM, 6, '', ('192.168.5.3', 443))],
[(AF_INET, SOCK_STREAM, 6, '', ('224.0.0.1', 443))],
[(AF_INET6, SOCK_STREAM, 6, '', ('::1', 443, 0, 0))],
[(AF_INET6, SOCK_STREAM, 6, '', ('fe80::1', 443, 0, 0))],
[(AF_INET6, SOCK_STREAM, 6, '', ('ff00::1', 443, 0, 0))],
[(AF_INET6, SOCK_STREAM, 6, '', ('fc00::1', 443, 0, 0))],
])
def test_dns_resolving_to_local_blocked(res):
with mock.patch('socket.getaddrinfo') as mock_addr:
mock_addr.return_value = res
with pytest.raises(HTTPError, match="Request to (multicast|private|local) address.*"):
requests.get("https://example.org", timeout=0.1)
with pytest.raises(HTTPError, match="Request to (multicast|private|local) address.*"):
requests.get("http://example.org", timeout=0.1)
def test_dns_remote_allowed():
class SocketOk(Exception):
pass
def side_effect(*args, **kwargs):
raise SocketOk
with mock.patch('socket.getaddrinfo') as mock_addr, mock.patch('socket.socket') as mock_socket:
mock_addr.return_value = [(AF_INET, SOCK_STREAM, 6, '', ('8.8.8.8', 443))]
mock_socket.side_effect = side_effect
with pytest.raises(SocketOk):
requests.get("https://example.org", timeout=0.1)
@override_settings(ALLOW_HTTP_TO_PRIVATE_NETWORKS=True)
def test_local_is_allowed():
class SocketOk(Exception):
pass
def side_effect(*args, **kwargs):
raise SocketOk
with mock.patch('socket.getaddrinfo') as mock_addr, mock.patch('socket.socket') as mock_socket:
mock_addr.return_value = [(AF_INET, SOCK_STREAM, 6, '', ('10.0.0.1', 443))]
mock_socket.side_effect = side_effect
with pytest.raises(SocketOk):
requests.get("https://example.org", timeout=0.1)