1
0
Fork 0
mirror of https://github.com/retspen/webvirtcloud synced 2026-11-11 12:46:49 +00:00

feat(security): apply Codex review hardening to console tokens and superuser authorization

This commit is contained in:
catborise 2026-09-27 11:30:03 +03:00
parent 1227674d8f
commit 708b986e8e
11 changed files with 168 additions and 26 deletions

View file

@ -99,7 +99,7 @@ def get_connection_infos(token):
signer = TimestampSigner(salt="console.novnc")
try:
unsigned = signer.unsign(token, max_age=600)
unsigned = signer.unsign(token, max_age=120)
parts = unsigned.split(":")
if len(parts) >= 2:
host = int(parts[0])
@ -107,7 +107,7 @@ def get_connection_infos(token):
if len(parts) >= 3:
user_id = int(parts[2])
except (BadSignature, SignatureExpired, ValueError, TypeError):
if getattr(settings, "DEBUG", False) or "test" in sys.argv:
if "test" in sys.argv:
try:
temptoken = token.split("-", 1)
host = int(temptoken[0])
@ -126,6 +126,8 @@ def get_connection_infos(token):
User = get_user_model()
try:
user = User.objects.get(id=user_id)
if not user.is_active:
raise PermissionError("User account is inactive or disabled")
if not user.is_superuser and not user.has_perm("instances.view_instances"):
if not UserInstance.objects.filter(instance=instance, user=user).exists():
raise PermissionError("User no longer has access to this instance")
@ -151,9 +153,7 @@ def get_connection_infos(token):
console_port = conn.get_console_port()
console_socket = conn.get_console_socket()
except Exception as e:
logging.error(
"Fail to retrieve console connection infos for token %s : %s" % (token, e)
)
logging.error("Fail to retrieve console connection infos: %s", e)
raise
return (
connhost,

View file

@ -83,7 +83,7 @@ class ConsoleViewsTestCase(TestCase):
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, "console-vnc-lite.html")
signer = TimestampSigner(salt="console.novnc")
unsigned = signer.unsign(response.cookies["token"].value, max_age=600)
unsigned = signer.unsign(response.cookies["token"].value, max_age=120)
self.assertEqual(
unsigned, f"{self.compute.id}:{self.instance.uuid}:{self.admin_user.id}"
)
@ -521,3 +521,63 @@ class NovncdDaemonLogicTestCase(TestCase):
with self.assertRaises(PermissionError):
novncd_mod.get_connection_infos(token)
def test_get_connection_infos_inactive_user(self):
User = get_user_model()
inactive_user = User.objects.create_user(
username="inactive_console_user",
password="password",
is_active=False,
)
compute = Compute.objects.create(
name="novnc-inactive-compute",
hostname="127.0.0.1",
login="root",
password="",
type=1,
)
instance = Instance.objects.create(
compute=compute,
name="novnc-inactive-vm",
uuid="44444444-4444-4444-4444-444444444444",
)
UserInstance.objects.create(instance=instance, user=inactive_user)
signer = TimestampSigner(salt="console.novnc")
token = signer.sign(f"{compute.id}:{instance.uuid}:{inactive_user.id}")
with self.assertRaises(PermissionError):
novncd_mod.get_connection_infos(token)
@patch("sys.argv", ["novncd"])
@override_settings(DEBUG=True)
def test_get_connection_infos_debug_true_does_not_bypass(self):
with self.assertRaises(PermissionError):
novncd_mod.get_connection_infos("invalid-token-in-debug")
@patch("sys.argv", ["novncd"])
def test_get_connection_infos_expired_token(self):
User = get_user_model()
user = User.objects.filter(is_superuser=True).first()
compute = Compute.objects.create(
name="novnc-expired-compute",
hostname="127.0.0.1",
login="root",
password="",
type=1,
)
instance = Instance.objects.create(
compute=compute,
name="novnc-expired-vm",
uuid="55555555-5555-5555-5555-555555555555",
)
import time
from unittest.mock import patch
# Sign token 300 seconds in the past (> 120s TTL)
with patch("time.time", return_value=time.time() - 300):
signer = TimestampSigner(salt="console.novnc")
expired_token = signer.sign(f"{compute.id}:{instance.uuid}:{user.id}")
with self.assertRaises(PermissionError):
novncd_mod.get_connection_infos(expired_token)

View file

@ -58,7 +58,7 @@ def console(request):
if token:
try:
unsigned = signer.unsign(token, max_age=600)
unsigned = signer.unsign(token, max_age=120)
parts = unsigned.split(":")
if len(parts) >= 2:
host = int(parts[0])