Skip to content

Python: fix(core): restrict unpickler module-prefix allowlist to types only#5923

Open
White-Mouse wants to merge 1 commit into
microsoft:mainfrom
White-Mouse:fix/restricted-unpickler-non-type-globals
Open

Python: fix(core): restrict unpickler module-prefix allowlist to types only#5923
White-Mouse wants to merge 1 commit into
microsoft:mainfrom
White-Mouse:fix/restricted-unpickler-non-type-globals

Conversation

@White-Mouse
Copy link
Copy Markdown

Summary

_RestrictedUnpickler.find_class currently allows any global from modules matching the agent_framework. or openai.types. prefixes. This can return non-type attributes (functions, sub-modules) which may be chained through builtins.getattr to bypass the restricted deserialization boundary.

Problem

The module-prefix check in find_class grants blanket access to all globals in framework and OpenAI SDK modules:

if (
    type_key in _BUILTIN_ALLOWED_TYPE_KEYS
    or type_key in self._allowed_types
    or module.startswith(_FRAMEWORK_MODULE_PREFIX)
    or module.startswith(_OPENAI_MODULE_PREFIX)
):
    return super().find_class(module, name)

Since builtins:getattr is in the built-in allowlist, a crafted checkpoint payload can chain through a non-type global from an allowed package prefix to reach pickle.loads, bypassing the restricted unpickler entirely.

Fix

Resolve the global first, then only return it if it is actually a type:

Non-type globals (functions, modules) are rejected with a clear error message.

Backwards Compatibility

All legitimate class/type references from agent_framework.* and openai.types.* continue to work. Only non-type globals are rejected, which were never valid deserialization targets.

🤖 Generated with Claude Code

The `_RestrictedUnpickler.find_class` method allows any attribute from
modules matching the `agent_framework.` and `openai.types.` prefixes.
This permits non-type globals such as functions and sub-modules, which
can be chained through `builtins.getattr` to reach `pickle.loads` and
execute unrestricted nested pickle payloads.

Limit module-prefix allowlisting to actual types by checking that the
resolved global is an instance of `type` before returning it.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings May 18, 2026 16:39
@github-actions github-actions Bot changed the title fix(core): restrict unpickler module-prefix allowlist to types only Python: fix(core): restrict unpickler module-prefix allowlist to types only May 18, 2026
@White-Mouse
Copy link
Copy Markdown
Author

@microsoft-github-policy-service agree

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Tightens the Python checkpoint restricted unpickling boundary by preventing agent_framework.* and openai.types.* module-prefix allowlisting from returning non-type globals (e.g., functions/modules), mitigating gadget chains that can pivot via builtins.getattr.

Changes:

  • Updates _RestrictedUnpickler.find_class to resolve globals under allowed prefixes and permit them only if they are actual type objects.
  • Adds a dedicated UnpicklingError message for blocked non-type globals from allowed prefixes.

Comment on lines 99 to 104
def find_class(self, module: str, name: str) -> type:
type_key = f"{module}:{name}"

if (
type_key in _BUILTIN_ALLOWED_TYPE_KEYS
or type_key in self._allowed_types
or module.startswith(_FRAMEWORK_MODULE_PREFIX)
or module.startswith(_OPENAI_MODULE_PREFIX)
):
if type_key in _BUILTIN_ALLOWED_TYPE_KEYS or type_key in self._allowed_types:
return super().find_class(module, name) # type: ignore[no-any-return] # nosec

Comment on lines +102 to 104
if type_key in _BUILTIN_ALLOWED_TYPE_KEYS or type_key in self._allowed_types:
return super().find_class(module, name) # type: ignore[no-any-return] # nosec

Comment on lines +105 to +118
if module.startswith(_FRAMEWORK_MODULE_PREFIX) or module.startswith(
_OPENAI_MODULE_PREFIX
):
resolved = super().find_class(module, name) # nosec
# Reject non-type globals from allowed package prefixes. A broad
# module-prefix allowlist returns arbitrary module attributes such
# as functions and sub-modules, which can be combined with
# builtins.getattr to call unrestricted pickle.loads.
if isinstance(resolved, type):
return resolved # type: ignore[return-value]
raise pickle.UnpicklingError(
f"Checkpoint deserialization blocked for non-type global "
f"'{type_key}'."
)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants