forked from CGM_Public/pretix_original
Compare commits
3 Commits
upstream/2
...
ssrf-prote
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f66f97bee1 | ||
|
|
998a62add2 | ||
|
|
614408f5ae |
@@ -1,31 +0,0 @@
|
||||
name: Build Deploy email notification tool
|
||||
run-name: ${{ gitea.actor }} building new version of the email notification tool
|
||||
on:
|
||||
push: # Baut bei jedem Push (Branches + Tags)
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
Apply-Kubernetes-Resources:
|
||||
runs-on: podman
|
||||
steps:
|
||||
- name: Check out repository code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Login to Docker Registry
|
||||
run: podman login -u ${{ secrets.REGISTRY_USERNAME }} -p ${{ secrets.REGISTRY_TOKEN }} cr.ortlerstrasse.de
|
||||
|
||||
- name: Set Docker Image Tag
|
||||
run: |
|
||||
if [[ "${{ gitea.ref }}" == refs/tags/* ]]; then
|
||||
echo "TAG_NAME=${{ gitea.ref_name }}" >> $GITHUB_ENV
|
||||
else
|
||||
echo "TAG_NAME=latest" >> $GITHUB_ENV
|
||||
fi
|
||||
|
||||
- name: Build Docker image
|
||||
run: podman build -t cr.ortlerstrasse.de/cgo/pretix:${{ env.TAG_NAME }} .
|
||||
|
||||
- name: Push Docker image
|
||||
run: |
|
||||
podman push cr.ortlerstrasse.de/cgo/pretix:${{ env.TAG_NAME }}
|
||||
echo "Image pushed successfully: cr.ortlerstrasse.de/cgo/pretix:${{ env.TAG_NAME }}"
|
||||
@@ -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.3.1"
|
||||
__version__ = "2026.3.0.dev0"
|
||||
|
||||
@@ -1122,7 +1122,7 @@ class CheckinViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
permission = 'event.orders:read'
|
||||
|
||||
def get_queryset(self):
|
||||
qs = Checkin.all.filter(list__event=self.request.event).select_related(
|
||||
qs = Checkin.all.filter().select_related(
|
||||
"position",
|
||||
"device",
|
||||
)
|
||||
|
||||
@@ -19,7 +19,10 @@
|
||||
# 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 logging
|
||||
import smtplib
|
||||
import socket
|
||||
from itertools import groupby
|
||||
from smtplib import SMTPResponseException
|
||||
from typing import TypeVar
|
||||
@@ -237,3 +240,80 @@ def base_renderers(sender, **kwargs):
|
||||
|
||||
def get_email_context(**kwargs):
|
||||
return PlaceholderContext(**kwargs).render_all()
|
||||
|
||||
|
||||
def create_connection(address, timeout=socket.getdefaulttimeout(),
|
||||
source_address=None, *, all_errors=False):
|
||||
# Taken from the python stdlib, extended with a check for local ips
|
||||
|
||||
host, port = address
|
||||
exceptions = []
|
||||
for res in socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM):
|
||||
af, socktype, proto, canonname, sa = res
|
||||
|
||||
if not settings.get("MAIL_CUSTOM_SMTP_ALLOW_PRIVATE_NETWORKS", False):
|
||||
ip_addr = ipaddress.ip_address(sa[0])
|
||||
if ip_addr.is_multicast:
|
||||
raise socket.error(f"Request to multicast address {sa[0]} blocked")
|
||||
if ip_addr.is_loopback or ip_addr.is_link_local:
|
||||
raise socket.error(f"Request to local address {sa[0]} blocked")
|
||||
if ip_addr.is_private:
|
||||
raise socket.error(f"Request to private address {sa[0]} blocked")
|
||||
|
||||
sock = None
|
||||
try:
|
||||
sock = socket.socket(af, socktype, proto)
|
||||
if timeout is not socket.getdefaulttimeout():
|
||||
sock.settimeout(timeout)
|
||||
if source_address:
|
||||
sock.bind(source_address)
|
||||
sock.connect(sa)
|
||||
# Break explicitly a reference cycle
|
||||
exceptions.clear()
|
||||
return sock
|
||||
|
||||
except socket.error as exc:
|
||||
if not all_errors:
|
||||
exceptions.clear() # raise only the last error
|
||||
exceptions.append(exc)
|
||||
if sock is not None:
|
||||
sock.close()
|
||||
|
||||
if len(exceptions):
|
||||
try:
|
||||
if not all_errors:
|
||||
raise exceptions[0]
|
||||
raise ExceptionGroup("create_connection failed", exceptions)
|
||||
finally:
|
||||
# Break explicitly a reference cycle
|
||||
exceptions.clear()
|
||||
else:
|
||||
raise socket.error("getaddrinfo returns an empty list")
|
||||
|
||||
|
||||
class CheckPrivateNetworkMixin:
|
||||
# _get_socket taken 1:1 from smtplib, just with a call to our own create_connection
|
||||
def _get_socket(self, host, port, timeout):
|
||||
# This makes it simpler for SMTP_SSL to use the SMTP connect code
|
||||
# and just alter the socket connection bit.
|
||||
if timeout is not None and not timeout:
|
||||
raise ValueError('Non-blocking socket (timeout=0) is not supported')
|
||||
if self.debuglevel > 0:
|
||||
self._print_debug('connect: to', (host, port), self.source_address)
|
||||
return create_connection((host, port), timeout, self.source_address)
|
||||
|
||||
|
||||
class SMTP(CheckPrivateNetworkMixin, smtplib.SMTP):
|
||||
pass
|
||||
|
||||
|
||||
# SMTP used here instead of mixin, because smtp.SMTP_SSL._get_socket calls super()._get_socket and then wraps this socket
|
||||
# super()._get_socket needs to be our version from the mixin
|
||||
class SMTP_SSL(smtplib.SMTP_SSL, SMTP): # noqa: N801
|
||||
pass
|
||||
|
||||
|
||||
class CheckPrivateNetworkSmtpBackend(EmailBackend):
|
||||
@property
|
||||
def connection_class(self):
|
||||
return SMTP_SSL if self.use_ssl else SMTP
|
||||
|
||||
@@ -28,6 +28,5 @@ from .items import * # noqa
|
||||
from .json import * # noqa
|
||||
from .mail import * # noqa
|
||||
from .orderlist import * # noqa
|
||||
from .relevant_orderlist import * # noqa
|
||||
from .reusablemedia import * # noqa
|
||||
from .waitinglist import * # noqa
|
||||
|
||||
@@ -89,7 +89,7 @@ class OrderListExporter(MultiSheetListExporter):
|
||||
description = gettext_lazy('Download a spreadsheet of all orders. The spreadsheet will include three sheets, one '
|
||||
'with a line for every order, one with a line for every order position, and one with '
|
||||
'a line for every additional fee charged in an order.')
|
||||
featured = False
|
||||
featured = True
|
||||
repeatable_read = False
|
||||
|
||||
@cached_property
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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):
|
||||
|
||||
@@ -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}")
|
||||
|
||||
@@ -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" %}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 getattr(settings, "ALLOW_HTTP_TO_PRIVATE_NETWORKS", False):
|
||||
# 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 getattr(settings, "ALLOW_HTTP_TO_PRIVATE_NETWORKS", False):
|
||||
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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
@@ -248,7 +249,8 @@ EMAIL_HOST_PASSWORD = config.get('mail', 'password', fallback='')
|
||||
EMAIL_USE_TLS = config.getboolean('mail', 'tls', fallback=False)
|
||||
EMAIL_USE_SSL = config.getboolean('mail', 'ssl', fallback=False)
|
||||
EMAIL_SUBJECT_PREFIX = '[pretix] '
|
||||
EMAIL_BACKEND = EMAIL_CUSTOM_SMTP_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
|
||||
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
|
||||
EMAIL_CUSTOM_SMTP_BACKEND = 'pretixbase.email.CheckPrivateNetworkSmtpBackend'
|
||||
EMAIL_TIMEOUT = 60
|
||||
|
||||
ADMINS = [('Admin', n) for n in config.get('mail', 'admins', fallback='').split(",") if n]
|
||||
|
||||
@@ -35,8 +35,11 @@
|
||||
import datetime
|
||||
import os
|
||||
import re
|
||||
import socket
|
||||
from contextlib import contextmanager
|
||||
from decimal import Decimal
|
||||
from email.mime.text import MIMEText
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
from django.conf import settings
|
||||
@@ -591,3 +594,117 @@ def test_attached_ical_localization(env, order):
|
||||
assert len(djmail.outbox) == 1
|
||||
assert len(djmail.outbox[0].attachments) == 1
|
||||
assert description in djmail.outbox[0].attachments[0][1]
|
||||
|
||||
|
||||
PRIVATE_IPS_RES = [
|
||||
[(socket.AF_INET, socket.SOCK_STREAM, 6, '', ('10.0.0.3', 443))],
|
||||
[(socket.AF_INET, socket.SOCK_STREAM, 6, '', ('0.0.0.0', 443))],
|
||||
[(socket.AF_INET, socket.SOCK_STREAM, 6, '', ('127.1.1.1', 443))],
|
||||
[(socket.AF_INET, socket.SOCK_STREAM, 6, '', ('192.168.5.3', 443))],
|
||||
[(socket.AF_INET, socket.SOCK_STREAM, 6, '', ('224.0.0.1', 443))],
|
||||
[(socket.AF_INET6, socket.SOCK_STREAM, 6, '', ('::1', 443, 0, 0))],
|
||||
[(socket.AF_INET6, socket.SOCK_STREAM, 6, '', ('fe80::1', 443, 0, 0))],
|
||||
[(socket.AF_INET6, socket.SOCK_STREAM, 6, '', ('ff00::1', 443, 0, 0))],
|
||||
[(socket.AF_INET6, socket.SOCK_STREAM, 6, '', ('fc00::1', 443, 0, 0))],
|
||||
]
|
||||
|
||||
|
||||
@contextmanager
|
||||
def test_mail_connection(res, should_connect, use_ssl):
|
||||
with (
|
||||
mock.patch('socket.socket') as mock_socket,
|
||||
mock.patch('socket.getaddrinfo', return_value=res),
|
||||
mock.patch('smtplib.SMTP.getreply', return_value=(220, "")),
|
||||
mock.patch('smtplib.SMTP.sendmail'),
|
||||
mock.patch('ssl.SSLContext.wrap_socket') as mock_ssl
|
||||
):
|
||||
yield
|
||||
|
||||
if should_connect:
|
||||
mock_socket.assert_called_once()
|
||||
mock_socket.return_value.connect.assert_called_once_with(res[0][-1])
|
||||
if use_ssl:
|
||||
mock_ssl.assert_called_once()
|
||||
else:
|
||||
mock_socket.assert_not_called()
|
||||
mock_socket.return_value.connect.assert_not_called()
|
||||
mock_ssl.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("res", PRIVATE_IPS_RES)
|
||||
@pytest.mark.parametrize("use_ssl", [
|
||||
True, False
|
||||
])
|
||||
def test_private_smtp_ip(res, use_ssl, settings):
|
||||
settings.EMAIL_CUSTOM_SMTP_BACKEND = 'pretix.base.email.CheckPrivateNetworkSmtpBackend'
|
||||
settings.MAIL_CUSTOM_SMTP_ALLOW_PRIVATE_NETWORKS = False
|
||||
with test_mail_connection(res=res, should_connect=False, use_ssl=use_ssl), pytest.raises(match="Request to .* blocked"):
|
||||
connection = djmail.get_connection(backend=settings.EMAIL_CUSTOM_SMTP_BACKEND,
|
||||
host="localhost",
|
||||
use_ssl=use_ssl)
|
||||
connection.open()
|
||||
|
||||
settings.MAIL_CUSTOM_SMTP_ALLOW_PRIVATE_NETWORKS = True
|
||||
with test_mail_connection(res=res, should_connect=True, use_ssl=use_ssl):
|
||||
connection = djmail.get_connection(backend=settings.EMAIL_CUSTOM_SMTP_BACKEND,
|
||||
host="localhost",
|
||||
use_ssl=use_ssl)
|
||||
connection.open()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("use_ssl", [
|
||||
True, False
|
||||
])
|
||||
@pytest.mark.parametrize("allow_private", [
|
||||
True, False
|
||||
])
|
||||
def test_public_smtp_ip(use_ssl, allow_private, settings):
|
||||
settings.EMAIL_CUSTOM_SMTP_BACKEND = 'pretix.base.email.CheckPrivateNetworkSmtpBackend'
|
||||
settings.MAIL_CUSTOM_SMTP_ALLOW_PRIVATE_NETWORKS = allow_private
|
||||
|
||||
with test_mail_connection(res=[(socket.AF_INET, socket.SOCK_STREAM, 6, '', ('8.8.8.8', 443))], should_connect=True, use_ssl=use_ssl):
|
||||
connection = djmail.get_connection(backend=settings.EMAIL_CUSTOM_SMTP_BACKEND,
|
||||
host="localhost",
|
||||
use_ssl=use_ssl)
|
||||
connection.open()
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.parametrize("use_ssl", [
|
||||
True, False
|
||||
])
|
||||
@pytest.mark.parametrize("allow_private_networks", [
|
||||
True, False
|
||||
])
|
||||
@pytest.mark.parametrize("res", PRIVATE_IPS_RES)
|
||||
def test_send_mail_private_ip(res, use_ssl, allow_private_networks, env):
|
||||
settings.EMAIL_CUSTOM_SMTP_BACKEND = 'pretix.base.email.CheckPrivateNetworkSmtpBackend'
|
||||
settings.MAIL_CUSTOM_SMTP_ALLOW_PRIVATE_NETWORKS = allow_private_networks
|
||||
|
||||
event, user, organizer = env
|
||||
event.settings.smtp_use_custom = True
|
||||
event.settings.smtp_host = "example.com"
|
||||
event.settings.smtp_use_ssl = use_ssl
|
||||
event.settings.smtp_use_tls = False
|
||||
|
||||
def send_mail():
|
||||
m = OutgoingMail.objects.create(
|
||||
to=['recipient@example.com'],
|
||||
subject='Test',
|
||||
body_plain='Test',
|
||||
sender='sender@example.com',
|
||||
event=event
|
||||
)
|
||||
assert m.status == OutgoingMail.STATUS_QUEUED
|
||||
mail_send_task.apply(kwargs={
|
||||
'outgoing_mail': m.pk,
|
||||
}, max_retries=0)
|
||||
m.refresh_from_db()
|
||||
return m
|
||||
|
||||
with test_mail_connection(res=res, should_connect=allow_private_networks, use_ssl=use_ssl):
|
||||
m = send_mail()
|
||||
if allow_private_networks:
|
||||
assert m.status == OutgoingMail.STATUS_SENT
|
||||
else:
|
||||
assert m.status == OutgoingMail.STATUS_FAILED
|
||||
|
||||
93
src/tests/helpers/test_urllib.py
Normal file
93
src/tests/helpers/test_urllib.py
Normal 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)
|
||||
Reference in New Issue
Block a user