1
0
Fork 0
mirror of https://github.com/retspen/webvirtcloud synced 2026-11-10 04:06:47 +00:00

fix: require POST for user block/unblock and OTP email, flag console cookie

- user_block, user_unblock and admin_email_otp changed state on GET, so an
  <img> tag could lock a user out or trigger OTP emails through an
  admin's session. They now require POST; the buttons are CSRF forms.
- The console token cookie is set with httponly, SameSite=Lax and, on
  HTTPS, secure. Only novncd reads it; the page gets the token directly.
This commit is contained in:
catborise 2026-10-02 22:44:00 +03:00
parent e76149c71a
commit cf1cde376b
8 changed files with 77 additions and 6 deletions

View file

@ -60,9 +60,13 @@
<a class="btn btn-success" title="{%trans "View Profile" %}" href="{% url 'accounts:account' user.id %}">{% bs_icon 'eye-fill' %}</a>
<a class="btn btn-primary" title="{%trans "Edit" %}" href="{% url 'admin:user_update' user.id %}">{% bs_icon 'pencil-fill' %}</a>
{% if user.is_active %}
<a class="btn btn-warning" title="{%trans "Block" %}" href="{% url 'admin:user_block' user.id %}">{% bs_icon 'stop-fill' %}</a>
<form method="post" action="{% url 'admin:user_block' user.id %}">{% csrf_token %}
<button type="submit" class="btn btn-warning rounded-0" title="{%trans "Block" %}">{% bs_icon 'stop-fill' %}</button>
</form>
{% else %}
<a class="btn btn-success" title="{%trans "Unblock" %}" href="{% url 'admin:user_unblock' user.id %}">{% bs_icon 'play-fill' %}</a>
<form method="post" action="{% url 'admin:user_unblock' user.id %}">{% csrf_token %}
<button type="submit" class="btn btn-success rounded-0" title="{%trans "Unblock" %}">{% bs_icon 'play-fill' %}</button>
</form>
{% endif %}
<a class="btn btn-danger" title="{%trans "Delete" %}" href="{% url 'admin:user_delete' user.id %}">{% bs_icon 'x-circle-fill' %}</a>
</div>

View file

@ -102,11 +102,21 @@ class AdminTestCase(TestCase):
self.assertEqual(ua.max_memory, 2048)
self.assertEqual(ua.max_disk_size, 8)
# Blocking changes state, so a GET (e.g. an <img> tag) must not do it.
User.objects.filter(id=user_id).update(is_active=True)
response = self.client.get(reverse("admin:user_block", args=[user_id]))
self.assertEqual(response.status_code, 405)
self.assertTrue(User.objects.get(id=user_id).is_active)
response = self.client.post(reverse("admin:user_block", args=[user_id]))
user = User.objects.get(id=user_id)
self.assertFalse(user.is_active)
response = self.client.get(reverse("admin:user_unblock", args=[user_id]))
self.assertEqual(response.status_code, 405)
self.assertFalse(User.objects.get(id=user_id).is_active)
response = self.client.post(reverse("admin:user_unblock", args=[user_id]))
user = User.objects.get(id=user_id)
self.assertTrue(user.is_active)

View file

@ -8,6 +8,7 @@ from django.contrib.auth.models import Group, User
from django.core.paginator import Paginator
from django.shortcuts import get_object_or_404, redirect, render
from django.utils.translation import gettext_lazy as _
from django.views.decorators.http import require_POST
from logs.models import Logs
from . import forms
@ -180,6 +181,7 @@ def user_delete(request, pk):
)
@require_POST
@superuser_only
def user_block(request, pk):
user: User = get_object_or_404(User, pk=pk)
@ -188,6 +190,7 @@ def user_block(request, pk):
return redirect("admin:user_list")
@require_POST
@superuser_only
def user_unblock(request, pk):
user: User = get_object_or_404(User, pk=pk)