← Back to Feed CACHED · 2026-05-17 09:42:19 · cache_key CVE-2025-29912
CVE-2026-48207 · CWE-502 · Disclosed 2026-05-21

Deserialization of untrusted data in Apache Fory PyFory

ASSESSED — NOISGATE V0.5
Vendor
Reassessed
Verdict:
01 · The Real Story

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.

"Dangerous if you actually deserialize attacker data with strict=False, but that is a much smaller population than a 9.8 suggests."
02 · The Attack Path

4 steps from start to impact.

STEP 01

Find a PyFory trust boundary

The attacker first needs an application endpoint, worker queue, RPC channel, cache feed, or file-import path that hands attacker-influenced bytes into 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.
Conditions required:
  • Unauthenticated remote access to an input path *or* control of an upstream producer
  • The application uses pyfory for inbound deserialization
  • Attacker can deliver serialized bytes without pre-validation
Where this breaks in practice:
  • pyfory is 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 pyfory sees them
Detection/coverage: External scanners have poor coverage because they cannot fingerprint a library-only condition; this is mainly SBOM/SCA plus code-path discovery.
STEP 02

Require the unsafe mode combination

The exploit path depends on Python-native mode and permissive settings: 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.
Conditions required:
  • Target code runs Python-native serialization mode
  • strict=False is configured
  • The application relies on policy hooks instead of strict registration
Where this breaks in practice:
  • 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
Detection/coverage: SAST and repo grep can usually find Fory(... strict=False ...); runtime detection is harder unless you instrument app configs or code.
STEP 03

Bypass policy enforcement in ReduceSerializer

A crafted payload abuses the 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.
Conditions required:
  • Payload reaches vulnerable deserialization logic unchanged
  • Application policy is the only barrier against unsafe object reconstruction
Where this breaks in practice:
  • 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
Detection/coverage: Network IDS coverage is weak unless you already decode the application protocol. App logs may only show deserialization exceptions or unusual module/class resolution.
STEP 04

Land code execution or unsafe object behavior

If the vulnerable application trusts the reconstructed object graph, the attacker can reach code execution or equivalent unsafe object instantiation inside the Python process. Weaponized tool: the final-stage object/callable payload tailored to the target app. Reference: Openwall disclosure.
Conditions required:
  • Exploit payload successfully reconstructs a dangerous callable/object
  • The process context has useful privileges or network reach
Where this breaks in practice:
  • 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
Detection/coverage: EDR can often see the *consequence* phase such as process spawn, shelling out, suspicious imports, or outbound connections, but not the root deserialization flaw.
03 · Intelligence Metadata

The supporting signals.

In-the-wild statusNo authoritative evidence of active exploitation found in this review. CISA KEV: not listed as of 2026-05-31.
Proof-of-concept availabilityNo 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.
EPSSUser-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 statusNo. No Known Exploited Vulnerabilities catalog entry found for CVE-2026-48207.
CVSS vector reality checkCVSS: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 versionsAuthoritative 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 versionUpgrade 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 dataNo 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 timelineDisclosed 2026-05-21 via Apache and mirrored to Openwall the same day.
ReporterCredited reporter: Lide Wen.
04 · The Call

noisgate verdict.

Final Verdict
DOWNGRADED to HIGH (7.3/10)

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.

HIGH Affected version range and fixed version
MEDIUM Exploitability assessment for real enterprise deployments
LOW Public PoC status and internet-scale exposure telemetry

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=False and dependence on DeserializationPolicy for 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.

05 · Compensating Control

What to do — in priority order.

  1. Force 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.
  2. 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.
  3. Disable Python-native mode for hostile inputs — Where the workflow permits it, move externally influenced traffic away from 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.
  4. 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.
  5. 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.
What doesn't work
  • 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 DeserializationPolicy alone is exactly the assumption this CVE breaks on the affected ReduceSerializer paths.
  • EDR is useful for catching post-exploitation behavior, but it is not a preventive control for the vulnerable deserialization decision itself.
06 · Verification

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.

noisgate-verify.py
PYTHONREAD-ONLYSAFE
#!/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)
07 · Bottom Line

If you remember one thing.

TL;DR
Monday morning, do not treat this like a fleet-wide 9.8 fire drill; treat it like a targeted hunt for unsafe deserialization paths. By the end of the week, inventory every app or image that imports 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

  1. Apache Fory security advisory
  2. NVD CVE entry
  3. Openwall oss-security disclosure
  4. CVE.org record
  5. PyPI pyfory package page
  6. Apache Fory GitHub releases
  7. CISA Known Exploited Vulnerabilities Catalog
  8. FIRST EPSS documentation
Peer Review

What defenders are saying.

Submit a review attribution: handle + country only
0 flags selected · stored anonymously
Validation Results

Crowdsourced verification outputs.

Results submitted by users who ran the verification payload against their environment.