From f670b6bb18d42ed33f6dc84858a256d394f4f38c Mon Sep 17 00:00:00 2001 From: Victoria Hall Date: Thu, 23 Apr 2026 10:35:59 -0500 Subject: [PATCH 1/4] create safe list of modules / classes to deserialize --- azure/functions/_durable_functions.py | 34 ++- tests/test_durable_functions_security.py | 255 +++++++++++++++++++++++ 2 files changed, 288 insertions(+), 1 deletion(-) create mode 100644 tests/test_durable_functions_security.py diff --git a/azure/functions/_durable_functions.py b/azure/functions/_durable_functions.py index aa533679..3d49d413 100644 --- a/azure/functions/_durable_functions.py +++ b/azure/functions/_durable_functions.py @@ -6,6 +6,15 @@ from importlib import import_module +# Allowlist of modules and classes that can be safely deserialized +# This prevents arbitrary code execution via malicious module imports +_SAFE_DESERIALIZATION_ALLOWLIST = { + 'azure.functions._cosmosdb': {'Document'}, + 'azure.functions._sql': {'SqlRow'}, + 'azure.functions._mysql': {'MySqlRow'}, +} + + # Utilities def _serialize_custom_object(obj): """Serialize a user-defined object to JSON. @@ -50,6 +59,9 @@ def _deserialize_custom_object(obj: dict) -> object: if it contains class metadata suggesting that it should be decoded further. + SECURITY: Only modules and classes in the allowlist can be deserialized + to prevent arbitrary code execution via malicious module imports. + Parameters: ---------- obj: dict @@ -62,6 +74,8 @@ def _deserialize_custom_object(obj: dict) -> object: Exceptions ---------- + ValueError + If the module or class is not in the safe deserialization allowlist TypeError If the decoded object does not contain a `from_json` function """ @@ -70,7 +84,25 @@ def _deserialize_custom_object(obj: dict) -> object: module_name = obj.pop("__module__") obj_data = obj.pop("__data__") - # Importing the clas + # SECURITY: Validate module and class against allowlist BEFORE importing + # This prevents arbitrary code execution via module-level code + if module_name not in _SAFE_DESERIALIZATION_ALLOWLIST: + raise ValueError( + f"Deserialization of module '{module_name}' is not allowed. " + f"Only the following modules are permitted: " + f"{', '.join(_SAFE_DESERIALIZATION_ALLOWLIST.keys())}" + ) + + allowed_classes = _SAFE_DESERIALIZATION_ALLOWLIST[module_name] + if class_name not in allowed_classes: + raise ValueError( + f"Deserialization of class '{class_name}' from module " + f"'{module_name}' is not allowed. " + f"Only the following classes are permitted: " + f"{', '.join(allowed_classes)}" + ) + + # Safe to import now that we've validated the module and class module = import_module(module_name) class_ = getattr(module, class_name) diff --git a/tests/test_durable_functions_security.py b/tests/test_durable_functions_security.py new file mode 100644 index 00000000..eff6b7d6 --- /dev/null +++ b/tests/test_durable_functions_security.py @@ -0,0 +1,255 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +""" +Security tests for durable functions deserialization. + +These tests verify that the security fix for arbitrary code execution +via malicious JSON payloads is working correctly. +""" + +import unittest +import json +import os +import sys +import tempfile + +from azure.functions.durable_functions import ActivityTriggerConverter +from azure.functions.meta import Datum +from azure.functions._cosmosdb import Document +from azure.functions._sql import SqlRow +from azure.functions._mysql import MySqlRow + + +class TestDurableFunctionsSecurityFix(unittest.TestCase): + """Test that the security vulnerability CVE fix is effective.""" + + def test_legitimate_cosmosdb_document_deserialization_works(self): + """Verify that legitimate Document deserialization still works.""" + payload = { + '__module__': 'azure.functions._cosmosdb', + '__class__': 'Document', + '__data__': '{"id": "123", "name": "test"}' + } + datum = Datum(value=json.dumps(payload), type='json') + + result = ActivityTriggerConverter.decode(datum, trigger_metadata={}) + + self.assertIsInstance(result, Document) + self.assertEqual(result['id'], '123') + self.assertEqual(result['name'], 'test') + + def test_legitimate_sql_row_deserialization_works(self): + """Verify that legitimate SqlRow deserialization still works.""" + payload = { + '__module__': 'azure.functions._sql', + '__class__': 'SqlRow', + '__data__': '{"column1": "value1", "column2": "value2"}' + } + datum = Datum(value=json.dumps(payload), type='json') + + result = ActivityTriggerConverter.decode(datum, trigger_metadata={}) + + self.assertIsInstance(result, SqlRow) + self.assertEqual(result['column1'], 'value1') + self.assertEqual(result['column2'], 'value2') + + def test_legitimate_mysql_row_deserialization_works(self): + """Verify that legitimate MySqlRow deserialization still works.""" + payload = { + '__module__': 'azure.functions._mysql', + '__class__': 'MySqlRow', + '__data__': '{"field1": "data1", "field2": "data2"}' + } + datum = Datum(value=json.dumps(payload), type='json') + + result = ActivityTriggerConverter.decode(datum, trigger_metadata={}) + + self.assertIsInstance(result, MySqlRow) + self.assertEqual(result['field1'], 'data1') + self.assertEqual(result['field2'], 'data2') + + def test_arbitrary_module_import_blocked(self): + """ + SECURITY TEST: Verify that arbitrary module imports are blocked. + + This test creates a malicious module and attempts to import it + via the deserialization function. The import should be blocked + by the allowlist before the module's code can execute. + """ + # Create a temporary malicious module + tmpdir = tempfile.mkdtemp() + marker_file = os.path.join(tmpdir, 'malicious_code_executed.txt') + evil_module_path = os.path.join(tmpdir, 'evil_module.py') + + # Write malicious module with code that executes on import + with open(evil_module_path, 'w') as f: + f.write(f''' +# Malicious code that runs on module import +with open(r"{marker_file}", "w") as f: + f.write("MALICIOUS CODE EXECUTED") + +class EvilClass: + @classmethod + def from_json(cls, data): + return cls() +''') + + # Add temp directory to sys.path to make module importable + sys.path.insert(0, tmpdir) + + try: + # Attempt to deserialize with malicious module + payload = { + '__module__': 'evil_module', + '__class__': 'EvilClass', + '__data__': '{}' + } + datum = Datum(value=json.dumps(payload), type='json') + + # This should raise a ValueError due to allowlist + with self.assertRaises(ValueError) as cm: + ActivityTriggerConverter.decode(datum, trigger_metadata={}) + + # The error is wrapped by ActivityTriggerConverter, check the cause + self.assertIsNotNone(cm.exception.__cause__) + cause_msg = str(cm.exception.__cause__) + self.assertIn('evil_module', cause_msg) + self.assertIn('not allowed', cause_msg.lower()) + + # CRITICAL: Verify the malicious code did NOT execute + self.assertFalse( + os.path.exists(marker_file), + "SECURITY FAILURE: Malicious code executed during module import!" + ) + + finally: + # Clean up + sys.path.remove(tmpdir) + if os.path.exists(evil_module_path): + os.remove(evil_module_path) + if os.path.exists(marker_file): + os.remove(marker_file) + os.rmdir(tmpdir) + + def test_unauthorized_class_from_allowed_module_blocked(self): + """SECURITY TEST: Verify that unauthorized classes from allowed modules are blocked.""" + # Try to deserialize a class that doesn't exist in the allowlist + payload = { + '__module__': 'azure.functions._cosmosdb', + '__class__': 'FakeClass', # Not in allowlist + '__data__': '{}' + } + datum = Datum(value=json.dumps(payload), type='json') + + with self.assertRaises(ValueError) as cm: + ActivityTriggerConverter.decode(datum, trigger_metadata={}) + + # Check the wrapped exception cause + self.assertIsNotNone(cm.exception.__cause__) + cause_msg = str(cm.exception.__cause__) + self.assertIn('FakeClass', cause_msg) + self.assertIn('not allowed', cause_msg.lower()) + + def test_builtin_module_blocked(self): + """SECURITY TEST: Verify that built-in dangerous modules are blocked.""" + # Attempt to import built-in modules that could be dangerous + dangerous_payloads = [ + { + '__module__': 'os', + '__class__': 'system', + '__data__': 'echo pwned' + }, + { + '__module__': 'subprocess', + '__class__': 'Popen', + '__data__': '{}' + }, + { + '__module__': '__builtin__', + '__class__': 'eval', + '__data__': 'print("pwned")' + } + ] + + for payload in dangerous_payloads: + datum = Datum(value=json.dumps(payload), type='json') + + with self.assertRaises(ValueError) as cm: + ActivityTriggerConverter.decode(datum, trigger_metadata={}) + + # Check the wrapped exception cause + self.assertIsNotNone(cm.exception.__cause__) + self.assertIn('not allowed', str(cm.exception.__cause__).lower()) + + def test_nested_malicious_object_blocked(self): + """ + SECURITY TEST: Verify that nested malicious objects are also blocked. + + The object_hook is called for every nested object in the JSON, + so we need to ensure malicious payloads can't be smuggled in + nested structures. + """ + payload = { + 'legitimate_data': 'some value', + 'nested_attack': { + '__module__': 'os', + '__class__': 'system', + '__data__': 'echo pwned' + } + } + datum = Datum(value=json.dumps(payload), type='json') + + with self.assertRaises(ValueError) as cm: + ActivityTriggerConverter.decode(datum, trigger_metadata={}) + + # Check the wrapped exception cause + self.assertIsNotNone(cm.exception.__cause__) + self.assertIn('not allowed', str(cm.exception.__cause__).lower()) + + def test_nested_legitimate_object_works(self): + """Verify that nested legitimate objects still work correctly.""" + payload = { + 'normal_data': 'value', + 'nested_document': { + '__module__': 'azure.functions._cosmosdb', + '__class__': 'Document', + '__data__': '{"nested": "data"}' + } + } + datum = Datum(value=json.dumps(payload), type='json') + + result = ActivityTriggerConverter.decode(datum, trigger_metadata={}) + + self.assertEqual(result['normal_data'], 'value') + self.assertIsInstance(result['nested_document'], Document) + self.assertEqual(result['nested_document']['nested'], 'data') + + def test_allowlist_comprehensiveness(self): + """ + Verify that the allowlist includes all expected legitimate classes + and only those classes. + """ + from azure.functions._durable_functions import _SAFE_DESERIALIZATION_ALLOWLIST + + # Verify expected modules are present + self.assertIn('azure.functions._cosmosdb', _SAFE_DESERIALIZATION_ALLOWLIST) + self.assertIn('azure.functions._sql', _SAFE_DESERIALIZATION_ALLOWLIST) + self.assertIn('azure.functions._mysql', _SAFE_DESERIALIZATION_ALLOWLIST) + + # Verify expected classes are present + self.assertIn('Document', _SAFE_DESERIALIZATION_ALLOWLIST['azure.functions._cosmosdb']) + self.assertIn('SqlRow', _SAFE_DESERIALIZATION_ALLOWLIST['azure.functions._sql']) + self.assertIn('MySqlRow', _SAFE_DESERIALIZATION_ALLOWLIST['azure.functions._mysql']) + + # Verify no unexpected modules or classes + expected_modules = { + 'azure.functions._cosmosdb', + 'azure.functions._sql', + 'azure.functions._mysql' + } + self.assertEqual(set(_SAFE_DESERIALIZATION_ALLOWLIST.keys()), expected_modules) + + +if __name__ == '__main__': + unittest.main() From 4c514b695a5f41b339ddd28026ee88c0e5efa9cc Mon Sep 17 00:00:00 2001 From: Victoria Hall Date: Thu, 23 Apr 2026 10:38:11 -0500 Subject: [PATCH 2/4] test syntax --- tests/test_durable_functions_security.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tests/test_durable_functions_security.py b/tests/test_durable_functions_security.py index eff6b7d6..310690ec 100644 --- a/tests/test_durable_functions_security.py +++ b/tests/test_durable_functions_security.py @@ -249,7 +249,3 @@ def test_allowlist_comprehensiveness(self): 'azure.functions._mysql' } self.assertEqual(set(_SAFE_DESERIALIZATION_ALLOWLIST.keys()), expected_modules) - - -if __name__ == '__main__': - unittest.main() From faa92dea44b7a14aaa741b8ee855068eec498c21 Mon Sep 17 00:00:00 2001 From: Victoria Hall Date: Thu, 23 Apr 2026 11:40:10 -0500 Subject: [PATCH 3/4] remove comments --- azure/functions/_durable_functions.py | 11 +- tests/test_durable_functions_security.py | 251 ----------------------- 2 files changed, 2 insertions(+), 260 deletions(-) delete mode 100644 tests/test_durable_functions_security.py diff --git a/azure/functions/_durable_functions.py b/azure/functions/_durable_functions.py index 3d49d413..fdf21475 100644 --- a/azure/functions/_durable_functions.py +++ b/azure/functions/_durable_functions.py @@ -6,8 +6,6 @@ from importlib import import_module -# Allowlist of modules and classes that can be safely deserialized -# This prevents arbitrary code execution via malicious module imports _SAFE_DESERIALIZATION_ALLOWLIST = { 'azure.functions._cosmosdb': {'Document'}, 'azure.functions._sql': {'SqlRow'}, @@ -59,9 +57,6 @@ def _deserialize_custom_object(obj: dict) -> object: if it contains class metadata suggesting that it should be decoded further. - SECURITY: Only modules and classes in the allowlist can be deserialized - to prevent arbitrary code execution via malicious module imports. - Parameters: ---------- obj: dict @@ -75,7 +70,7 @@ def _deserialize_custom_object(obj: dict) -> object: Exceptions ---------- ValueError - If the module or class is not in the safe deserialization allowlist + If the module or class is not in the deserialization list TypeError If the decoded object does not contain a `from_json` function """ @@ -84,8 +79,7 @@ def _deserialize_custom_object(obj: dict) -> object: module_name = obj.pop("__module__") obj_data = obj.pop("__data__") - # SECURITY: Validate module and class against allowlist BEFORE importing - # This prevents arbitrary code execution via module-level code + # Validate module and class if module_name not in _SAFE_DESERIALIZATION_ALLOWLIST: raise ValueError( f"Deserialization of module '{module_name}' is not allowed. " @@ -102,7 +96,6 @@ def _deserialize_custom_object(obj: dict) -> object: f"{', '.join(allowed_classes)}" ) - # Safe to import now that we've validated the module and class module = import_module(module_name) class_ = getattr(module, class_name) diff --git a/tests/test_durable_functions_security.py b/tests/test_durable_functions_security.py deleted file mode 100644 index 310690ec..00000000 --- a/tests/test_durable_functions_security.py +++ /dev/null @@ -1,251 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -""" -Security tests for durable functions deserialization. - -These tests verify that the security fix for arbitrary code execution -via malicious JSON payloads is working correctly. -""" - -import unittest -import json -import os -import sys -import tempfile - -from azure.functions.durable_functions import ActivityTriggerConverter -from azure.functions.meta import Datum -from azure.functions._cosmosdb import Document -from azure.functions._sql import SqlRow -from azure.functions._mysql import MySqlRow - - -class TestDurableFunctionsSecurityFix(unittest.TestCase): - """Test that the security vulnerability CVE fix is effective.""" - - def test_legitimate_cosmosdb_document_deserialization_works(self): - """Verify that legitimate Document deserialization still works.""" - payload = { - '__module__': 'azure.functions._cosmosdb', - '__class__': 'Document', - '__data__': '{"id": "123", "name": "test"}' - } - datum = Datum(value=json.dumps(payload), type='json') - - result = ActivityTriggerConverter.decode(datum, trigger_metadata={}) - - self.assertIsInstance(result, Document) - self.assertEqual(result['id'], '123') - self.assertEqual(result['name'], 'test') - - def test_legitimate_sql_row_deserialization_works(self): - """Verify that legitimate SqlRow deserialization still works.""" - payload = { - '__module__': 'azure.functions._sql', - '__class__': 'SqlRow', - '__data__': '{"column1": "value1", "column2": "value2"}' - } - datum = Datum(value=json.dumps(payload), type='json') - - result = ActivityTriggerConverter.decode(datum, trigger_metadata={}) - - self.assertIsInstance(result, SqlRow) - self.assertEqual(result['column1'], 'value1') - self.assertEqual(result['column2'], 'value2') - - def test_legitimate_mysql_row_deserialization_works(self): - """Verify that legitimate MySqlRow deserialization still works.""" - payload = { - '__module__': 'azure.functions._mysql', - '__class__': 'MySqlRow', - '__data__': '{"field1": "data1", "field2": "data2"}' - } - datum = Datum(value=json.dumps(payload), type='json') - - result = ActivityTriggerConverter.decode(datum, trigger_metadata={}) - - self.assertIsInstance(result, MySqlRow) - self.assertEqual(result['field1'], 'data1') - self.assertEqual(result['field2'], 'data2') - - def test_arbitrary_module_import_blocked(self): - """ - SECURITY TEST: Verify that arbitrary module imports are blocked. - - This test creates a malicious module and attempts to import it - via the deserialization function. The import should be blocked - by the allowlist before the module's code can execute. - """ - # Create a temporary malicious module - tmpdir = tempfile.mkdtemp() - marker_file = os.path.join(tmpdir, 'malicious_code_executed.txt') - evil_module_path = os.path.join(tmpdir, 'evil_module.py') - - # Write malicious module with code that executes on import - with open(evil_module_path, 'w') as f: - f.write(f''' -# Malicious code that runs on module import -with open(r"{marker_file}", "w") as f: - f.write("MALICIOUS CODE EXECUTED") - -class EvilClass: - @classmethod - def from_json(cls, data): - return cls() -''') - - # Add temp directory to sys.path to make module importable - sys.path.insert(0, tmpdir) - - try: - # Attempt to deserialize with malicious module - payload = { - '__module__': 'evil_module', - '__class__': 'EvilClass', - '__data__': '{}' - } - datum = Datum(value=json.dumps(payload), type='json') - - # This should raise a ValueError due to allowlist - with self.assertRaises(ValueError) as cm: - ActivityTriggerConverter.decode(datum, trigger_metadata={}) - - # The error is wrapped by ActivityTriggerConverter, check the cause - self.assertIsNotNone(cm.exception.__cause__) - cause_msg = str(cm.exception.__cause__) - self.assertIn('evil_module', cause_msg) - self.assertIn('not allowed', cause_msg.lower()) - - # CRITICAL: Verify the malicious code did NOT execute - self.assertFalse( - os.path.exists(marker_file), - "SECURITY FAILURE: Malicious code executed during module import!" - ) - - finally: - # Clean up - sys.path.remove(tmpdir) - if os.path.exists(evil_module_path): - os.remove(evil_module_path) - if os.path.exists(marker_file): - os.remove(marker_file) - os.rmdir(tmpdir) - - def test_unauthorized_class_from_allowed_module_blocked(self): - """SECURITY TEST: Verify that unauthorized classes from allowed modules are blocked.""" - # Try to deserialize a class that doesn't exist in the allowlist - payload = { - '__module__': 'azure.functions._cosmosdb', - '__class__': 'FakeClass', # Not in allowlist - '__data__': '{}' - } - datum = Datum(value=json.dumps(payload), type='json') - - with self.assertRaises(ValueError) as cm: - ActivityTriggerConverter.decode(datum, trigger_metadata={}) - - # Check the wrapped exception cause - self.assertIsNotNone(cm.exception.__cause__) - cause_msg = str(cm.exception.__cause__) - self.assertIn('FakeClass', cause_msg) - self.assertIn('not allowed', cause_msg.lower()) - - def test_builtin_module_blocked(self): - """SECURITY TEST: Verify that built-in dangerous modules are blocked.""" - # Attempt to import built-in modules that could be dangerous - dangerous_payloads = [ - { - '__module__': 'os', - '__class__': 'system', - '__data__': 'echo pwned' - }, - { - '__module__': 'subprocess', - '__class__': 'Popen', - '__data__': '{}' - }, - { - '__module__': '__builtin__', - '__class__': 'eval', - '__data__': 'print("pwned")' - } - ] - - for payload in dangerous_payloads: - datum = Datum(value=json.dumps(payload), type='json') - - with self.assertRaises(ValueError) as cm: - ActivityTriggerConverter.decode(datum, trigger_metadata={}) - - # Check the wrapped exception cause - self.assertIsNotNone(cm.exception.__cause__) - self.assertIn('not allowed', str(cm.exception.__cause__).lower()) - - def test_nested_malicious_object_blocked(self): - """ - SECURITY TEST: Verify that nested malicious objects are also blocked. - - The object_hook is called for every nested object in the JSON, - so we need to ensure malicious payloads can't be smuggled in - nested structures. - """ - payload = { - 'legitimate_data': 'some value', - 'nested_attack': { - '__module__': 'os', - '__class__': 'system', - '__data__': 'echo pwned' - } - } - datum = Datum(value=json.dumps(payload), type='json') - - with self.assertRaises(ValueError) as cm: - ActivityTriggerConverter.decode(datum, trigger_metadata={}) - - # Check the wrapped exception cause - self.assertIsNotNone(cm.exception.__cause__) - self.assertIn('not allowed', str(cm.exception.__cause__).lower()) - - def test_nested_legitimate_object_works(self): - """Verify that nested legitimate objects still work correctly.""" - payload = { - 'normal_data': 'value', - 'nested_document': { - '__module__': 'azure.functions._cosmosdb', - '__class__': 'Document', - '__data__': '{"nested": "data"}' - } - } - datum = Datum(value=json.dumps(payload), type='json') - - result = ActivityTriggerConverter.decode(datum, trigger_metadata={}) - - self.assertEqual(result['normal_data'], 'value') - self.assertIsInstance(result['nested_document'], Document) - self.assertEqual(result['nested_document']['nested'], 'data') - - def test_allowlist_comprehensiveness(self): - """ - Verify that the allowlist includes all expected legitimate classes - and only those classes. - """ - from azure.functions._durable_functions import _SAFE_DESERIALIZATION_ALLOWLIST - - # Verify expected modules are present - self.assertIn('azure.functions._cosmosdb', _SAFE_DESERIALIZATION_ALLOWLIST) - self.assertIn('azure.functions._sql', _SAFE_DESERIALIZATION_ALLOWLIST) - self.assertIn('azure.functions._mysql', _SAFE_DESERIALIZATION_ALLOWLIST) - - # Verify expected classes are present - self.assertIn('Document', _SAFE_DESERIALIZATION_ALLOWLIST['azure.functions._cosmosdb']) - self.assertIn('SqlRow', _SAFE_DESERIALIZATION_ALLOWLIST['azure.functions._sql']) - self.assertIn('MySqlRow', _SAFE_DESERIALIZATION_ALLOWLIST['azure.functions._mysql']) - - # Verify no unexpected modules or classes - expected_modules = { - 'azure.functions._cosmosdb', - 'azure.functions._sql', - 'azure.functions._mysql' - } - self.assertEqual(set(_SAFE_DESERIALIZATION_ALLOWLIST.keys()), expected_modules) From c1cac1a0b40fe5dc26170a6403624862223bcae8 Mon Sep 17 00:00:00 2001 From: Victoria Hall Date: Thu, 23 Apr 2026 12:06:49 -0500 Subject: [PATCH 4/4] change name --- azure/functions/_durable_functions.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/azure/functions/_durable_functions.py b/azure/functions/_durable_functions.py index fdf21475..833b65ad 100644 --- a/azure/functions/_durable_functions.py +++ b/azure/functions/_durable_functions.py @@ -6,7 +6,7 @@ from importlib import import_module -_SAFE_DESERIALIZATION_ALLOWLIST = { +_DESERIALIZATION_LIST = { 'azure.functions._cosmosdb': {'Document'}, 'azure.functions._sql': {'SqlRow'}, 'azure.functions._mysql': {'MySqlRow'}, @@ -80,14 +80,14 @@ def _deserialize_custom_object(obj: dict) -> object: obj_data = obj.pop("__data__") # Validate module and class - if module_name not in _SAFE_DESERIALIZATION_ALLOWLIST: + if module_name not in _DESERIALIZATION_LIST: raise ValueError( f"Deserialization of module '{module_name}' is not allowed. " f"Only the following modules are permitted: " - f"{', '.join(_SAFE_DESERIALIZATION_ALLOWLIST.keys())}" + f"{', '.join(_DESERIALIZATION_LIST.keys())}" ) - allowed_classes = _SAFE_DESERIALIZATION_ALLOWLIST[module_name] + allowed_classes = _DESERIALIZATION_LIST[module_name] if class_name not in allowed_classes: raise ValueError( f"Deserialization of class '{class_name}' from module "