Skip to content

Commit 423b969

Browse files
fix: Prevent Python RCE via __import__ on metadata classnames (#2514)
* Fixes * Consolidate common code * Rename to secure_import
1 parent d0124f1 commit 423b969

4 files changed

Lines changed: 48 additions & 58 deletions

File tree

cognitive/src/main/python/synapse/ml/services/langchain/LangchainTransform.py

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
from pyspark.sql.types import StructType, StructField, StringType
4949
from typing import cast, Optional, TypeVar, Type
5050
from synapse.ml.core.platform import running_on_synapse_internal
51+
from synapse.ml.core.serialize._safe_import import secure_import_class
5152

5253
OPENAI_API_VERSION = "2022-12-01"
5354
RL = TypeVar("RL", bound="MLReadable")
@@ -82,21 +83,9 @@ def saveImpl(self, path: str) -> None:
8283

8384

8485
class LangchainTransformerParamsReader(DefaultParamsReader):
85-
@staticmethod
86-
def __get_class(clazz: str) -> Type[RL]:
87-
"""
88-
Loads Python class from its name.
89-
"""
90-
parts = clazz.split(".")
91-
module = ".".join(parts[:-1])
92-
m = __import__(module, fromlist=[parts[-1]])
93-
return getattr(m, parts[-1])
94-
9586
def load(self, path: str) -> RL:
9687
metadata = LangchainTransformerParamsReader.loadMetadata(path, self.sc)
97-
py_type: Type[RL] = LangchainTransformerParamsReader.__get_class(
98-
metadata["class"]
99-
)
88+
py_type: Type[RL] = secure_import_class(metadata["class"])
10089
instance = py_type()
10190
cast("Params", instance)._resetUid(metadata["uid"])
10291
# deserialize the chain before setting Params

core/src/main/python/synapse/ml/core/schema/Utils.py

Lines changed: 2 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from pyspark.ml.wrapper import JavaParams
1111
from pyspark.ml.common import inherit_doc, _java2py
1212
from pyspark import SparkContext
13+
from synapse.ml.core.serialize._safe_import import secure_import_class
1314

1415

1516
def from_java(java_stage, stage_name):
@@ -26,25 +27,8 @@ def from_java(java_stage, stage_name):
2627
object: The python wrapper
2728
"""
2829

29-
def __get_class(clazz):
30-
"""
31-
Loads a python object from its class
32-
33-
Args:
34-
clazz (str): The name of the class
35-
36-
Returns:
37-
object: The python object
38-
"""
39-
parts = clazz.split(".")
40-
module = ".".join(parts[:-1])
41-
m = __import__(module)
42-
for comp in parts[1:]:
43-
m = getattr(m, comp)
44-
return m
45-
4630
# Generate a default new instance from the stage_name class.
47-
py_type = __get_class(stage_name)
31+
py_type = secure_import_class(stage_name)
4832
if issubclass(py_type, JavaParams):
4933
# Load information from java_stage to the instance.
5034
py_stage = py_type()
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# Copyright (C) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License. See LICENSE in project root for information.
3+
4+
"""
5+
Secure dynamic class loading for SynapseML model deserialization.
6+
7+
All classname-based imports during model loading MUST go through
8+
``secure_import_class`` to prevent arbitrary code execution via
9+
crafted model metadata.
10+
"""
11+
12+
import importlib
13+
14+
_ALLOWED_MODULE_PREFIXES = ("pyspark.", "synapse.ml.")
15+
16+
17+
def secure_import_class(fully_qualified_name):
18+
"""
19+
Import a Python class by its fully-qualified name, restricted to
20+
an allowlist of trusted module prefixes.
21+
22+
Args:
23+
fully_qualified_name (str): e.g. ``"pyspark.ml.classification.LogisticRegression"``
24+
25+
Returns:
26+
type: The resolved Python class.
27+
28+
Raises:
29+
ImportError: If the name does not start with an allowed prefix.
30+
"""
31+
if not fully_qualified_name.startswith(_ALLOWED_MODULE_PREFIXES):
32+
raise ImportError(
33+
f"Refusing to load class '{fully_qualified_name}': "
34+
f"module must start with one of {_ALLOWED_MODULE_PREFIXES}"
35+
)
36+
parts = fully_qualified_name.split(".")
37+
module = ".".join(parts[:-1])
38+
m = importlib.import_module(module)
39+
return getattr(m, parts[-1])

core/src/main/python/synapse/ml/core/serialize/java_params_patch.py

Lines changed: 5 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
from pyspark.ml.wrapper import JavaParams, JavaWrapper
22
from py4j.java_gateway import JavaObject
33
from pyspark import RDD, SparkContext
4-
from pyspark.serializers import PickleSerializer, AutoBatchedSerializer
5-
from pyspark.sql import DataFrame, SQLContext
4+
from pyspark.serializers import PickleSerializer
5+
from pyspark.sql import DataFrame
66
from pyspark.ml.common import _to_java_object_rdd, _java2py
7-
import pyspark
87
from pyspark.ml import PipelineModel
98
from pyspark.ml.util import DefaultParamsReader
109
from pyspark.sql.types import DataType
10+
from synapse.ml.core.serialize._safe_import import secure_import_class
1111

1212

1313
@staticmethod
@@ -19,21 +19,10 @@ def _mml_from_java(java_stage):
1919
Meta-algorithms such as Pipeline should override this method as a classmethod.
2020
"""
2121

22-
def __get_class(clazz):
23-
"""
24-
Loads Python class from its name.
25-
"""
26-
parts = clazz.split(".")
27-
module = ".".join(parts[:-1])
28-
m = __import__(module)
29-
for comp in parts[1:]:
30-
m = getattr(m, comp)
31-
return m
32-
3322
stage_name = java_stage.getClass().getName().replace("org.apache.spark", "pyspark")
3423
stage_name = stage_name.replace("com.microsoft.azure.synapse.ml", "synapse.ml")
3524
# Generate a default new instance from the stage_name class.
36-
py_type = __get_class(stage_name)
25+
py_type = secure_import_class(stage_name)
3726
if issubclass(py_type, JavaParams):
3827
# Load information from java_stage to the instance.
3928
py_stage = py_type()
@@ -59,17 +48,6 @@ def _mml_loadParamsInstance(path, sc):
5948
This assumes the instance inherits from :py:class:`MLReadable`.
6049
"""
6150

62-
def __get_class(clazz):
63-
"""
64-
Loads Python class from its name.
65-
"""
66-
parts = clazz.split(".")
67-
module = ".".join(parts[:-1])
68-
m = __import__(module)
69-
for comp in parts[1:]:
70-
m = getattr(m, comp)
71-
return m
72-
7351
metadata = DefaultParamsReader.loadMetadata(path, sc)
7452
if DefaultParamsReader.isPythonParamsInstance(metadata):
7553
pythonClassName = metadata["class"]
@@ -78,7 +56,7 @@ def __get_class(clazz):
7856
pythonClassName = pythonClassName.replace(
7957
"com.microsoft.azure.synapse.ml", "synapse.ml"
8058
)
81-
py_type = __get_class(pythonClassName)
59+
py_type = secure_import_class(pythonClassName)
8260
instance = py_type.load(path)
8361
return instance
8462

0 commit comments

Comments
 (0)