3454a55636
cffconvert / validate (push) Has been cancelled
ci-workflow / pre-commit (push) Has been cancelled
ci-workflow / Minimal NLTK Download Test (macos-latest) (push) Has been cancelled
ci-workflow / Minimal NLTK Download Test (ubuntu-latest) (push) Has been cancelled
ci-workflow / Minimal NLTK Download Test (windows-latest) (push) Has been cancelled
ci-workflow / Python 3.10 on macos-latest (push) Has been cancelled
ci-workflow / Python 3.11 on macos-latest (push) Has been cancelled
ci-workflow / Python 3.12 on macos-latest (push) Has been cancelled
ci-workflow / Python 3.13 on macos-latest (push) Has been cancelled
ci-workflow / Python 3.14 on macos-latest (push) Has been cancelled
ci-workflow / Python 3.14t on macos-latest (push) Has been cancelled
ci-workflow / Python 3.10 on ubuntu-latest (push) Has been cancelled
ci-workflow / Python 3.11 on ubuntu-latest (push) Has been cancelled
ci-workflow / Python 3.12 on ubuntu-latest (push) Has been cancelled
ci-workflow / Python 3.13 on ubuntu-latest (push) Has been cancelled
ci-workflow / Python 3.14 on ubuntu-latest (push) Has been cancelled
ci-workflow / Python 3.14t on ubuntu-latest (push) Has been cancelled
ci-workflow / Python 3.10 on windows-latest (push) Has been cancelled
ci-workflow / Python 3.11 on windows-latest (push) Has been cancelled
ci-workflow / Python 3.12 on windows-latest (push) Has been cancelled
ci-workflow / Python 3.13 on windows-latest (push) Has been cancelled
ci-workflow / Python 3.14 on windows-latest (push) Has been cancelled
144 lines
5.0 KiB
Python
144 lines
5.0 KiB
Python
# Natural Language Toolkit: Safer pickle loading.
|
|
#
|
|
# Copyright (C) 2001-2026 NLTK Project
|
|
# Author: Eric Kafe <kafe.eric@gmail.com>
|
|
# URL: <https://www.nltk.org/>
|
|
# For license information, see LICENSE.TXT
|
|
#
|
|
|
|
"""
|
|
Helpers for safer and/or more explicit pickle usage in NLTK.
|
|
|
|
- RestrictedUnpickler: blocks unpickling of *any* globals (classes/functions).
|
|
Intended for loading NLTK data packages where we control the serialization.
|
|
- WarningUnpickler: emits a security warning before unpickling (does not make
|
|
unpickling safe).
|
|
- AllowlistUnpickler: only reconstructs an explicit, audited allowlist of
|
|
globals. Use it for loading objects whose set of legitimate classes is known
|
|
(e.g. a trained model) so that arbitrary-code gadgets such as ``os.system``
|
|
can never be reconstructed from an untrusted file.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pickle
|
|
import warnings
|
|
from collections.abc import Iterable
|
|
from typing import Any, BinaryIO
|
|
|
|
PICKLE_WARNING = (
|
|
"Security warning: loading pickles can execute arbitrary code. "
|
|
"Only load pickle files from trusted sources and never from untrusted "
|
|
"or unauthenticated locations."
|
|
)
|
|
|
|
|
|
class RestrictedUnpickler(pickle.Unpickler):
|
|
"""
|
|
Unpickler that prevents any class or function from being used during loading.
|
|
"""
|
|
|
|
def find_class(self, module: str, name: str) -> Any:
|
|
# Forbid every function/class global.
|
|
raise pickle.UnpicklingError(f"global '{module}.{name}' is forbidden")
|
|
|
|
|
|
class WarningUnpickler(pickle.Unpickler):
|
|
"""Unpickler that emits PICKLE_WARNING once per instance."""
|
|
|
|
def __init__(self, file: BinaryIO, *, context: str | None = None, **kwargs: Any):
|
|
super().__init__(file, **kwargs)
|
|
self._context = context
|
|
self._warned = False
|
|
|
|
def load(self) -> Any:
|
|
if not self._warned:
|
|
msg = (
|
|
PICKLE_WARNING
|
|
if self._context is None
|
|
else f"{PICKLE_WARNING} ({self._context})"
|
|
)
|
|
warnings.warn(msg, RuntimeWarning, stacklevel=3)
|
|
self._warned = True
|
|
return super().load()
|
|
|
|
|
|
def pickle_load(
|
|
file: BinaryIO, *, context: str | None = None, restricted: bool = False
|
|
) -> Any:
|
|
"""
|
|
Convenience wrapper similar to pickle.load(file).
|
|
|
|
- If restricted=True, uses RestrictedUnpickler (no warning by default).
|
|
- If restricted=False, uses WarningUnpickler and emits a warning.
|
|
"""
|
|
if restricted:
|
|
return RestrictedUnpickler(file).load()
|
|
return WarningUnpickler(file, context=context).load()
|
|
|
|
|
|
class AllowlistUnpickler(pickle.Unpickler):
|
|
"""
|
|
Unpickler that only reconstructs an explicit, audited allowlist of globals.
|
|
|
|
Two complementary allowlists are supported:
|
|
|
|
- ``allowed_globals``: a set of exact ``(module, qualname)`` pairs.
|
|
- ``allowed_modules``: a set of module names; a global is permitted when its
|
|
module equals an allowed name or is a submodule of one (i.e. ``module``
|
|
equals ``name`` or starts with ``name + "."``).
|
|
|
|
Anything not covered by either allowlist raises ``UnpicklingError`` before
|
|
the global is resolved, so dangerous callables such as ``os.system`` or
|
|
``builtins.eval`` can never be reconstructed from an untrusted pickle. This
|
|
is stricter than :func:`pickle_load` (which only warns and then executes)
|
|
but, unlike :class:`RestrictedUnpickler` (which blocks *all* globals), it
|
|
still allows the known-good classes a saved object legitimately needs.
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
file: BinaryIO,
|
|
*,
|
|
allowed_globals: Iterable[tuple[str, str]] = (),
|
|
allowed_modules: Iterable[str] = (),
|
|
**kwargs: Any,
|
|
):
|
|
super().__init__(file, **kwargs)
|
|
if isinstance(allowed_modules, str):
|
|
allowed_modules = (allowed_modules,)
|
|
self._allowed_globals = set(allowed_globals)
|
|
self._allowed_modules = tuple(allowed_modules)
|
|
|
|
def _module_allowed(self, module: str) -> bool:
|
|
return any(
|
|
module == name or module.startswith(name + ".")
|
|
for name in self._allowed_modules
|
|
)
|
|
|
|
def find_class(self, module: str, name: str) -> Any:
|
|
if (module, name) in self._allowed_globals or self._module_allowed(module):
|
|
return super().find_class(module, name)
|
|
raise pickle.UnpicklingError(
|
|
f"global '{module}.{name}' is not in the pickle allowlist"
|
|
)
|
|
|
|
|
|
def allowlisted_pickle_load(
|
|
file: BinaryIO,
|
|
*,
|
|
allowed_globals: Iterable[tuple[str, str]] = (),
|
|
allowed_modules: Iterable[str] = (),
|
|
) -> Any:
|
|
"""
|
|
Load a pickle while only permitting an explicit allowlist of globals.
|
|
|
|
See :class:`AllowlistUnpickler` for the meaning of ``allowed_globals`` and
|
|
``allowed_modules``. Prefer this over the warn-only :func:`pickle_load` when
|
|
the set of legitimate classes in the file is known (e.g. a trained model),
|
|
because :func:`pickle_load` still executes arbitrary code.
|
|
"""
|
|
return AllowlistUnpickler(
|
|
file, allowed_globals=allowed_globals, allowed_modules=allowed_modules
|
|
).load()
|