fix(web): require clone dir strictly under build_vms on remove (W6)

remove_vm() deletes vmx.parent, so containment must check the clone dir, not just the vmx: a crafted vmx_id of build_vms/x.vmx passed the old check and would rmtree the entire clone base. Now the parent must be strictly inside build_vms.
This commit is contained in:
2026-07-23 18:18:52 +02:00
parent 45670394b9
commit 43925e6746
3 changed files with 39 additions and 2 deletions
+32
View File
@@ -356,6 +356,38 @@ def test_vm_remove_outside_build_vms_is_400_and_does_not_delete(tmp_path: Path)
assert not any(e.result == "ok" for e in entries)
def test_vm_remove_vmx_directly_in_build_vms_is_400_not_wiping_base(tmp_path: Path) -> None:
"""A vmx whose parent IS build_vms would rmtree the whole clone base -- reject.
remove_vm() deletes vmx.parent; for ``build_vms/x.vmx`` that parent is
build_vms itself. The containment check must reject it so a crafted
vmx_id can't wipe every clone at once.
"""
config = _make_config(tmp_path)
build_vms = config.paths.build_vms
build_vms.mkdir(parents=True, exist_ok=True)
survivor = build_vms / "Clone_keep"
survivor.mkdir()
(survivor / "Clone_keep.vmx").write_text("keep", encoding="utf-8")
danger_vmx = build_vms / "x.vmx" # parent == build_vms itself
danger_vmx.write_text("x", encoding="utf-8")
fake_backend = _FakeBackend([])
app = create_app(config)
app.dependency_overrides[get_backend] = lambda: fake_backend
client = TestClient(app)
csrf = _get_csrf(client)
vmx_id = routes_module.encode_vmx_id(danger_vmx)
resp = client.post(f"/vms/{vmx_id}/remove", data={"csrf": csrf, "stage": "execute"})
assert resp.status_code == 400
assert build_vms.exists() # clone base NOT wiped
assert survivor.exists() # sibling clone survived
assert fake_backend.delete_calls == []
def test_vm_remove_malformed_vmx_id_is_400(tmp_path: Path) -> None:
config = _make_config(tmp_path)
app = create_app(config)