← Back to Feed CACHED · 2026-05-17 09:42:19 · cache_key CVE-2025-29912
CVE-2026-49858 · CWE-524 · Disclosed 2026-06-04

Cross-user attribute leak in JSON:API and HAL item normalizers due to missing isCacheKeySafe gate

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

Like labeling a shared locker by room number instead of by person

CVE-2026-49858 is a cross-request cache-key bug in API Platform's JSON:API and HAL item normalizers. Affected ranges are api-platform/core >=2.6.0,<4.1.29 || >=4.2.0,<4.2.25 || >=4.3.0,<4.3.8, plus the split packages api-platform/json-api and api-platform/hal in their corresponding vulnerable ranges. If your app uses #[ApiProperty(security: ...)] rules that vary by requester, the normalizer can reuse a component layout calculated for a more privileged user and expose attribute/relationship structure to a less privileged or even anonymous requester.

The vendor's MEDIUM 5.9 is defensible in theory but too generous for fleet prioritization. Real exploitation needs four things to line up at once: JSON:API or HAL output, user-dependent property security, a request ordering race where a privileged request warms the cache first, and a long-lived PHP worker model that preserves in-memory state across requests. That is not the default Symfony/PHP-FPM reality in most enterprises, so this should be treated as a narrow information-disclosure bug, not a front-of-queue internet fire.

"This is a real cross-user leak, but only in a narrow, non-default deployment pattern."
02 · The Attack Path

4 steps from start to impact.

STEP 01

Find a JSON:API or HAL endpoint with per-user property rules

Using curl or Burp Suite Repeater, the attacker identifies resources served through JSON:API or HAL rather than JSON-LD or plain JSON. They also need the target app to use #[ApiProperty(security: ...)] on individual fields, with the decision depending on the current user or request context.
Conditions required:
  • Target application uses API Platform
  • Target exposes JSON:API and/or HAL responses
  • At least one property uses user-dependent ApiProperty(security=...)
Where this breaks in practice:
  • Many API Platform deployments use other formats and never enable JSON:API or HAL
  • Many apps use coarse resource-level auth, not per-property predicates
  • Field-level security rules are implementation-specific and not remotely obvious
Detection/coverage: SCA catches vulnerable package versions well; runtime scanners generally cannot confirm the required serializer configuration.
STEP 02

Get a privileged response to warm componentsCache

The buggy normalizer stores a component structure keyed too loosely on cache_key, without a sufficient user-context safety gate. A prior request from an admin or otherwise authorized user causes the worker to cache a richer field/relationship layout.
Conditions required:
  • A higher-privilege request reaches the same worker first
  • The worker process keeps the normalizer instance alive across requests
Where this breaks in practice:
  • This is request-order dependent
  • Classic per-request behavior makes the cache transient and hard to reuse
  • Load balancing can send later attacker traffic to a different process
Detection/coverage: No dependable network signature. You need app-aware testing or code review to prove warm-cache reuse.
STEP 03

Hit the same worker as a lower-privilege or anonymous user

With curl, vegeta, or Burp Intruder, the attacker issues the same resource request after the privileged warm-up. If the same long-running worker handles both requests, the lower-privilege caller may inherit the previously cached component map and see properties that should have been hidden.
Conditions required:
  • Second request lands on the same live worker
  • Anonymous or low-privilege access to the endpoint exists
Where this breaks in practice:
  • Worker affinity is not guaranteed
  • The leak is about response structure, not arbitrary server-side state
  • Modern gateways may make deterministic same-worker targeting difficult
Detection/coverage: Behavioral testing can reproduce it; commodity DAST usually misses it because it depends on multi-user sequencing against one worker.
STEP 04

Use leaked attribute names and links for follow-on discovery

The practical impact is unauthorized visibility into which fields, relationships, or links exist for the resource under a different privilege context. That can reveal sensitive schema details and sometimes business-sensitive object structure, but it does not by itself grant value-level reads, writes, or code execution.
Conditions required:
  • Leaked field names or relationships are themselves sensitive
  • Attacker can interpret the app's data model
Where this breaks in practice:
  • Many apps expose field names that are not materially sensitive
  • No direct integrity or availability impact
  • Blast radius is limited to affected resources and serializer formats
Detection/coverage: App-layer regression tests comparing admin vs anonymous field sets are the best validation method.
03 · Intelligence Metadata

The supporting signals.

In-the-wild statusNo confirmed active exploitation found during this assessment. The advisory names no campaigns, and CISA KEV does not surface this CVE.
Public PoCNo public exploit repo located. Reproduction appears straightforward from the advisory, but I did not find a weaponized public PoC tied to CVE-2026-49858 or GHSA-pjhx-3c3w-9v23.
EPSSUnavailable / unconfirmed from accessible FIRST data during this assessment. FIRST documents the endpoint, but I could not verify a live EPSS record for this specific CVE from the available interface.
KEV statusNot KEV-listed. That removes the strongest urgency amplifier for enterprise patch queues.
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:N — the important part is AC:H. The bug is network-reachable in theory, but exploitation depends on multi-request timing, worker reuse, and specific app security design.
Affected versionsapi-platform/core >=2.6.0,<4.1.29 || >=4.2.0,<4.2.25 || >=4.3.0,<4.3.8; api-platform/json-api >=4.0.0,<4.1.29 || >=4.2.0,<4.2.25 || >=4.3.0,<4.3.8; api-platform/hal >=4.0.0,<4.1.29 || >=4.2.0,<4.2.25 || >=4.3.0,<4.3.8
Fixed versionsUpgrade to 4.1.29, 4.2.25, or 4.3.8 on the branch you run. The advisory states all three branches received fixes in core, json-api, and hal.
Runtime preconditionsThis is the big downgrade factor: the issue needs a long-running PHP runtime that preserves in-memory state across requests, such as FrankenPHP worker mode, RoadRunner, Swoole, or similar. The advisory says classic php-fpm makes it much harder to observe in practice.
Internet exposure measurabilityPoorly fingerprintable from outside. Shodan/Censys-style counts are not meaningful here because the bug depends on serializer format choice, code-level property security predicates, and runtime worker behavior rather than a simple banner or version string.
Disclosure / creditsPublished 2026-06-04 in GHSA-pjhx-3c3w-9v23. Credits in the advisory go to Tillmann Baumgart for identifying the broader cache-key gap and Antoine Bluchet (@soyuka) for extending the fix to JSON:API and HAL.
04 · The Call

noisgate verdict.

Final Verdict
DOWNGRADED to LOW (3.8/10)

The decisive factor is the deployment prerequisite: this bug needs a long-lived PHP worker model plus a specific field-level authorization pattern before it can leak anything across users. That sharply limits the reachable population compared with the vendor's abstract network-facing CVSS baseline.

HIGH package version and fix ranges
HIGH required attack preconditions from the vendor advisory
MEDIUM fleet-wide severity downgrade based on likely enterprise exposure

Why this verdict

  • Downgrade 1 — requires non-default runtime behavior: classic php-fpm is explicitly called out as much harder to exploit in practice, so a huge chunk of ordinary PHP estates fall out immediately.
  • Downgrade 2 — requires app-specific field-level security: the vulnerable path matters only when #[ApiProperty(security: ...)] decisions vary by requester. If your team never built per-property auth rules, there is no practical attack.
  • Downgrade 3 — narrow blast radius: the leak is about response structure in JSON:API/HAL, not arbitrary object values, code execution, integrity loss, or service interruption.
  • Downgrade 4 — attacker position is weaker than it looks on paper: PR:N is technically true because an anonymous user can be the recipient, but the exploit usually *implies prior privileged traffic on the same worker*. That is a hidden dependency, and it matters.

Why not higher?

There is no KEV listing, no active exploitation signal, and no public weaponized PoC located. More importantly, the exploit chain compounds friction at every stage: specific serializer formats, specific auth annotations, privileged cache priming, and same-worker reuse in a persistent runtime.

Why not lower?

It is still a genuine cross-user authorization leak, not a theoretical code smell. In deployments using persistent PHP workers and user-dependent property security, an unauthenticated or low-privilege caller can receive data structure they were not meant to see, and that deserves remediation rather than documentation-only treatment.

05 · Compensating Control

What to do — in priority order.

  1. Disable JSON:API and HAL where unused — Remove the affected serializer paths from applications that do not require them. This is the cleanest exposure reduction and should be deployed within backlog hygiene cadence for a LOW issue, ahead of the actual library update if change control is slower.
  2. Pin affected services to classic php-fpm request isolation — If you currently run FrankenPHP worker mode, RoadRunner, Swoole, or similar, reverting the impacted API services to classic request-isolated php-fpm breaks the cache persistence assumption behind this bug. Use this as a temporary control where upgrade lead time is long.
  3. Audit ApiProperty(security=...) usage on JSON:API/HAL resources — Inventory resources that apply per-property security depending on the requesting user and are serialized through JSON:API or HAL. Those endpoints are your high-value subset; review them first and keep the audit inside the normal LOW-severity remediation cycle.
  4. Override the normalizers if you cannot upgrade quickly — The advisory explicitly suggests overriding the JSON:API and HAL ItemNormalizer services to gate cache_key with a resource-class security check. Use this only when package upgrades are blocked and document the local patch.
What doesn't work
  • A WAF does not help; there is no malicious payload to pattern-match, just normal API traffic hitting a buggy cache path.
  • MFA does not help; the unintended recipient can be anonymous, and the vulnerable condition is cache reuse, not account takeover.
  • EDR on the PHP host does not help; this is application-layer authorization leakage, not malware or process abuse.
  • Blindly restarting workers on a schedule is weak medicine; it may reduce persistence windows but does not remove the vulnerable logic.
06 · Verification

Crowdsourced verification payload.

Run this on the application host, build workspace, or CI runner that has the target app's composer.lock. Invoke it as python3 check_cve_2026_49858.py /var/www/app/composer.lock. It needs only read access to the lockfile; no root privileges are required.

noisgate-verify.py
PYTHONREAD-ONLYSAFE
#!/usr/bin/env python3
# Check exposure to CVE-2026-49858 in API Platform packages.
# Usage: python3 check_cve_2026_49858.py /path/to/composer.lock
# Exit codes: 0=PATCHED, 1=VULNERABLE, 2=UNKNOWN

import json
import os
import re
import sys
from typing import Optional, Tuple

AFFECTED = {
    'api-platform/core': [
        ((2, 6, 0), (4, 1, 29)),
        ((4, 2, 0), (4, 2, 25)),
        ((4, 3, 0), (4, 3, 8)),
    ],
    'api-platform/json-api': [
        ((4, 0, 0), (4, 1, 29)),
        ((4, 2, 0), (4, 2, 25)),
        ((4, 3, 0), (4, 3, 8)),
    ],
    'api-platform/hal': [
        ((4, 0, 0), (4, 1, 29)),
        ((4, 2, 0), (4, 2, 25)),
        ((4, 3, 0), (4, 3, 8)),
    ],
}

FIXED = {
    'api-platform/core': '4.1.29 / 4.2.25 / 4.3.8',
    'api-platform/json-api': '4.1.29 / 4.2.25 / 4.3.8',
    'api-platform/hal': '4.1.29 / 4.2.25 / 4.3.8',
}

SEMVER_RE = re.compile(r'^(?:v)?(\d+)\.(\d+)\.(\d+)')


def parse_version(v: str) -> Optional[Tuple[int, int, int]]:
    m = SEMVER_RE.match((v or '').strip())
    if not m:
        return None
    return tuple(int(x) for x in m.groups())


def in_range(ver: Tuple[int, int, int], start: Tuple[int, int, int], end_exclusive: Tuple[int, int, int]) -> bool:
    return start <= ver < end_exclusive


def load_packages(lockfile: str):
    with open(lockfile, 'r', encoding='utf-8') as fh:
        data = json.load(fh)
    packages = []
    packages.extend(data.get('packages', []))
    packages.extend(data.get('packages-dev', []))
    return packages


def main():
    if len(sys.argv) != 2:
        print('UNKNOWN - usage: python3 check_cve_2026_49858.py /path/to/composer.lock')
        sys.exit(2)

    lockfile = sys.argv[1]
    if not os.path.isfile(lockfile):
        print(f'UNKNOWN - composer.lock not found: {lockfile}')
        sys.exit(2)

    try:
        packages = load_packages(lockfile)
    except Exception as exc:
        print(f'UNKNOWN - failed to parse composer.lock: {exc}')
        sys.exit(2)

    found = []
    vulnerable = []
    unknown_versions = []

    for pkg in packages:
        name = pkg.get('name')
        version = pkg.get('version', '')
        if name not in AFFECTED:
            continue
        found.append((name, version))
        parsed = parse_version(version)
        if parsed is None:
            unknown_versions.append((name, version))
            continue
        for start, end_exc in AFFECTED[name]:
            if in_range(parsed, start, end_exc):
                vulnerable.append((name, version, FIXED[name]))
                break

    if vulnerable:
        details = '; '.join([f'{n} {v} (fixed in {f})' for n, v, f in vulnerable])
        print(f'VULNERABLE - {details}. Note: exploitability additionally requires JSON:API/HAL + user-dependent ApiProperty security + long-running PHP worker reuse.')
        sys.exit(1)

    if unknown_versions:
        details = '; '.join([f'{n} {v}' for n, v in unknown_versions])
        print(f'UNKNOWN - installed package versions could not be parsed: {details}')
        sys.exit(2)

    if found:
        details = '; '.join([f'{n} {v}' for n, v in found])
        print(f'PATCHED - installed API Platform packages are outside known affected ranges: {details}')
        sys.exit(0)

    print('PATCHED - none of api-platform/core, api-platform/json-api, or api-platform/hal were found in composer.lock')
    sys.exit(0)


if __name__ == '__main__':
    main()
07 · Bottom Line

If you remember one thing.

TL;DR
Monday morning: identify the subset of API Platform services that both expose JSON:API/HAL and run on persistent PHP workers; those are the only systems worth fast attention here. Because this is a LOW reassessment, there is no noisgate mitigation SLA and noisgate remediation SLA is backlog hygiene rather than an emergency push: if you can cheaply disable JSON:API/HAL or revert those services to classic php-fpm, do it in the next normal change window, then upgrade vulnerable branches to 4.1.29, 4.2.25, or 4.3.8 as part of regular dependency maintenance rather than burning an out-of-band patch cycle.

Sources

  1. GitHub Security Advisory GHSA-pjhx-3c3w-9v23
  2. Packagist: api-platform/core
  3. Packagist: api-platform/json-api
  4. Packagist: api-platform/hal
  5. CISA Known Exploited Vulnerabilities Catalog
  6. FIRST EPSS API documentation
  7. Prior related API Platform advisory GHSA-428q-q3vv-3fq3
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.