Files
2026-07-13 12:40:42 +08:00

250 lines
7.6 KiB
Python

# Copyright (c) 2026 PaddlePaddle Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
Restricted Unpickler for secure deserialization of model files.
This module provides a RestrictedUnpickler that only allows a whitelist
of safe classes to be deserialized, preventing arbitrary code execution
via malicious pickle payloads (CWE-502).
"""
from __future__ import annotations
import pickle
import types
from enum import Enum
# Whitelist of allowed modules and their allowed classes.
# Only these classes can be instantiated during deserialization.
_ALLOWED_CLASSES: dict[str, set[str]] = {
# NumPy types (required for model parameters)
'numpy': {
'ndarray',
'dtype',
'float32',
'float64',
'float16',
'int32',
'int64',
'int16',
'int8',
'uint8',
'bool_',
'complex64',
'complex128',
'bfloat16',
},
'numpy.core.multiarray': {
'_reconstruct',
'scalar',
},
'numpy.core.numeric': {
'*',
},
'numpy._core.multiarray': {
'_reconstruct',
'scalar',
},
'numpy._core.numeric': {
'*',
},
# Collections (required for state_dict structures)
'collections': {
'OrderedDict',
'defaultdict',
},
# Python builtins (required for basic data types in state dicts)
'builtins': {
'dict',
'list',
'tuple',
'set',
'frozenset',
'bytes',
'bytearray',
'str',
'int',
'float',
'bool',
'complex',
'slice',
'range',
'type',
},
# copyreg (used by pickle protocol for reconstructing objects)
'copyreg': {
'_reconstructor',
},
# _codecs (used for encoding in pickle)
'_codecs': {
'encode',
},
# Paddle internal: safe DenseTensor reconstruction function
'paddle.framework.io_utils': {
'_reconstruct_dense_tensor_data',
},
# Paddle internal: generator state for RNG serialization
'paddle.base.libpaddle': {
'GeneratorState',
},
# Paddle internal: distributed flex checkpoint metadata classes
# These dataclasses are serialized via paddle.save() during checkpoint
# operations and must be allowed for paddle.load() to restore them.
'paddle.distributed.flex_checkpoint.dcp.metadata': {
'Metadata',
'LocalTensorMetadata',
'LocalTensorIndex',
},
}
def _is_safe_class(cls) -> bool:
"""Check if a class is safe to deserialize.
Returns True if the class is a user-defined class without dangerous methods.
Returns False for built-in functions, modules, and classes with __reduce__.
This allows paddle.load() to safely deserialize configuration classes
(like PreTrainingArguments) that are saved via paddle.save(), while
blocking potential RCE attacks through __reduce__ exploitation.
"""
# Reject built-in functions and modules
if isinstance(
cls,
(types.BuiltinFunctionType, types.BuiltinMethodType, types.ModuleType),
):
return False
# Only allow actual classes (types)
if not isinstance(cls, type):
return False
# Check if class has __dict__ (user-defined classes do)
cls_dict = getattr(cls, '__dict__', None)
if cls_dict is None:
return False
# Check for dangerous methods that could be exploited for RCE
dangerous_methods = {
'__reduce__',
'__reduce_ex__',
'__getstate__',
'__setstate__',
}
for method in dangerous_methods:
# Check each class in the MRO for dangerous method definitions
for base in cls.__mro__:
# Skip object - its default __reduce__ is safe for user-defined classes
if base is object:
continue
# Enum-related stdlib base implementations are safe.
if base is Enum:
continue
# Check if this base class defines the dangerous method
if method in getattr(base, '__dict__', {}):
return False
return True
class RestrictedUnpickler(pickle.Unpickler):
"""A restricted unpickler that only allows whitelisted classes.
This prevents arbitrary code execution during deserialization by
blocking dangerous modules such as os, subprocess, builtins.eval,
builtins.exec, etc.
Usage:
with open('model.pdparams', 'rb') as f:
data = RestrictedUnpickler(f).load()
"""
def find_class(self, module: str, name: str) -> type:
"""Override find_class to restrict which classes can be loaded.
Args:
module: The module name containing the class.
name: The class name to load.
Returns:
The class object if it is in the whitelist or is a safe class.
Raises:
pickle.UnpicklingError: If the class is not in the whitelist
and is not a safe user-defined class.
"""
allowed_names = _ALLOWED_CLASSES.get(module)
if allowed_names is not None:
if '*' in allowed_names or name in allowed_names:
return super().find_class(module, name)
# Allow safe user-defined classes (without __reduce__)
# This supports loading configuration classes like PreTrainingArguments
try:
cls = super().find_class(module, name)
if _is_safe_class(cls):
return cls
else:
raise pickle.UnpicklingError(
f"Forbidden class: {module}.{name}. "
f"Only user-defined classes without __reduce__ are allowed."
)
except pickle.UnpicklingError:
raise
except (ImportError, AttributeError):
pass
raise pickle.UnpicklingError(
f"Forbidden class: {module}.{name}. "
f"For security, only whitelisted classes are allowed during "
f"deserialization of model files. If you believe this class "
f"should be allowed, please report an issue at "
f"https://github.com/PaddlePaddle/Paddle/issues"
)
def safe_load_pickle(f, encoding='latin1'):
"""Safely load a pickle file using RestrictedUnpickler.
Args:
f: A file-like object (opened in binary mode) to read from.
encoding: The encoding to use for unpickling (default: 'latin1').
Returns:
The deserialized Python object.
Raises:
pickle.UnpicklingError: If the pickle data contains forbidden classes.
"""
return RestrictedUnpickler(f, encoding=encoding).load()
def safe_loads_pickle(data, encoding='latin1'):
"""Safely load pickle data from bytes using RestrictedUnpickler.
Args:
data: Bytes or bytearray containing pickled data.
encoding: The encoding to use for unpickling (default: 'latin1').
Returns:
The deserialized Python object.
Raises:
pickle.UnpicklingError: If the pickle data contains forbidden classes.
"""
import io
return RestrictedUnpickler(io.BytesIO(data), encoding=encoding).load()