mirror of
https://github.com/pretix/pretix.git
synced 2026-06-10 01:15:05 +00:00
Compare commits
2 Commits
bank-perms
...
ssrf-cgnat
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
96b705d6bb | ||
|
|
62f35f0c10 |
@@ -57,6 +57,8 @@ logger = logging.getLogger('pretix.base.email')
|
||||
|
||||
T = TypeVar("T", bound=EmailBackend)
|
||||
|
||||
_cgnat_net = ipaddress.ip_network('100.64.0.0/10')
|
||||
|
||||
|
||||
def test_custom_smtp_backend(backend: T, from_addr: str) -> None:
|
||||
try:
|
||||
@@ -253,12 +255,15 @@ def create_connection(address, timeout=socket.getdefaulttimeout(),
|
||||
|
||||
if not getattr(settings, "MAIL_CUSTOM_SMTP_ALLOW_PRIVATE_NETWORKS", False):
|
||||
ip_addr = ipaddress.ip_address(sa[0])
|
||||
check_ip4 = ip_addr.ipv4_mapped if getattr(ip_addr, "ipv4_mapped", None) else ip_addr
|
||||
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")
|
||||
if check_ip4 in _cgnat_net:
|
||||
raise socket.error(f"Request to RFC 6598 address {sa[0]} blocked")
|
||||
|
||||
sock = None
|
||||
try:
|
||||
|
||||
@@ -148,13 +148,14 @@ def monkeypatch_urllib3_ssrf_protection():
|
||||
|
||||
if not getattr(settings, "ALLOW_HTTP_TO_PRIVATE_NETWORKS", False):
|
||||
ip_addr = ipaddress.ip_address(sa[0])
|
||||
check_ip4 = ip_addr.ipv4_mapped if getattr(ip_addr, "ipv4_mapped", None) else ip_addr
|
||||
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")
|
||||
if ip_addr in _cgnat_net:
|
||||
if check_ip4 in _cgnat_net:
|
||||
raise HTTPError(f"Request to RFC 6598 address {sa[0]} blocked")
|
||||
|
||||
sock = None
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
# 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 django.db.models import Exists, OuterRef, Q
|
||||
from django.dispatch import receiver
|
||||
from django.template.loader import get_template
|
||||
from django.urls import resolve, reverse
|
||||
@@ -77,18 +76,10 @@ def control_nav_import(sender, request=None, **kwargs):
|
||||
@receiver(nav_organizer, dispatch_uid="payment_banktransfer_organav")
|
||||
def control_nav_orga_import(sender, request=None, **kwargs):
|
||||
url = resolve(request.path_info)
|
||||
|
||||
events_without_permission = request.organizer.events.filter(
|
||||
~Exists(
|
||||
request.user.teams.with_event_permission(
|
||||
"event.orders:read"
|
||||
).filter(
|
||||
Q(all_events=True) | Q(limit_events=OuterRef("pk")),
|
||||
organizer_id=OuterRef("organizer_id"),
|
||||
)
|
||||
)
|
||||
).exists()
|
||||
if events_without_permission:
|
||||
has_any_event_perm = request.user.get_events_with_permission(
|
||||
"event.orders:write", request=request
|
||||
).filter(organizer=request.organizer).exists()
|
||||
if not has_any_event_perm:
|
||||
return []
|
||||
return [
|
||||
{
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
Therefore, you won't be able to mark any order as paid here.
|
||||
{% endblocktrans %}
|
||||
</div>
|
||||
{% elif can_write %}
|
||||
{% else %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">{% trans "Upload a new file" %}</h3>
|
||||
@@ -93,7 +93,7 @@
|
||||
</div>
|
||||
</form>
|
||||
<div class="col-md-2">
|
||||
{% if not filter_form.is_valid and can_write %}
|
||||
{% if not filter_form.is_valid %}
|
||||
<form action="" method="post" class="helper-display-inline pull-right flip">
|
||||
{% csrf_token %}
|
||||
<button class="btn btn-danger" type="submit" name="discard" value="all">
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if num_new > 0 and can_write %}
|
||||
{% if num_new > 0 %}
|
||||
<form action="" method="post">
|
||||
{% csrf_token %}
|
||||
<button class="btn btn-primary">
|
||||
|
||||
@@ -20,24 +20,24 @@
|
||||
{% for trans in list %}
|
||||
<tr data-id="{{ trans.id }}">
|
||||
<td class="actions">
|
||||
{% if trans.order and trans.state == 'invalid' and can_write %}
|
||||
{% if trans.order and trans.state == 'invalid' %}
|
||||
<button type="button" class="btn btn-default" name="action_{{ trans.id }}" value="accept"
|
||||
data-toggle="tooltip" title="{% trans "Accept anyway" %}" data-placement="right">
|
||||
<span class="fa fa-check"></span>
|
||||
</button>
|
||||
{% elif trans.state == 'nomatch' and can_write %}
|
||||
{% elif trans.state == 'nomatch' %}
|
||||
<input type="text" class="form-control" placeholder="{% trans "Order code" %}">
|
||||
<button class="btn btn-default" type="button" name="action_{{ trans.id }}"
|
||||
value="assign" data-toggle="tooltip" title="{% trans "Assign to order" %}"
|
||||
data-placement="right">
|
||||
<span class="fa fa-check"></span>
|
||||
</button>
|
||||
{% elif trans.state == 'error' and can_write %}
|
||||
{% elif trans.state == 'error' %}
|
||||
<button type="button" class="btn btn-default" name="action_{{ trans.id }}" value="retry"
|
||||
data-toggle="tooltip" title="{% trans "Retry" %}" data-placement="right">
|
||||
<span class="fa fa-refresh"></span>
|
||||
</button>
|
||||
{% elif trans.state == 'already' and can_write %}
|
||||
{% elif trans.state == 'already' %}
|
||||
<input type="text" class="form-control" placeholder="{% trans "Order code" %}">
|
||||
<div class="btn-group" role="group">
|
||||
<button class="btn btn-default" type="button" name="action_{{ trans.id }}"
|
||||
@@ -76,15 +76,13 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
{{ trans.reference }}
|
||||
{% if can_write %}
|
||||
<div class="comment-box" data-plain="{{ trans.comment }}">
|
||||
<strong>{% trans "Comment:" %}</strong>
|
||||
<span class="comment">{{ trans.comment|rich_text }}</span>
|
||||
<a href="#" class="comment-modify btn btn-default btn-xs">
|
||||
<span class="fa fa-edit"></span>
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="comment-box" data-plain="{{ trans.comment }}">
|
||||
<strong>{% trans "Comment:" %}</strong>
|
||||
<span class="comment">{{ trans.comment|rich_text }}</span>
|
||||
<a href="#" class="comment-modify btn btn-default btn-xs">
|
||||
<span class="fa fa-edit"></span>
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
{% if trans.currency %}
|
||||
@@ -121,12 +119,10 @@
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="discard">
|
||||
{% if can_write %}
|
||||
<button type="button" class="btn btn-default" name="action_{{ trans.id }}" value="discard"
|
||||
data-toggle="tooltip" title="{% trans "Discard" %}">
|
||||
<span class="fa fa-trash"></span>
|
||||
</button>
|
||||
{% endif %}
|
||||
<button type="button" class="btn btn-default" name="action_{{ trans.id }}" value="discard"
|
||||
data-toggle="tooltip" title="{% trans "Discard" %}">
|
||||
<span class="fa fa-trash"></span>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
||||
@@ -46,7 +46,7 @@ from django import forms
|
||||
from django.contrib import messages
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.db import transaction
|
||||
from django.db.models import Count, Exists, OuterRef, Q, QuerySet
|
||||
from django.db.models import Count, Q, QuerySet
|
||||
from django.http import FileResponse, JsonResponse
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.urls import reverse
|
||||
@@ -586,7 +586,6 @@ class ImportView(ListView):
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data()
|
||||
ctx['job_running'] = self.job_running
|
||||
ctx['can_write'] = self.can_write
|
||||
ctx['no_more_payments'] = False
|
||||
ctx['filter_form'] = BankTransactionFilterForm(self.request.GET or None)
|
||||
|
||||
@@ -624,94 +623,45 @@ class ImportView(ListView):
|
||||
return ctx
|
||||
|
||||
|
||||
class EventPermissionOnAllEventsRequiredMixin:
|
||||
@cached_property
|
||||
def can_write(self):
|
||||
perm_name = self.event_permission
|
||||
if hasattr(self, 'write_event_permission'):
|
||||
perm_name = self.write_event_permission
|
||||
|
||||
events_without_permission = self.request.organizer.events.filter(
|
||||
~Exists(
|
||||
self.request.user.teams.with_event_permission(
|
||||
perm_name
|
||||
).filter(
|
||||
Q(all_events=True) | Q(limit_events=OuterRef("pk")),
|
||||
organizer_id=OuterRef("organizer_id"),
|
||||
)
|
||||
)
|
||||
).exists()
|
||||
return not events_without_permission
|
||||
|
||||
class OrganizerBanktransferView:
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
perm_name = self.event_permission
|
||||
if request.method not in ("GET", "HEAD") and hasattr(self, 'write_event_permission'):
|
||||
perm_name = self.write_event_permission
|
||||
|
||||
events_without_permission = self.request.organizer.events.filter(
|
||||
~Exists(
|
||||
self.request.user.teams.with_event_permission(
|
||||
perm_name
|
||||
).filter(
|
||||
Q(all_events=True) | Q(limit_events=OuterRef("pk")),
|
||||
organizer_id=OuterRef("organizer_id"),
|
||||
)
|
||||
)
|
||||
).exists()
|
||||
if events_without_permission:
|
||||
has_any_event_perm = request.user.get_events_with_permission(
|
||||
"event.orders:write", request=request
|
||||
).filter(organizer=request.organizer).exists()
|
||||
if not has_any_event_perm:
|
||||
raise PermissionDenied()
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
|
||||
class PostEventPermissionRequiredMixin(EventPermissionRequiredMixin):
|
||||
@cached_property
|
||||
def can_write(self):
|
||||
return self.request.user.has_event_permission(
|
||||
self.request.organizer, self.request.event, self.write_permission, request=self.request
|
||||
)
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
if request.method not in ("GET", "HEAD"):
|
||||
if not self.can_write:
|
||||
raise PermissionDenied()
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
|
||||
class EventImportView(PostEventPermissionRequiredMixin, ImportView):
|
||||
class EventImportView(EventPermissionRequiredMixin, ImportView):
|
||||
permission = 'event.orders:write'
|
||||
write_permission = 'event.orders:write'
|
||||
|
||||
|
||||
class OrganizerImportView(EventPermissionOnAllEventsRequiredMixin, OrganizerDetailViewMixin,
|
||||
class OrganizerImportView(OrganizerBanktransferView, OrganizerDetailViewMixin,
|
||||
ImportView):
|
||||
event_permission = 'event.orders:read'
|
||||
write_event_permission = 'event.orders:write'
|
||||
pass
|
||||
|
||||
|
||||
class EventJobDetailView(EventPermissionRequiredMixin, JobDetailView):
|
||||
permission = 'event.orders:read'
|
||||
permission = 'event.orders:write'
|
||||
|
||||
|
||||
class OrganizerJobDetailView(EventPermissionOnAllEventsRequiredMixin, OrganizerDetailViewMixin,
|
||||
class OrganizerJobDetailView(OrganizerBanktransferView, OrganizerDetailViewMixin,
|
||||
JobDetailView):
|
||||
event_permission = 'event.orders:read'
|
||||
pass
|
||||
|
||||
|
||||
class EventActionView(PostEventPermissionRequiredMixin, ActionView):
|
||||
permission = 'event.orders:read'
|
||||
write_permission = 'event.orders:write'
|
||||
class EventActionView(EventPermissionRequiredMixin, ActionView):
|
||||
permission = 'event.orders:write'
|
||||
|
||||
|
||||
class OrganizerActionView(EventPermissionOnAllEventsRequiredMixin, OrganizerDetailViewMixin,
|
||||
class OrganizerActionView(OrganizerBanktransferView, OrganizerDetailViewMixin,
|
||||
ActionView):
|
||||
event_permission = "event.orders:read"
|
||||
write_event_permission = "event.orders:write"
|
||||
|
||||
def order_qs(self):
|
||||
# The filters here are basically pointless with EventPermissionOnAllEventsRequiredMixin
|
||||
# but let's keep them for safety with future refactorings
|
||||
all = self.request.user.teams.filter(
|
||||
TeamQuerySet.event_permission_q("event.orders:read"),
|
||||
TeamQuerySet.event_permission_q("event.orders:write"),
|
||||
all_events=True,
|
||||
organizer=self.request.organizer,
|
||||
).exists()
|
||||
@@ -721,6 +671,7 @@ class OrganizerActionView(EventPermissionOnAllEventsRequiredMixin, OrganizerDeta
|
||||
return Order.objects.filter(
|
||||
event_id__in=self.request.user.teams.filter(
|
||||
TeamQuerySet.event_permission_q("event.orders:read"),
|
||||
TeamQuerySet.event_permission_q("event.orders:write"),
|
||||
organizer=self.request.organizer,
|
||||
).values_list('limit_events__id', flat=True)
|
||||
)
|
||||
@@ -761,7 +712,6 @@ class RefundExportListView(ListView):
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data()
|
||||
ctx['num_new'] = self.get_unexported().count()
|
||||
ctx['can_write'] = self.can_write
|
||||
ctx['basetpl'] = "pretixcontrol/event/base.html"
|
||||
if not hasattr(self.request, 'event'):
|
||||
ctx['basetpl'] = "pretixcontrol/organizers/base.html"
|
||||
@@ -814,9 +764,8 @@ class RefundExportListView(ListView):
|
||||
return redirect(self.get_success_url())
|
||||
|
||||
|
||||
class EventRefundExportListView(PostEventPermissionRequiredMixin, RefundExportListView):
|
||||
permission = 'event.orders:read'
|
||||
write_permission = 'event.orders:write'
|
||||
class EventRefundExportListView(EventPermissionRequiredMixin, RefundExportListView):
|
||||
permission = 'event.orders:write'
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse('plugins:banktransfer:refunds.list', kwargs={
|
||||
@@ -838,9 +787,7 @@ class EventRefundExportListView(PostEventPermissionRequiredMixin, RefundExportLi
|
||||
)
|
||||
|
||||
|
||||
class OrganizerRefundExportListView(EventPermissionOnAllEventsRequiredMixin, RefundExportListView):
|
||||
event_permission = 'event.orders:read'
|
||||
write_event_permission = 'event.orders:write'
|
||||
class OrganizerRefundExportListView(OrganizerBanktransferView, RefundExportListView):
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse('plugins:banktransfer:refunds.list', kwargs={
|
||||
@@ -873,7 +820,7 @@ class DownloadRefundExportView(DetailView):
|
||||
|
||||
|
||||
class EventDownloadRefundExportView(EventPermissionRequiredMixin, DownloadRefundExportView):
|
||||
permission = 'event.orders:read'
|
||||
permission = 'event.orders:write'
|
||||
|
||||
def get_object(self, *args, **kwargs):
|
||||
return get_object_or_404(
|
||||
@@ -883,8 +830,7 @@ class EventDownloadRefundExportView(EventPermissionRequiredMixin, DownloadRefund
|
||||
)
|
||||
|
||||
|
||||
class OrganizerDownloadRefundExportView(EventPermissionOnAllEventsRequiredMixin, OrganizerDetailViewMixin, DownloadRefundExportView):
|
||||
event_permission = 'event.orders:read'
|
||||
class OrganizerDownloadRefundExportView(OrganizerBanktransferView, OrganizerDetailViewMixin, DownloadRefundExportView):
|
||||
|
||||
def get_object(self, *args, **kwargs):
|
||||
return get_object_or_404(
|
||||
@@ -931,7 +877,7 @@ class SepaXMLExportView(SingleObjectMixin, FormView):
|
||||
|
||||
|
||||
class EventSepaXMLExportView(EventPermissionRequiredMixin, SepaXMLExportView):
|
||||
permission = 'event.orders:read'
|
||||
permission = 'event.orders:write'
|
||||
|
||||
def get_object(self, *args, **kwargs):
|
||||
return get_object_or_404(
|
||||
@@ -946,8 +892,7 @@ class EventSepaXMLExportView(EventPermissionRequiredMixin, SepaXMLExportView):
|
||||
return form
|
||||
|
||||
|
||||
class OrganizerSepaXMLExportView(EventPermissionOnAllEventsRequiredMixin, OrganizerDetailViewMixin, SepaXMLExportView):
|
||||
permission = 'event.orders:read'
|
||||
class OrganizerSepaXMLExportView(OrganizerBanktransferView, OrganizerDetailViewMixin, SepaXMLExportView):
|
||||
|
||||
def get_object(self, *args, **kwargs):
|
||||
return get_object_or_404(
|
||||
|
||||
@@ -602,10 +602,13 @@ PRIVATE_IPS_RES = [
|
||||
[(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_INET, socket.SOCK_STREAM, 6, '', ('100.64.0.1', 443))],
|
||||
[(socket.AF_INET, socket.SOCK_STREAM, 6, '', ('100.100.100.100', 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))],
|
||||
[(socket.AF_INET6, socket.SOCK_STREAM, 6, '', ('::ffff:100.64.0.1', 443, 0, 0))],
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -43,6 +43,8 @@ def test_private_ip_blocked():
|
||||
requests.get("https://10.0.0.1", timeout=0.1)
|
||||
with pytest.raises(HTTPError, match="Request to RFC 6598 address.*"):
|
||||
requests.get("https://100.100.100.100", timeout=0.1)
|
||||
with pytest.raises(HTTPError, match="Request to RFC 6598 address.*"):
|
||||
requests.get("https://[::ffff:100.64.0.1]", timeout=0.1)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@@ -58,6 +60,7 @@ def test_private_ip_blocked():
|
||||
[(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))],
|
||||
[(AF_INET6, SOCK_STREAM, 6, "", ("::ffff:100.64.0.1", 443, 0, 0))],
|
||||
])
|
||||
def test_dns_resolving_to_local_blocked(res):
|
||||
with mock.patch('socket.getaddrinfo') as mock_addr:
|
||||
|
||||
Reference in New Issue
Block a user