CVE-2024-29032: Insufficient Information (NVD-CWE-noinfo)
value: Qiskit IBM Runtime is an environment that streamlines quantum computations and provides optimal implementations of the Qiskit quantum computing SDK. Starting in version 0.1.0 and prior to version 0.21.2, deserializing json data using `qiskit_ibm_runtime.RuntimeDecoder` can lead to arbitrary code execution given a correctly formatted input string. Version 0.21.2 contains a fix for this issue.
CWE description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
Published: 2024-03-20T21:15Z
Repository: https://github.com/Qiskit/qiskit-ibm-runtime
Language: Python
Files changed: qiskit_ibm_runtime/utils/json.py, test/unit/test_data_serialization.py
Commit date: 2024-03-18 12:19:02 -0400
Commit message: Merge pull request from GHSA-x4x5-jv3x-9c7m

* restrict import path

* add test
Diff: diff --git a/qiskit_ibm_runtime/utils/json.py b/qiskit_ibm_runtime/utils/json.py
index 53ab980f..9ff354ea 100644
--- a/qiskit_ibm_runtime/utils/json.py
+++ b/qiskit_ibm_runtime/utils/json.py
@@ -371,11 +371,13 @@ class RuntimeDecoder(json.JSONDecoder):
             if obj_type == "Instruction":
                 # Standalone instructions are encoded as the sole instruction in a QPY serialized circuit
                 # to deserialize load qpy circuit and return first instruction object in that circuit.
                 circuit = _decode_and_deserialize(obj_val, load)[0]
                 return circuit.data[0][0]
-            if obj_type == "settings":
+            if obj_type == "settings" and obj["__module__"].startswith(
+                "qiskit.quantum_info.operators"
+            ):
                 return _deserialize_from_settings(
                     mod_name=obj["__module__"],
                     class_name=obj["__class__"],
                     settings=_cast_strings_keys_to_int(obj_val),
                 )
diff --git a/test/unit/test_data_serialization.py b/test/unit/test_data_serialization.py
index 08b632a9..6e1de6d2 100644
--- a/test/unit/test_data_serialization.py
+++ b/test/unit/test_data_serialization.py
@@ -25,11 +25,11 @@ from ddt import data, ddt
 
 from qiskit.circuit import Parameter, ParameterVector, QuantumCircuit
 from qiskit.circuit.library import EfficientSU2, CXGate, PhaseGate, U2Gate
 
 import qiskit.quantum_info as qi
-from qiskit.quantum_info import SparsePauliOp, Pauli, Statevector
+from qiskit.quantum_info import SparsePauliOp, Pauli
 from qiskit.result import Result, Counts
 from qiskit.primitives.containers.bindings_array import BindingsArray
 from qiskit.primitives.containers.observables_array import ObservablesArray
 from qiskit.primitives.containers.estimator_pub import EstimatorPub
 from qiskit.primitives.containers.sampler_pub import SamplerPub
@@ -105,21 +105,22 @@ class TestDataSerialization(IBMTestCase):
                 decoded = json.loads(encoded, cls=RuntimeDecoder)
                 if not isinstance(circ, list):
                     decoded = [decoded]
                 self.assertTrue(all(isinstance(item, QuantumCircuit) for item in decoded))
 
-    @skip("Skip until qiskit-ibm-provider/736 is merged")
     def test_coder_operators(self):
         """Test runtime encoder and decoder for operators."""
 
-        coeff_x = Parameter("x")
-        coeff_y = coeff_x + 1
+        # TODO: Re-enable use of Parameter when #1521 is fixed.
+        # coeff_x = Parameter("x")
+        # coeff_y = coeff_x + 1
 
         subtests = (
             SparsePauliOp(Pauli("XYZX"), coeffs=[2]),
-            SparsePauliOp(Pauli("XYZX"), coeffs=[coeff_y]),
+            # SparsePauliOp(Pauli("XYZX"), coeffs=[coeff_y]),
             SparsePauliOp(Pauli("XYZX"), coeffs=[1 + 2j]),
+            Pauli("XYZ"),
         )
 
         for operator in subtests:
             with self.subTest(operator=operator):
                 encoded = json.dumps(operator, cls=RuntimeEncoder)
@@ -222,11 +223,11 @@ if __name__ == '__main__':
         temp_fp.write(script)
         temp_fp.close()
 
         subtests = (
             SparsePauliOp(Pauli("XYZX"), coeffs=[2]),
-            Statevector([1, 0]),
+            Pauli("XYZX"),
         )
         for operator in subtests:
             with self.subTest(operator=operator):
                 encoded = json.dumps(operator, cls=RuntimeEncoder)
                 self.assertIsInstance(encoded, str)
@@ -529,5 +530,20 @@ class TestContainerSerialization(IBMTestCase):
             payload = {"primitive_result": primitive_result}
             encoded = json.dumps(payload, cls=RuntimeEncoder)
             decoded = json.loads(encoded, cls=RuntimeDecoder)["primitive_result"]
             self.assertIsInstance(decoded, PrimitiveResult)
             self.assert_primitive_results_equal(primitive_result, decoded)
+
+    def test_unknown_settings(self):
+        """Test settings not on whitelisted path."""
+        random_settings = {
+            "__type__": "settings",
+            "__module__": "subprocess",
+            "__class__": "Popen",
+            "__value__": {
+                "args": ["echo", "hi"]
+            }
+        }
+        encoded = json.dumps(random_settings)
+        decoded = json.loads(encoded, cls=RuntimeDecoder)
+        self.assertIsInstance(decoded, dict)
+        self.assertDictEqual(decoded, random_settings)

Vulnerable code:             if obj_type == "settings":
from qiskit.quantum_info import SparsePauliOp, Pauli, Statevector
    @skip("Skip until qiskit-ibm-provider/736 is merged")
        coeff_x = Parameter("x")
        coeff_y = coeff_x + 1
            SparsePauliOp(Pauli("XYZX"), coeffs=[coeff_y]),
            Statevector([1, 0]),
Fixed code:             if obj_type == "settings" and obj["__module__"].startswith(
                "qiskit.quantum_info.operators"
            ):
from qiskit.quantum_info import SparsePauliOp, Pauli
        # TODO: Re-enable use of Parameter when #1521 is fixed.
        # coeff_x = Parameter("x")
        # coeff_y = coeff_x + 1
            # SparsePauliOp(Pauli("XYZX"), coeffs=[coeff_y]),
            Pauli("XYZ"),
            Pauli("XYZX"),

    def test_unknown_settings(self):
        """Test settings not on whitelisted path."""
        random_settings = {
            "__type__": "settings",
            "__module__": "subprocess",
            "__class__": "Popen",
            "__value__": {
                "args": ["echo", "hi"]
            }
        }
        encoded = json.dumps(random_settings)
        decoded = json.loads(encoded, cls=RuntimeDecoder)
        self.assertIsInstance(decoded, dict)
        self.assertDictEqual(decoded, random_settings)