From: Ming Lu Subject: CA-430073: VM.create bypasses RBAC on HA restart priority and appliance VM.create only needs the VM admin role, but it takes a whole VM record, including fields whose setters need a higher role. Two of them are checked in this PR: - ha_restart_priority: a VM with "restart" is protected by HA and takes resources from the pool's point of view. - appliance: is managed from pool's point of view as well. There are other VM fields which currently require higher role. But one of them, ha_always_run, is deprecated. Others seem not necessary to be managed by higher role, or from the pool's point of view. This is CVE-2026-102706, part of XSA-530. Signed-off-by: Ming Lu Reviewed-by: Andrii Sultanov diff --git a/ocaml/xapi/helpers.ml b/ocaml/xapi/helpers.ml index ef46341cd143..c4cd232b1256 100644 --- a/ocaml/xapi/helpers.ml +++ b/ocaml/xapi/helpers.ml @@ -2735,3 +2735,28 @@ let assert_map_with_rbac ~__context ~match_protected ~object_name ~field_name , [action; "cannot determine the originating session"] ) ) + +(* With similar purpose of assert_map_with_rbac, check if setting a non-default + value on a field of an object is allowed. *) +let assert_field_value_with_rbac ~__context ~object_name ~field_name ~is_default + = + if not is_default then + let action = Printf.sprintf "%s.set_%s" object_name field_name in + let permission = String.lowercase_ascii action in + match original_session ~__context with + | Ok None -> + () + | Ok (Some session_id) -> + if not (Rbac.is_access_allowed ~__context ~session_id ~permission) then + raise + (Rbac.disallowed_permission_exn ~extra_dmsg:"" ~extra_msg:"" + ~__context ~permission ~action + ) + | Error () -> + raise + Api_errors.( + Server_error + ( rbac_permission_denied + , [action; "cannot determine the originating session"] + ) + ) diff --git a/ocaml/xapi/import.ml b/ocaml/xapi/import.ml index b71fb7c2dbd0..16ebb4c9264c 100644 --- a/ocaml/xapi/import.ml +++ b/ocaml/xapi/import.ml @@ -521,6 +521,10 @@ module VM : HandlerTools = struct ~field_name:"other_config" ~value:vm_record.API.vM_other_config ; Xapi_vm_helpers.assert_protected_map_keys_allowed ~__context ~field_name:"platform" ~value:vm_record.API.vM_platform ; + (* The appliance is cleared on import, so it isn't checked. *) + Xapi_vm_helpers.assert_protected_field_allowed ~__context + ~field_name:"ha_restart_priority" + ~is_default:(vm_record.API.vM_ha_restart_priority = "") ; if is_live config then assert_can_live_import __context vm_record ; ( if needs_cpu_check config vm_record then let vmm_record = diff --git a/ocaml/xapi/xapi_vm.ml b/ocaml/xapi/xapi_vm.ml index 2573cb8af024..da87a18752c4 100644 --- a/ocaml/xapi/xapi_vm.ml +++ b/ocaml/xapi/xapi_vm.ml @@ -595,6 +595,10 @@ let create ~__context ~name_label ~name_description ~power_state ~user_version ~field_name:"other_config" ~value:other_config ; Xapi_vm_helpers.assert_protected_map_keys_allowed ~__context ~field_name:"platform" ~value:platform ; + Xapi_vm_helpers.assert_protected_field_allowed ~__context + ~field_name:"ha_restart_priority" ~is_default:(ha_restart_priority = "") ; + Xapi_vm_helpers.assert_protected_field_allowed ~__context + ~field_name:"appliance" ~is_default:(appliance = Ref.null) ; (* NB apart from the above, parameter validation is delayed until VM.start *) let uuid = Uuidx.make () in let vm_ref = Ref.make () in diff --git a/ocaml/xapi/xapi_vm_clone.ml b/ocaml/xapi/xapi_vm_clone.ml index ea76589db52b..3d62851e5209 100644 --- a/ocaml/xapi/xapi_vm_clone.ml +++ b/ocaml/xapi/xapi_vm_clone.ml @@ -431,11 +431,22 @@ let assert_protected_map_keys_allowed ~__context ~vm disk_op = | Disk_op_snapshot | Disk_op_checkpoint -> () +let assert_protected_fields_allowed ~__context ~vm disk_op = + match disk_op with + | Disk_op_clone | Disk_op_copy _ -> + (* The appliance of the new VM is reset, so it isn't checked. *) + Xapi_vm_helpers.assert_protected_field_allowed ~__context + ~field_name:"ha_restart_priority" + ~is_default:(Db.VM.get_ha_restart_priority ~__context ~self:vm = "") + | Disk_op_snapshot | Disk_op_checkpoint -> + () + (* NB this function may be called when the VM is suspended for copy/clone operations. Snapshot can be done in live. *) let clone ?snapshot_info_record ?(ignore_vdis = []) disk_op ~__context ~vm ~new_name = assert_protected_map_keys_allowed ~__context ~vm disk_op ; + assert_protected_fields_allowed ~__context ~vm disk_op ; Helpers.call_api_functions ~__context (fun rpc session_id -> let task_id = Ref.string_of (Context.get_task_id __context) in let vbds = Db.VM.get_VBDs ~__context ~self:vm in diff --git a/ocaml/xapi/xapi_vm_helpers.ml b/ocaml/xapi/xapi_vm_helpers.ml index bcb5e8875cb7..c6270a6a8245 100644 --- a/ocaml/xapi/xapi_vm_helpers.ml +++ b/ocaml/xapi/xapi_vm_helpers.ml @@ -1743,3 +1743,7 @@ let assert_protected_map_keys_allowed ~__context ~field_name ~value = let match_protected = Helpers.match_protected_key "VM" in Helpers.assert_map_with_rbac ~__context ~match_protected ~object_name:"vm" ~field_name ~value + +let assert_protected_field_allowed ~__context ~field_name ~is_default = + Helpers.assert_field_value_with_rbac ~__context ~object_name:"vm" ~field_name + ~is_default