How to fix CVE-2026-48207: PyFory ReduceSerializer skips your DeserializationPolicy
| Severity | Critical, CVSS 3.1 base 9.8 (AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H) |
|---|---|
| Product | Apache Fory / PyFory (pyfory on PyPI) |
| Actively exploited? | No. SSVC Exploitation: none; not on CISA KEV |
| Affected versions | 0.13.0 up to (not including) 1.0.0 |
| Fixed in | pyfory 1.0.0 or later |
| Type (CWE) | CWE-502: Deserialization of Untrusted Data |
| Fix command | pip install -U "pyfory>=1.0.0" |
Exploitation status
CISA's ADP vulnrichment scores this one SSVC Exploitation: none, and CVE-2026-48207 is not on the CISA KEV catalog as of writing. There is no public proof-of-concept referenced in the record. So the urgency here comes from the score, not from active abuse: with a CVSS base of 9.8 and SSVC marking it Automatable: yes with Technical Impact: total, this is exactly the kind of flaw that gets weaponised quietly once someone writes the gadget chain.
One nuance specific to this bug: it only bites applications that opted into PyFory's policy model in the first place. If you built a DeserializationPolicy to whitelist safe classes and you trusted it to hold, that trust was misplaced for the ReduceSerializer paths. Teams that never deserialize untrusted input, or that run strict mode, were never really in the blast radius.
Authoritative reference:
What is CVE-2026-48207?
CVE-2026-48207 is a deserialization-of-untrusted-data flaw (CWE-502) in PyFory, the Python implementation of Apache Fory, the fast cross-language serialization framework. In affected versions, the ReduceSerializer can sidestep the DeserializationPolicy validation hooks you configured — the very hooks that are supposed to block unsafe classes, functions, and module attributes: during two specific operations: reduce-state restoration and global-name resolution. The result is that a payload you believed your policy would reject can still be reconstructed, which in a deserialization context is the road to arbitrary code execution.
The vendor's own words: "PyFory's ReduceSerializer could bypass documented DeserializationPolicy validation hooks during reduce-state restoration and global-name resolution." Note the precondition that makes this exploitable in practice: you are at risk only when you deserialize attacker-controlled data in Python-native mode with strict mode disabled, while leaning on a DeserializationPolicy as your safety net. That is the trap: the policy gives a false sense of containment that the ReduceSerializer quietly defeats.
Exploit preconditions (read the CVSS vector literally)
The CVSS 3.1 vector AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H (base 9.8) tells you exactly who is exposed. Walk it field by field:
- AV:N (network): the malicious bytes reach PyFory over the network in a typical deployment, for example an API endpoint or message queue that deserializes payloads.
- AC:L / PR:N / UI:N: no special conditions, no authentication, and no user interaction. An attacker who can submit a payload to the deserialization path is enough.
- C:H/I:H/A:H: full confidentiality, integrity, and availability loss, consistent with code execution in the process. SSVC backs this with Technical Impact: total.
The unstated precondition, which is the real gate, is application-side: your code must (a) deserialize untrusted input with PyFory, (b) use Python-native mode, (c) have strict mode off, and (d) rely on a DeserializationPolicy. Miss any one of those and the network vector never lands. That is why the honest risk picture for most installs is "critical if you fit the pattern, not applicable if you don't."
Am I affected? Detect your PyFory version
You are affected if pyfory is in the range 0.13.0 up to but not including 1.0.0. Anything 1.0.0 or newer already enforces the policy on the ReduceSerializer paths. Here is how to check fast, regardless of how the package got installed:
# The fastest version check (pip metadata)
pip show pyfory | grep -i version
# Or straight from the interpreter that actually runs your app
python -c "import pyfory, sys; print(pyfory.__version__)"
# In a Poetry / PDM / uv project, inspect the lockfile too
grep -A2 '"pyfory"' poetry.lock 2>/dev/null || grep -i pyfory uv.lock requirements*.txt 2>/dev/null
# Scan every virtualenv on a host (CI runners love to hide old copies)
find / -name "pyfory" -path "*/site-packages/*" -maxdepth 12 2>/dev/null
Do not stop at the top-level project. PyFory shows up as a transitive dependency of serialization-heavy stacks, so a clean requirements.txt can still pull a vulnerable copy through another package. The find sweep above catches the stray virtualenv in a CI image that nobody remembers building.
How to fix CVE-2026-48207
The fix is a single, real upgrade: move pyfory to 1.0.0 or later, which re-applies DeserializationPolicy validation to the ReduceSerializer paths. This is a PyPI package, so you fix it with pip (or Poetry / uv / PDM), not with an OS package manager and absolutely not with a Windows hotfix.
- Pin and upgrade to the patched release:
pip install -U "pyfory>=1.0.0". - Update your lockfile so the patched version is reproducible across environments (
poetry update pyfory,uv lock --upgrade-package pyfory, or regeneraterequirements.txt). - Rebuild any container images that bundle PyFory so the new wheel is baked into the layer, then roll the workload.
- Restart the Python processes that import PyFory, since a long-running worker keeps the old module loaded in memory until it restarts.
- Verify the running version (see the verification section), then re-deploy across the fleet.
Upgrade with pip / Poetry / uv
# pip (the common case)
pip install -U "pyfory>=1.0.0"
# Poetry
poetry add "pyfory@>=1.0.0"
poetry update pyfory
# uv
uv pip install -U "pyfory>=1.0.0"
# or, in a uv-managed project:
uv lock --upgrade-package pyfory && uv sync
# PDM
pdm update pyfory
# Container deployments: bump the pin in the image, then rebuild and roll.
# In your requirements file or Dockerfile RUN layer:
# pyfory>=1.0.0
# Then:
# docker build -t your-registry/your-app:patched .
# docker push your-registry/your-app:patched
# Kubernetes rollout:
# kubectl set image deployment/your-deploy app=your-registry/your-app:patched
# kubectl rollout status deployment/your-deploy
Confirm the fix actually applied
Installing the wheel is not the same as running it. Two checks prove the patch is live: the version probe, and a behavioural test that the policy now holds.
# 1. Version probe in the SAME interpreter your service uses
python -c "import pyfory; print('pyfory', pyfory.__version__)"
# Expect 1.0.0 or higher.
# 2. Restart the worker so the patched module is the one in memory
sudo systemctl restart your-python-service 2>/dev/null || true
# (or re-roll the k8s deployment / restart the gunicorn/uvicorn master)
# 3. Re-scan with your SCA tool of choice; it should clear CVE-2026-48207
pip-audit 2>/dev/null | grep -i pyfory || echo "pip-audit: no pyfory finding"
If you maintain a regression test for your DeserializationPolicy, this is the moment to run it: feed it a payload that your policy is supposed to reject through a reduce-state path, and confirm 1.0.0 raises instead of reconstructing. That behavioural check is worth more than any version string, because it tests the exact code path the CVE was about.
If you cannot patch immediately
If a version bump has to wait for a release train, you can shrink the exposure with controls that target this CVE's exact preconditions, ranked by how much they actually help:
- Stop deserializing untrusted input through PyFory Python-native mode. This removes the network vector entirely and is the strongest stopgap: route untrusted payloads through a safer codec or reject them.
- Enable strict mode. The bug is scoped to "strict mode disabled"; turning it on changes how reduce-state and global-name resolution are handled and takes you off the vulnerable path.
- Treat the DeserializationPolicy as non-authoritative. Do not rely on it to contain hostile classes while you are on an affected version: assume the ReduceSerializer can route around it.
- Constrain reachability. Keep the deserialization endpoint off the public internet and behind authenticated, internal-only access while you schedule the upgrade.
These buy time. None of them is a substitute for shipping pyfory>=1.0.0, where the validation is enforced in the right place.
Post-patch checklist
- Confirm
pyfory.__version__reports 1.0.0 or higher in the interpreter that runs your service, not just on the build host. - Restart every long-running worker, scheduler, and queue consumer that imports PyFory so the patched module is the one in memory.
- Regenerate and commit the lockfile so the pinned version propagates to teammates and CI.
- Run your dependency scanner (
pip-audit, Trivy, Grype, Snyk) and confirm CVE-2026-48207 no longer appears. - If you ran a behavioural DeserializationPolicy test, keep it in CI as a regression guard against future serializer additions.
Because SSVC marks this Exploitation: none and no public PoC exists, a full incident-response sweep is not warranted unless you have independent evidence of abuse. If you were on an affected version and you deserialized untrusted input in Python-native mode with strict mode off, then a log review of that endpoint is reasonable due diligence: look for malformed payloads, unexpected object types being reconstructed, and any process activity (new files, outbound connections) that lines up with those requests.
Frequently asked questions
What is the patched version of PyFory for CVE-2026-48207?
pyfory 1.0.0. Anything from 0.13.0 up to but not including 1.0.0 is affected. Upgrade with pip install -U "pyfory>=1.0.0".
Is this an Apache HTTP Server or Tomcat bug?
No. Despite "Apache" in the name, this is Apache Fory, a serialization framework, and specifically its Python package PyFory. There is no apt/dnf package and no web-server config involved. The fix is a Python dependency upgrade.
Am I vulnerable if I never deserialize untrusted data?
Effectively no. The flaw requires deserializing attacker-controlled data in Python-native mode with strict mode disabled while relying on a DeserializationPolicy. If none of that describes your usage, the ReduceSerializer bypass cannot be triggered: but upgrading is still the clean move.
Is CVE-2026-48207 on CISA KEV or being exploited?
No. CISA's vulnrichment records SSVC Exploitation: none, and it is not on the KEV catalog as of writing. The 9.8 base score still warrants patching on a critical-severity timeline.
Related fixes
Other flaws in this area worth reviewing while you patch this one:
- How to Fix CVE-2026-28563: Apache Airflow: DAG authorization bypass in Apache Airflow
- How to Fix CVE-2026-24880: Inconsistent interpretation of http requests in Apache Tomcat
- How to Fix CVE-2026-34481: Encoding or escaping of output in Apache Log4j JSON Template Layout
- How to Fix CVE-2026-40690: CWE-1220: Insufficient Granularity of Access Control in Apache Airflow
- How to Fix CVE-2026-5088: Use of cryptographically weak pseudo-random number flaw in Apache::API::Password
References
- Official Apache Fory advisory: fory.apache.org/security: CVE-2026-48207 PyFory ReduceSerializer DeserializationPolicy bypass
- oss-security disclosure thread: openwall.com/lists/oss-security/2026/05/21/10
- PyFory on PyPI: pypi.org/project/pyfory
- MITRE CVE record: cve.org/CVERecord?id=CVE-2026-48207
This guide was assembled from the official Apache Fory security advisory and the published MITRE CVE record (Apache assigner, CISA ADP vulnrichment), reviewed 2026-06-21. CVSS 3.1 base 9.8 and the CWE-502 classification are taken from those records. Confirm against the vendor advisory before changing production.
Why a DeserializationPolicy alone wasn't enough
The mental model worth fixing here is the one that bites teams: "I have a DeserializationPolicy that whitelists safe types, so untrusted input is contained." That model assumes the policy sits on every path that reconstructs an object. CVE-2026-48207 is the counterexample. The ReduceSerializer handles Python's reduce protocol: the same machinery that pickle uses via __reduce__: and during reduce-state restoration and global-name resolution it reached past the policy hooks. So a payload could name a callable or restore state the policy was meant to forbid.
This is why deserialization flaws are a whole CWE category (CWE-502) rather than a single bug class. Reduce-style protocols are powerful by design: they let an object describe how to rebuild itself, including which callable to invoke and which arguments to pass. Any validation layer has to intercept that description before it executes, on every code path, or it is not really a control. The 1.0.0 fix closes the specific gap by enforcing the policy on those ReduceSerializer paths too.
Practical takeaway for your own code: a policy is a defence-in-depth layer, not a license to deserialize hostile bytes. The most robust posture is to not deserialize untrusted input in a reduce-capable, native mode at all, and to keep strict mode on. Treat the policy as the second line, not the first.
Finding every PyFory usage that matters
Patching the version is step one. Step two is knowing whether you even hit the vulnerable preconditions, because that decides how hard you push the rollout. Grep your codebase for the patterns that put you in scope:
# Where do we import and call PyFory?
grep -rn "import pyfory\|from pyfory" --include="*.py" .
# Where do we deserialize? Look for loads/deserialize on attacker-reachable input
grep -rn "\.loads(\|\.deserialize(\|fory\.\|Fory(" --include="*.py" .
# Are we using a DeserializationPolicy at all, and is strict mode on?
grep -rni "DeserializationPolicy\|strict" --include="*.py" .
The combination you are hunting for: a PyFory deserialize call whose input crosses a trust boundary (an HTTP body, a message off a broker, a cache entry an attacker can poison), in Python-native mode, with strict mode off, guarded only by a DeserializationPolicy. If you find that, treat the upgrade as urgent on those services. If every PyFory call only ever handles data your own trusted process produced, you are far lower risk, though you should still upgrade to stay current.
A note on cross-language deployments
Apache Fory is a cross-language framework: there are Java, Python, and other implementations that can interoperate. This specific CVE is scoped to PyFory (the pyfory PyPI package) and its ReduceSerializer, which is a Python-language construct. If you run Fory across services in multiple languages, audit which side performs the untrusted deserialization. The Python service that calls ReduceSerializer on attacker-influenced bytes is the one carrying this risk; a Java producer feeding a Python consumer means the Python consumer needs pyfory>=1.0.0. Pin the fix where the deserialization actually runs.
Rolling it out without surprises
PyFory 1.0.0 is a major version, so a quick read of the changelog before a blanket bump is sensible, since a 0.x to 1.0 jump can carry API changes alongside the security fix. Stage it: upgrade in a branch, run your serialization round-trip tests, then promote. The one regression class to watch for is a payload your application legitimately relied on now being rejected because the policy is finally enforced where it was being skipped. That is the fix working as intended, but it can surface as a "why did this stop deserializing" ticket. Treat such cases as a prompt to review whether that payload should have been trusted in the first place.
For a fleet, drive the version through your lockfile and CI rather than ad-hoc pip install on hosts. A pinned pyfory>=1.0.0 in the lockfile, plus a pip-audit gate in the pipeline, turns "did everyone patch?" from a manual chase into a build that fails until they did.