Compare commits

..

2 Commits

9 changed files with 59 additions and 115 deletions

View File

@@ -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:

View File

@@ -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

View File

@@ -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 [
{

View File

@@ -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">

View File

@@ -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">

View File

@@ -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 %}

View File

@@ -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(

View File

@@ -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))],
]

View File

@@ -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: