From: Ming Lu <ming.lu@citrix.com>
Subject: CA-430074: Network.create lets vm-admin reconfigure dom0 IP and HIMN
 listener

The minimum allowed role of invoking Network.create is vm-admin. But the
[other_config] map of a Network object (except the key specified by
[map_keys_roles]) are of minimum role of pool-operator. So the [other_config]
parameter of Network.create should be checked to not allow two
particular keys which might impact the host internal management network
(HIMN) when the caller of the API is of vm-admin role. The HIMN is
allowed to be managed with pool-operator or higher privilege levels.

Additionally, there is another path (import) to get the privilege
escalation happen.

This is CVE-2026-102707, part of XSA-530.

Signed-off-by: Ming Lu <ming.lu@citrix.com>
Reviewed-by: Andrii Sultanov <andriy.sultanov@vates.tech>

diff --git a/ocaml/idl/datamodel.ml b/ocaml/idl/datamodel.ml
index 861e48852207..97e26278ccc3 100644
--- a/ocaml/idl/datamodel.ml
+++ b/ocaml/idl/datamodel.ml
@@ -1493,6 +1493,11 @@ module Network = struct
                   ("folder", _R_VM_OP)
                 ; ("XenCenter.CustomFields.*", _R_VM_OP)
                 ; ("XenCenterCreateInProgress", _R_VM_OP)
+                ; (* Host Internal Management Network markers. Redundant for
+                     the writer_roles, but they generate the per-key rules that
+                     Network.create checks, its default role being VM admin. *)
+                  ("is_guest_installer_network", _R_POOL_OP)
+                ; ("is_host_internal_management_network", _R_POOL_OP)
                 ]
               ~published:rel_rio
           ; field
diff --git a/ocaml/xapi/import.ml b/ocaml/xapi/import.ml
index 16ebb4c9264c..080582b47309 100644
--- a/ocaml/xapi/import.ml
+++ b/ocaml/xapi/import.ml
@@ -1273,6 +1273,17 @@ module Net : HandlerTools = struct
     | Found_net _ ->
         handle_dry_run __context config rpc session_id state x precheck_result
     | Create net_record ->
+        (* The [session_id] passed to Client.Network.create_from_record
+           (and hence to Network.create) may have been created by
+           Helpers.call_api_functions with a higher privilege level than the
+           requesting client session. This is intentional, but it means the
+           per-key RBAC check inside Network.create would evaluate that
+           elevated session and let protected keys through. The import
+           metadata is caller-controlled, so enforce the check here instead,
+           where Helpers.assert_map_with_rbac can still resolve the
+           originating session. *)
+        Xapi_network_helpers.assert_protected_map_keys_allowed ~__context
+          ~field_name:"other_config" ~value:net_record.API.network_other_config ;
         let net =
           log_reraise
             ("failed to create Network with name_label "
diff --git a/ocaml/xapi/xapi_network.ml b/ocaml/xapi/xapi_network.ml
index dfa15d0a18ab..d35de92696a5 100644
--- a/ocaml/xapi/xapi_network.ml
+++ b/ocaml/xapi/xapi_network.ml
@@ -235,6 +235,8 @@ let rec choose_bridge_name bridges =
 
 let create ~__context ~name_label ~name_description ~mTU ~other_config ~bridge
     ~managed ~tags =
+  Xapi_network_helpers.assert_protected_map_keys_allowed ~__context
+    ~field_name:"other_config" ~value:other_config ;
   with_lock mutex (fun () ->
       let networks = Db.Network.get_all ~__context in
       let bridges =
diff --git a/ocaml/xapi/xapi_network_helpers.ml b/ocaml/xapi/xapi_network_helpers.ml
index 0a6bcd47c0e2..0e1932abfda6 100644
--- a/ocaml/xapi/xapi_network_helpers.ml
+++ b/ocaml/xapi/xapi_network_helpers.ml
@@ -123,3 +123,8 @@ let assert_network_compatible_with_sriov ~__context ~pif ~network =
                 )
             )
   )
+
+let assert_protected_map_keys_allowed ~__context ~field_name ~value =
+  let match_protected = Helpers.match_protected_key "network" in
+  Helpers.assert_map_with_rbac ~__context ~match_protected
+    ~object_name:"network" ~field_name ~value
