This is not a skeleton key for every Python host, it is a trapdoor that only opens when your app already accepts hostile PyFory blobs
CVE-2026-48207 is a deserialization-policy bypass in Apache Fory's Python package pyfory. The vulnerable path is ReduceSerializer during reduce-state restoration and global-name resolution, where documented DeserializationPolicy hooks are not consistently enforced. The vendor advisory says affected versions are pyfory 0.13.0 through 0.17.0, with the fix in 1.0.0; NVD models the range as 0.13.0 up to but excluding 1.0.0.
The raw 9.8 score overstates enterprise urgency because exploitation is not just "network reachable" in the real world. The attacker needs a target application that both accepts attacker-controlled serialized bytes and uses Python-native mode with xlang=False, has strict=False, and relies on DeserializationPolicy as the safety boundary. That is a real RCE-style failure mode when present, but it is a *conditional library exposure*, not a universally exposed service bug.
4 steps from start to impact.
Find a PyFory trust boundary
pyfory.deserialize() or loads(). Weaponized tool: a bespoke PyFory payload generator built against the published protocol and vendor docs, not a broadly used mass-exploitation kit. Reference: Apache advisory.- Unauthenticated remote access to an input path *or* control of an upstream producer
- The application uses
pyforyfor inbound deserialization - Attacker can deliver serialized bytes without pre-validation
pyforyis a library, not an internet-bannered daemon, so exposure discovery is application-specific- Many enterprise deployments never deserialize untrusted external data with this library
- Message schemas, auth layers, or protocol gateways may reject attacker-crafted payloads before
pyforysees them
Require the unsafe mode combination
xlang=False with strict=False, while the application expects DeserializationPolicy to block dangerous classes, functions, or module attributes. Weaponized tool: custom payload plus app-specific protocol wrapper. Reference: PyPI docs and Apache security page.- Target code runs Python-native serialization mode
strict=Falseis configured- The application relies on policy hooks instead of strict registration
- Teams following the package's production-hardening guidance may already run
strict=True - Some apps use cross-language mode or trusted-only local workflows, which are outside the vulnerable scenario
- Many deserialization surfaces are internal-only, implying the attacker is already post-initial-access
Fory(... strict=False ...); runtime detection is harder unless you instrument app configs or code.Bypass policy enforcement in ReduceSerializer
ReduceSerializer restore path so policy validation does not fire where the defender expected it to. In practical terms, the attacker uses a reduce/global-name path that slips past custom policy checks and reconstructs unsafe objects or callables. Weaponized tool: a proof payload implementing the vulnerable reduce-state/global-name sequence described by the advisory. Reference: NVD description.- Payload reaches vulnerable deserialization logic unchanged
- Application policy is the only barrier against unsafe object reconstruction
- Custom wrappers may add their own allowlists before deserialization
- Serialization format mismatches can break exploit reliability
- Some deployments isolate the worker process and reduce blast radius even if deserialization succeeds
Land code execution or unsafe object behavior
- Exploit payload successfully reconstructs a dangerous callable/object
- The process context has useful privileges or network reach
- Containerization, seccomp, low-privilege service accounts, and egress controls can limit impact after compromise
- EDR may catch the follow-on behavior even if it cannot prevent the deserialization bug itself
strict=True where possible — Make registration-based deserialization the default safety control instead of trusting policy hooks on vulnerable paths. For confirmed affected apps, deploy this change within 30 days because that is the compensating-control deadline for a HIGH finding.xlang=False Python-native object reconstruction and into safer typed formats or cross-language modes with tighter schemas. Treat this as a containment change to complete within 30 days on exposed applications.- A generic WAF does not reliably solve this because the dangerous payload is application-specific serialized data, not a clean HTTP signature problem.
- TLS or mTLS in transit does not help if the remote peer itself is attacker-controlled or already compromised; it protects the channel, not the object graph.
- Relying on
DeserializationPolicyalone is exactly the assumption this CVE breaks on the affectedReduceSerializerpaths. - EDR is useful for catching post-exploitation behavior, but it is not a preventive control for the vulnerable deserialization decision itself.
The supporting signals.
| In-the-wild status | No authoritative evidence of active exploitation found in this review. CISA KEV: not listed as of 2026-05-31. |
|---|---|
| Proof-of-concept availability | No solid public exploit repo was confirmed from primary sources. One aggregator page claims a GitHub-linked PoC, but the referenced repo appears to be an intel bot entry rather than validated weaponized exploit code; treat public PoC status as unconfirmed. |
| EPSS | User-provided EPSS is 0.0014. A live third-party view from Tenable showed 0.00041 on 2026-05-31, which still points the same direction: very low short-term exploitation probability. |
| KEV status | No. No Known Exploited Vulnerabilities catalog entry found for CVE-2026-48207. |
| CVSS vector reality check | CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H assumes a clean pre-auth network path to the vulnerable function. In practice, the exploit chain usually also requires a reachable application deserialization surface plus unsafe pyfory configuration, which CVSS does not model well. |
| Affected versions | Authoritative vendor range: pyfory 0.13.0 through 0.17.0. NVD normalizes this as 0.13.0 up to but excluding 1.0.0. |
| Fixed version | Upgrade to pyfory 1.0.0 or later. I did not find distro-specific backport advisories during this review; assume Python package upgrade, vendor bundle update, or application dependency bump is required. |
| Exposure/scanning data | No direct GreyNoise, Shodan, or Censys exposure signal was located for this CVE. That is expected: this is a library flaw, not a bannered internet service, so exposure must be measured through SBOMs, import inventory, and code-path discovery rather than internet scans. |
| Disclosure timeline | Disclosed 2026-05-21 via Apache and mirrored to Openwall the same day. |
| Reporter | Credited reporter: Lide Wen. |
Sources.
Why this verdict
- Downgrade from 9.8 for attacker position reality: the attacker does not win by finding a host with
pyfory; they need a live application path that deserializes attacker-controlled bytes. That is a major narrowing from CVSS's abstract network assumption. - Further downgrade for configuration dependence: the vulnerable scenario requires Python-native mode with
strict=Falseand dependence onDeserializationPolicyfor safety. Each prerequisite compounds downward pressure because many installs will not line up that way. - Hold at HIGH because impact is still serious when the chain exists: if your app does expose that deserialization boundary, exploitation can cross directly into unsafe object reconstruction and probable RCE-like outcomes inside the service account context.
Why not higher?
I would reserve CRITICAL for bugs with a large, easy-to-reach exposed population or active exploitation evidence. Here, the exploit chain is gated by application design and unsafe configuration choices, and there is no KEV listing or strong real-world exploitation signal yet.
Why not lower?
This is not a harmless theoretical bug. Once an exposed app actually feeds hostile bytes into vulnerable pyfory with the unsafe mode combination, the path to process compromise is short and the security boundary being bypassed is the very control defenders may have been relying on.
Crowdsourced verification payload.
Run this on the target host that has the Python environment or container image you want to assess. Invoke it as python3 check_cve_2026_48207.py /path/to/app to check the installed pyfory version and optionally scan source files for risky Fory(... strict=False ...) usage; no admin rights are needed unless the code path you want to scan is unreadable to your current account.
#!/usr/bin/env python3
# CVE-2026-48207 verifier for Apache Fory PyFory
# Exit codes: 0=PATCHED, 1=VULNERABLE, 2=UNKNOWN
import os
import re
import sys
from pathlib import Path
try:
from importlib.metadata import version, PackageNotFoundError
except Exception:
print('UNKNOWN - importlib.metadata unavailable')
sys.exit(2)
TARGET = Path(sys.argv[1]).resolve() if len(sys.argv) > 1 else None
PKG = 'pyfory'
def parse_ver(v):
m = re.match(r'^(\d+)\.(\d+)\.(\d+)', str(v))
if not m:
return None
return tuple(int(x) for x in m.groups())
def lt(a, b):
return a < b
def scan_repo(root):
findings = []
if root is None or not root.exists():
return findings
exts = {'.py', '.pyw'}
patterns = [
re.compile(r'Fory\s*\([^\)]*strict\s*=\s*False', re.S),
re.compile(r'Fory\s*\([^\)]*xlang\s*=\s*False', re.S),
re.compile(r'DeserializationPolicy'),
re.compile(r'pyfory\.(loads|deserialize)\s*\('),
re.compile(r'\b(loads|deserialize)\s*\('),
]
for p in root.rglob('*'):
if not p.is_file() or p.suffix.lower() not in exts:
continue
try:
text = p.read_text(encoding='utf-8', errors='ignore')
except Exception:
continue
hits = []
for pat in patterns:
if pat.search(text):
hits.append(pat.pattern)
if hits:
findings.append((str(p), hits))
return findings
try:
installed = version(PKG)
except PackageNotFoundError:
print('UNKNOWN - pyfory not installed in this Python environment')
sys.exit(2)
except Exception as e:
print(f'UNKNOWN - failed to query package version: {e}')
sys.exit(2)
installed_t = parse_ver(installed)
fixed_t = (1, 0, 0)
if installed_t is None:
print(f'UNKNOWN - could not parse pyfory version: {installed}')
sys.exit(2)
repo_findings = scan_repo(TARGET)
strict_false = False
python_native = False
policy_used = False
for _, hits in repo_findings:
for h in hits:
if 'strict\s*=\s*False' in h:
strict_false = True
if 'xlang\s*=\s*False' in h:
python_native = True
if 'DeserializationPolicy' in h:
policy_used = True
if lt(installed_t, fixed_t):
if strict_false and (python_native or policy_used):
print(f'VULNERABLE - pyfory {installed} < 1.0.0 and code scan found risky usage (strict=False with Python-native/policy indicators)')
if TARGET:
for path, hits in repo_findings[:20]:
print(f' hit: {path} :: {", ".join(hits)}')
sys.exit(1)
else:
print(f'UNKNOWN - pyfory {installed} is in the affected version range, but risky runtime usage was not confirmed from static scan alone')
if TARGET and repo_findings:
for path, hits in repo_findings[:20]:
print(f' context: {path} :: {", ".join(hits)}')
sys.exit(2)
else:
print(f'PATCHED - pyfory {installed} >= 1.0.0')
sys.exit(0)