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
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. |
noisgate verdict.
The decisive factor is that exploitation requires a *specific unsafe deployment pattern* rather than mere package presence: attacker-controlled deserialization plus Python-native mode plus strict=False plus reliance on DeserializationPolicy. That sharply narrows the reachable population versus a true internet-facing pre-auth service RCE, but any app that matches those conditions can still hand an attacker code execution inside the Python process.
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.
What to do — in priority order.
- Force
strict=Truewhere 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. - Block untrusted PyFory inputs at the edge — If a service accepts serialized blobs from clients, brokers, or partner systems, gate that path with authentication, allowlisted producers, and protocol-level rejection until the dependency is upgraded. Put those blocks in place within 30 days for any internet-facing or partner-exposed flow.
- Disable Python-native mode for hostile inputs — Where the workflow permits it, move externally influenced traffic away from
xlang=FalsePython-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. - Inventory imports and code paths — Use SBOM/SCA plus code search to separate simple package presence from *actual deserialization use on untrusted data*. Do this immediately so you do not waste time emergency-patching dormant developer tools while missing the one real RPC service.
- Constrain the runtime — Run vulnerable Python services with low-privilege identities, no shell utilities they do not need, and tight egress controls, so a successful deserialization bypass has less room to turn into full environment compromise. Apply these hardening changes within 30 days on any confirmed exposed workload.
- 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.
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)
If you remember one thing.
pyfory, then prioritize the ones that accept external or partner-controlled serialized data and run with strict=False; for those confirmed exposures, put compensating controls in place within 30 days under the noisgate mitigation SLA, and move them to pyfory 1.0.0+ within 180 days under the noisgate remediation SLA. If you discover an internet-facing service actually deserializing untrusted PyFory data, pull that case to the front of the queue immediately even though the overall CVE verdict stays HIGH.Sources
What defenders are saying.
Crowdsourced verification outputs.
Results submitted by users who ran the verification payload against their environment.