mirror of
https://github.com/django/django.git
synced 2024-12-22 17:16:24 +00:00
Fixed #35328 - Improved debug messaging behind proxies.
This commit is contained in:
parent
d36ad43f61
commit
1ad20e8bc0
1
AUTHORS
1
AUTHORS
@ -902,6 +902,7 @@ answer newbie questions, and generally made Django that much better:
|
||||
Russ Webber
|
||||
Ryan Hall <ryanhall989@gmail.com>
|
||||
Ryan Heard <ryanwheard@gmail.com>
|
||||
Ryan Hiebert <ryan@ryanhiebert.com>
|
||||
ryankanno
|
||||
Ryan Kelly <ryan@rfk.id.au>
|
||||
Ryan Niemeyer <https://profiles.google.com/ryan.niemeyer/about>
|
||||
|
@ -26,7 +26,10 @@ logger = logging.getLogger("django.security.csrf")
|
||||
# This matches if any character is not in CSRF_ALLOWED_CHARS.
|
||||
invalid_token_chars_re = _lazy_re_compile("[^a-zA-Z0-9]")
|
||||
|
||||
REASON_BAD_ORIGIN = "Origin checking failed - %s does not match any trusted origins."
|
||||
REASON_DISALLOWED_HOST = "Host is not allowed."
|
||||
REASON_BAD_ORIGIN = (
|
||||
"Origin checking failed - %r does not match %r or any other trusted origins."
|
||||
)
|
||||
REASON_NO_REFERER = "Referer checking failed - no Referer."
|
||||
REASON_BAD_REFERER = "Referer checking failed - %s does not match any trusted origins."
|
||||
REASON_NO_CSRF_COOKIE = "CSRF cookie not set."
|
||||
@ -268,19 +271,10 @@ class CsrfViewMiddleware(MiddlewareMixin):
|
||||
# Set the Vary header since content varies with the CSRF cookie.
|
||||
patch_vary_headers(response, ("Cookie",))
|
||||
|
||||
def _origin_verified(self, request):
|
||||
def _origin_verified(self, request, host_origin):
|
||||
request_origin = request.META["HTTP_ORIGIN"]
|
||||
try:
|
||||
good_host = request.get_host()
|
||||
except DisallowedHost:
|
||||
pass
|
||||
else:
|
||||
good_origin = "%s://%s" % (
|
||||
"https" if request.is_secure() else "http",
|
||||
good_host,
|
||||
)
|
||||
if request_origin == good_origin:
|
||||
return True
|
||||
if request_origin == host_origin:
|
||||
return True
|
||||
if request_origin in self.allowed_origins_exact:
|
||||
return True
|
||||
try:
|
||||
@ -434,9 +428,17 @@ class CsrfViewMiddleware(MiddlewareMixin):
|
||||
# Reject the request if the Origin header doesn't match an allowed
|
||||
# value.
|
||||
if "HTTP_ORIGIN" in request.META:
|
||||
if not self._origin_verified(request):
|
||||
try:
|
||||
host_origin = "%s://%s" % (
|
||||
"https" if request.is_secure() else "http",
|
||||
request.get_host(),
|
||||
)
|
||||
except DisallowedHost:
|
||||
return self._reject(request, REASON_DISALLOWED_HOST)
|
||||
if not self._origin_verified(request, host_origin):
|
||||
return self._reject(
|
||||
request, REASON_BAD_ORIGIN % request.META["HTTP_ORIGIN"]
|
||||
request,
|
||||
REASON_BAD_ORIGIN % (request.META["HTTP_ORIGIN"], host_origin),
|
||||
)
|
||||
elif request.is_secure():
|
||||
# If the Origin header wasn't provided, reject HTTPS requests if
|
||||
|
@ -61,6 +61,16 @@ def csrf_failure(request, reason="", template_name=CSRF_FAILURE_TEMPLATE_NAME):
|
||||
"re-enable them, at least for this site, or for “same-origin” "
|
||||
"requests."
|
||||
),
|
||||
"bad_origin": reason.startswith("Origin checking failed"),
|
||||
"forwarded_may_fix": (
|
||||
request.headers.get("X-Forwarded-Proto", "") == "https"
|
||||
and not request.is_secure()
|
||||
or request.headers.get("Origin", "").endswith(
|
||||
f'://{request.headers.get("X-Forwarded-Host", "")}'
|
||||
)
|
||||
),
|
||||
"x_forwarded_proto": request.headers.get("X-Forwarded-Proto"),
|
||||
"x_forwarded_host": request.headers.get("X-Forwarded-Host"),
|
||||
"DEBUG": settings.DEBUG,
|
||||
"docs_version": get_docs_version(),
|
||||
"more": _("More information is available with DEBUG=True."),
|
||||
|
@ -43,6 +43,34 @@
|
||||
</pre>
|
||||
{% endif %}
|
||||
|
||||
{% if bad_origin %}
|
||||
{% if forwarded_may_fix %}
|
||||
<p>The <code>Origin</code> header does not match
|
||||
the expected server origin,
|
||||
but common proxy headers are present in the request
|
||||
and may include parts of the <code>Origin</code> header.</p>
|
||||
<ul>
|
||||
<li><strong><code>X-Forwarded-Proto</code></strong>:
|
||||
<code>{{ x_forwarded_proto }}</code></li>
|
||||
<li><strong><code>X-Forwarded-Host</code></strong>:
|
||||
<code>{{ x_forwarded_host }}</code></li>
|
||||
</ul>
|
||||
<p>If you’re sure that you are only running behind a secure proxy
|
||||
that always set these headers to avoid spoofing as described in
|
||||
<a href="https://docs.djangoproject.com/en/{{ docs_version }}/ref/settings/#secure-proxy-ssl-header">this warning in the docs</a>,
|
||||
you may wish to add one or more of the following
|
||||
settings to permit Django to trust these headers.</p>
|
||||
<p><pre>
|
||||
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
|
||||
USE_X_FORWARDED_HOST = True
|
||||
</pre></p>
|
||||
{% else %}
|
||||
If the expected server origin looks correct,
|
||||
you may wish to add the origin to the <code>CSRF_TRUSTED_ORIGINS</code>
|
||||
setting.
|
||||
{% endif %}
|
||||
{% else %}
|
||||
|
||||
<p>In general, this can occur when there is a genuine Cross Site Request Forgery, or when
|
||||
<a
|
||||
href="https://docs.djangoproject.com/en/{{ docs_version }}/ref/csrf/">Django’s
|
||||
@ -68,6 +96,7 @@
|
||||
tab or hitting the back button after a login, you may need to reload the
|
||||
page with the form, because the token is rotated after a login.</li>
|
||||
</ul>
|
||||
{% endif %}
|
||||
|
||||
<p>You’re seeing the help section of this page because you have <code>DEBUG =
|
||||
True</code> in your Django settings file. Change that to <code>False</code>,
|
||||
|
@ -198,6 +198,42 @@ Minor features
|
||||
session engines now provide async API. The new asynchronous methods all have
|
||||
``a`` prefixed names, e.g. ``aget()``, ``akeys()``, or ``acycle_key()``.
|
||||
|
||||
:mod:`django.contrib.sitemaps`
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
* ...
|
||||
|
||||
:mod:`django.contrib.sites`
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
* ...
|
||||
|
||||
:mod:`django.contrib.staticfiles`
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
* ...
|
||||
|
||||
:mod:`django.contrib.syndication`
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
* ...
|
||||
|
||||
Asynchronous views
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
* ...
|
||||
|
||||
Cache
|
||||
~~~~~
|
||||
|
||||
* ...
|
||||
|
||||
CSRF
|
||||
~~~~
|
||||
|
||||
* The error messaging is improved when ``Origin`` checking fails,
|
||||
including better hints when likely proxy headers are detected.
|
||||
|
||||
Database backends
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
|
@ -9,7 +9,6 @@ from django.middleware.csrf import (
|
||||
CSRF_SECRET_LENGTH,
|
||||
CSRF_SESSION_KEY,
|
||||
CSRF_TOKEN_LENGTH,
|
||||
REASON_BAD_ORIGIN,
|
||||
REASON_CSRF_TOKEN_MISSING,
|
||||
REASON_NO_CSRF_COOKIE,
|
||||
CsrfViewMiddleware,
|
||||
@ -891,6 +890,20 @@ class CsrfViewMiddlewareTestMixin(CsrfFunctionTestMixin):
|
||||
with self.assertRaises(OSError):
|
||||
mw.process_view(req, post_form_view, (), {})
|
||||
|
||||
@override_settings(ALLOWED_HOSTS=["www.example.com"])
|
||||
def test_good_origin_disallowed_host(self):
|
||||
"""A request with a disallowed host is rejected."""
|
||||
req = self._get_POST_request_with_token()
|
||||
req.META["HTTP_HOST"] = "www.disallowed.com"
|
||||
req.META["HTTP_ORIGIN"] = "https://www.disallowed.com"
|
||||
mw = CsrfViewMiddleware(post_form_view)
|
||||
self._check_referer_rejects(mw, req)
|
||||
with self.assertLogs("django.security.csrf", "WARNING") as cm:
|
||||
response = mw.process_view(req, post_form_view, (), {})
|
||||
self.assertEqual(response.status_code, 403)
|
||||
msg = "Host is not allowed."
|
||||
self.assertEqual(cm.records[0].getMessage(), "Forbidden (%s): " % msg)
|
||||
|
||||
@override_settings(ALLOWED_HOSTS=["www.example.com"])
|
||||
def test_bad_origin_bad_domain(self):
|
||||
"""A request with a bad origin is rejected."""
|
||||
@ -899,11 +912,15 @@ class CsrfViewMiddlewareTestMixin(CsrfFunctionTestMixin):
|
||||
req.META["HTTP_ORIGIN"] = "https://www.evil.org"
|
||||
mw = CsrfViewMiddleware(post_form_view)
|
||||
self._check_referer_rejects(mw, req)
|
||||
self.assertIs(mw._origin_verified(req), False)
|
||||
self.assertIs(mw._origin_verified(req, "http://www.example.com"), False)
|
||||
with self.assertLogs("django.security.csrf", "WARNING") as cm:
|
||||
response = mw.process_view(req, post_form_view, (), {})
|
||||
self.assertEqual(response.status_code, 403)
|
||||
msg = REASON_BAD_ORIGIN % req.META["HTTP_ORIGIN"]
|
||||
msg = (
|
||||
"Origin checking failed - 'https://www.evil.org' "
|
||||
"does not match 'http://www.example.com' "
|
||||
"or any other trusted origins."
|
||||
)
|
||||
self.assertEqual(cm.records[0].getMessage(), "Forbidden (%s): " % msg)
|
||||
|
||||
@override_settings(ALLOWED_HOSTS=["www.example.com"])
|
||||
@ -914,11 +931,15 @@ class CsrfViewMiddlewareTestMixin(CsrfFunctionTestMixin):
|
||||
req.META["HTTP_ORIGIN"] = "null"
|
||||
mw = CsrfViewMiddleware(post_form_view)
|
||||
self._check_referer_rejects(mw, req)
|
||||
self.assertIs(mw._origin_verified(req), False)
|
||||
self.assertIs(mw._origin_verified(req, "http://www.example.com"), False)
|
||||
with self.assertLogs("django.security.csrf", "WARNING") as cm:
|
||||
response = mw.process_view(req, post_form_view, (), {})
|
||||
self.assertEqual(response.status_code, 403)
|
||||
msg = REASON_BAD_ORIGIN % req.META["HTTP_ORIGIN"]
|
||||
msg = (
|
||||
"Origin checking failed - 'null' "
|
||||
"does not match 'http://www.example.com' "
|
||||
"or any other trusted origins."
|
||||
)
|
||||
self.assertEqual(cm.records[0].getMessage(), "Forbidden (%s): " % msg)
|
||||
|
||||
@override_settings(ALLOWED_HOSTS=["www.example.com"])
|
||||
@ -930,11 +951,15 @@ class CsrfViewMiddlewareTestMixin(CsrfFunctionTestMixin):
|
||||
req.META["HTTP_ORIGIN"] = "http://example.com"
|
||||
mw = CsrfViewMiddleware(post_form_view)
|
||||
self._check_referer_rejects(mw, req)
|
||||
self.assertIs(mw._origin_verified(req), False)
|
||||
self.assertIs(mw._origin_verified(req, "https://www.example.com"), False)
|
||||
with self.assertLogs("django.security.csrf", "WARNING") as cm:
|
||||
response = mw.process_view(req, post_form_view, (), {})
|
||||
self.assertEqual(response.status_code, 403)
|
||||
msg = REASON_BAD_ORIGIN % req.META["HTTP_ORIGIN"]
|
||||
msg = (
|
||||
"Origin checking failed - 'http://example.com' "
|
||||
"does not match 'https://www.example.com' "
|
||||
"or any other trusted origins."
|
||||
)
|
||||
self.assertEqual(cm.records[0].getMessage(), "Forbidden (%s): " % msg)
|
||||
|
||||
@override_settings(
|
||||
@ -957,11 +982,15 @@ class CsrfViewMiddlewareTestMixin(CsrfFunctionTestMixin):
|
||||
req.META["HTTP_ORIGIN"] = "http://foo.example.com"
|
||||
mw = CsrfViewMiddleware(post_form_view)
|
||||
self._check_referer_rejects(mw, req)
|
||||
self.assertIs(mw._origin_verified(req), False)
|
||||
self.assertIs(mw._origin_verified(req, "https://www.example.com"), False)
|
||||
with self.assertLogs("django.security.csrf", "WARNING") as cm:
|
||||
response = mw.process_view(req, post_form_view, (), {})
|
||||
self.assertEqual(response.status_code, 403)
|
||||
msg = REASON_BAD_ORIGIN % req.META["HTTP_ORIGIN"]
|
||||
msg = (
|
||||
"Origin checking failed - 'http://foo.example.com' "
|
||||
"does not match 'https://www.example.com' "
|
||||
"or any other trusted origins."
|
||||
)
|
||||
self.assertEqual(cm.records[0].getMessage(), "Forbidden (%s): " % msg)
|
||||
self.assertEqual(mw.allowed_origins_exact, {"http://no-match.com"})
|
||||
self.assertEqual(
|
||||
@ -983,11 +1012,15 @@ class CsrfViewMiddlewareTestMixin(CsrfFunctionTestMixin):
|
||||
req.META["HTTP_ORIGIN"] = "https://["
|
||||
mw = CsrfViewMiddleware(post_form_view)
|
||||
self._check_referer_rejects(mw, req)
|
||||
self.assertIs(mw._origin_verified(req), False)
|
||||
self.assertIs(mw._origin_verified(req, "http://www.example.com"), False)
|
||||
with self.assertLogs("django.security.csrf", "WARNING") as cm:
|
||||
response = mw.process_view(req, post_form_view, (), {})
|
||||
self.assertEqual(response.status_code, 403)
|
||||
msg = REASON_BAD_ORIGIN % req.META["HTTP_ORIGIN"]
|
||||
msg = (
|
||||
"Origin checking failed - 'https://[' "
|
||||
"does not match 'http://www.example.com' "
|
||||
"or any other trusted origins."
|
||||
)
|
||||
self.assertEqual(cm.records[0].getMessage(), "Forbidden (%s): " % msg)
|
||||
|
||||
@override_settings(ALLOWED_HOSTS=["www.example.com"])
|
||||
@ -997,7 +1030,7 @@ class CsrfViewMiddlewareTestMixin(CsrfFunctionTestMixin):
|
||||
req.META["HTTP_HOST"] = "www.example.com"
|
||||
req.META["HTTP_ORIGIN"] = "http://www.example.com"
|
||||
mw = CsrfViewMiddleware(post_form_view)
|
||||
self.assertIs(mw._origin_verified(req), True)
|
||||
self.assertIs(mw._origin_verified(req, "http://www.example.com"), True)
|
||||
response = mw.process_view(req, post_form_view, (), {})
|
||||
self.assertIsNone(response)
|
||||
|
||||
@ -1009,7 +1042,7 @@ class CsrfViewMiddlewareTestMixin(CsrfFunctionTestMixin):
|
||||
req.META["HTTP_HOST"] = "www.example.com"
|
||||
req.META["HTTP_ORIGIN"] = "https://www.example.com"
|
||||
mw = CsrfViewMiddleware(post_form_view)
|
||||
self.assertIs(mw._origin_verified(req), True)
|
||||
self.assertIs(mw._origin_verified(req, "https://www.example.com"), True)
|
||||
response = mw.process_view(req, post_form_view, (), {})
|
||||
self.assertIsNone(response)
|
||||
|
||||
@ -1027,7 +1060,7 @@ class CsrfViewMiddlewareTestMixin(CsrfFunctionTestMixin):
|
||||
req.META["HTTP_HOST"] = "www.example.com"
|
||||
req.META["HTTP_ORIGIN"] = "https://dashboard.example.com"
|
||||
mw = CsrfViewMiddleware(post_form_view)
|
||||
self.assertIs(mw._origin_verified(req), True)
|
||||
self.assertIs(mw._origin_verified(req, "https://www.example.com"), True)
|
||||
resp = mw.process_view(req, post_form_view, (), {})
|
||||
self.assertIsNone(resp)
|
||||
self.assertEqual(mw.allowed_origins_exact, {"https://dashboard.example.com"})
|
||||
@ -1047,12 +1080,101 @@ class CsrfViewMiddlewareTestMixin(CsrfFunctionTestMixin):
|
||||
req.META["HTTP_HOST"] = "www.example.com"
|
||||
req.META["HTTP_ORIGIN"] = "https://foo.example.com"
|
||||
mw = CsrfViewMiddleware(post_form_view)
|
||||
self.assertIs(mw._origin_verified(req), True)
|
||||
self.assertIs(mw._origin_verified(req, "https://www.example.com"), True)
|
||||
response = mw.process_view(req, post_form_view, (), {})
|
||||
self.assertIsNone(response)
|
||||
self.assertEqual(mw.allowed_origins_exact, set())
|
||||
self.assertEqual(mw.allowed_origin_subdomains, {"https": [".example.com"]})
|
||||
|
||||
@override_settings(ALLOWED_HOSTS=["www.example.com", "localhost"], DEBUG=True)
|
||||
def test_bad_origin_x_forwarded_host_with_port(self):
|
||||
"""Give a helpful message if we see a likely X-Forwarded-Host header."""
|
||||
req = self._get_POST_request_with_token()
|
||||
req.META["HTTP_HOST"] = "localhost:8000"
|
||||
req.META["HTTP_ORIGIN"] = "http://www.example.com:8080"
|
||||
req.META["HTTP_X_FORWARDED_HOST"] = "www.example.com:8080"
|
||||
mw = CsrfViewMiddleware(post_form_view)
|
||||
self._check_referer_rejects(mw, req)
|
||||
self.assertIs(mw._origin_verified(req, "http://www.example.com:8000"), False)
|
||||
with self.assertLogs("django.security.csrf", "WARNING") as cm:
|
||||
response = mw.process_view(req, post_form_view, (), {})
|
||||
self.assertEqual(response.status_code, 403)
|
||||
msg = (
|
||||
"Origin checking failed - 'http://www.example.com:8080' "
|
||||
"does not match 'http://localhost:8000' "
|
||||
"or any other trusted origins."
|
||||
)
|
||||
self.assertEqual(cm.records[0].getMessage(), "Forbidden (%s): " % msg)
|
||||
self.assertContains(response, "X-Forwarded-Host", status_code=403)
|
||||
|
||||
@override_settings(ALLOWED_HOSTS=["www.example.com", "localhost"], DEBUG=True)
|
||||
def test_bad_origin_x_forwarded_host_no_port(self):
|
||||
"""Give a helpful message if we see a likely X-Forwarded-Host header."""
|
||||
req = self._get_POST_request_with_token()
|
||||
req.META["HTTP_HOST"] = "localhost"
|
||||
req.META["HTTP_ORIGIN"] = "http://www.example.com"
|
||||
req.META["HTTP_X_FORWARDED_HOST"] = "www.example.com"
|
||||
mw = CsrfViewMiddleware(post_form_view)
|
||||
self._check_referer_rejects(mw, req)
|
||||
self.assertIs(mw._origin_verified(req, "http://localhost"), False)
|
||||
with self.assertLogs("django.security.csrf", "WARNING") as cm:
|
||||
response = mw.process_view(req, post_form_view, (), {})
|
||||
self.assertEqual(response.status_code, 403)
|
||||
msg = (
|
||||
"Origin checking failed - 'http://www.example.com' "
|
||||
"does not match 'http://localhost' "
|
||||
"or any other trusted origins."
|
||||
)
|
||||
self.assertEqual(cm.records[0].getMessage(), "Forbidden (%s): " % msg)
|
||||
self.assertContains(response, "X-Forwarded-Host", status_code=403)
|
||||
|
||||
@override_settings(ALLOWED_HOSTS=["www.example.com"], DEBUG=True)
|
||||
def test_bad_origin_x_forwarded_proto(self):
|
||||
"""Give a helpful message if we see a likely X-Forwarded-Proto header."""
|
||||
req = self._get_POST_request_with_token()
|
||||
req.META["HTTP_HOST"] = "www.example.com"
|
||||
req.META["HTTP_ORIGIN"] = "https://www.example.com"
|
||||
req.META["HTTP_X_FORWARDED_PROTO"] = "https"
|
||||
mw = CsrfViewMiddleware(post_form_view)
|
||||
self._check_referer_rejects(mw, req)
|
||||
self.assertIs(mw._origin_verified(req, "http://www.example.com"), False)
|
||||
with self.assertLogs("django.security.csrf", "WARNING") as cm:
|
||||
response = mw.process_view(req, post_form_view, (), {})
|
||||
self.assertEqual(response.status_code, 403)
|
||||
msg = (
|
||||
"Origin checking failed - 'https://www.example.com' "
|
||||
"does not match 'http://www.example.com' "
|
||||
"or any other trusted origins."
|
||||
)
|
||||
self.assertEqual(cm.records[0].getMessage(), "Forbidden (%s): " % msg)
|
||||
self.assertContains(response, "X-Forwarded-Proto", status_code=403)
|
||||
|
||||
@override_settings(ALLOWED_HOSTS=["www.example.com"], DEBUG=True)
|
||||
def test_no_likely_proxy_headers(self):
|
||||
"""Show a message about CSRF_TRUSTED_ORIGINS if no proxy headers."""
|
||||
req = self._get_POST_request_with_token()
|
||||
req.META["HTTP_HOST"] = "www.example.com"
|
||||
req.META["HTTP_ORIGIN"] = "https://www.example.com"
|
||||
mw = CsrfViewMiddleware(post_form_view)
|
||||
self._check_referer_rejects(mw, req)
|
||||
self.assertIs(mw._origin_verified(req, "http://www.example.com"), False)
|
||||
with self.assertLogs("django.security.csrf", "WARNING") as cm:
|
||||
response = mw.process_view(req, post_form_view, (), {})
|
||||
self.assertEqual(response.status_code, 403)
|
||||
msg = (
|
||||
"Origin checking failed - 'https://www.example.com' "
|
||||
"does not match 'http://www.example.com' "
|
||||
"or any other trusted origins."
|
||||
)
|
||||
self.assertEqual(cm.records[0].getMessage(), "Forbidden (%s): " % msg)
|
||||
self.assertContains(
|
||||
response, "If the expected server origin looks correct", status_code=403
|
||||
)
|
||||
self.assertContains(
|
||||
response, "you may wish to add the origin to", status_code=403
|
||||
)
|
||||
self.assertContains(response, "CSRF_TRUSTED_ORIGINS", status_code=403)
|
||||
|
||||
|
||||
class CsrfViewMiddlewareTests(CsrfViewMiddlewareTestMixin, SimpleTestCase):
|
||||
def _set_csrf_cookie(self, req, cookie):
|
||||
|
@ -130,7 +130,7 @@ class CsrfViewTests(SimpleTestCase):
|
||||
from django.views.csrf import Path
|
||||
|
||||
with mock.patch.object(Path, "open") as m:
|
||||
csrf_failure(mock.MagicMock(), mock.Mock())
|
||||
csrf_failure(mock.MagicMock(), "Example reason")
|
||||
m.assert_called_once_with(encoding="utf-8")
|
||||
|
||||
@override_settings(DEBUG=True)
|
||||
|
Loading…
Reference in New Issue
Block a user