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.
4 steps from start to impact.
Find a JSON:API or HAL endpoint with per-user property rules
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.- Target application uses API Platform
- Target exposes JSON:API and/or HAL responses
- At least one property uses user-dependent
ApiProperty(security=...)
- 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
Get a privileged response to warm componentsCache
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.- A higher-privilege request reaches the same worker first
- The worker process keeps the normalizer instance alive across requests
- 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
Hit the same worker as a lower-privilege or anonymous user
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.- Second request lands on the same live worker
- Anonymous or low-privilege access to the endpoint exists
- 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
Use leaked attribute names and links for follow-on discovery
- Leaked field names or relationships are themselves sensitive
- Attacker can interpret the app's data model
- 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
The supporting signals.
| In-the-wild status | No confirmed active exploitation found during this assessment. The advisory names no campaigns, and CISA KEV does not surface this CVE. |
|---|---|
| Public PoC | No 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. |
| EPSS | Unavailable / 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 status | Not KEV-listed. That removes the strongest urgency amplifier for enterprise patch queues. |
| CVSS vector | CVSS: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 versions | 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 versions | Upgrade 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 preconditions | This 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 measurability | Poorly 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 / credits | Published 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. |
noisgate verdict.
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.
Why this verdict
- Downgrade 1 — requires non-default runtime behavior: classic
php-fpmis 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:Nis 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.
What to do — in priority order.
- 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.
- Pin affected services to classic
php-fpmrequest isolation — If you currently run FrankenPHP worker mode, RoadRunner, Swoole, or similar, reverting the impacted API services to classic request-isolatedphp-fpmbreaks the cache persistence assumption behind this bug. Use this as a temporary control where upgrade lead time is long. - 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. - Override the normalizers if you cannot upgrade quickly — The advisory explicitly suggests overriding the JSON:API and HAL
ItemNormalizerservices to gatecache_keywith a resource-class security check. Use this only when package upgrades are blocked and document the local patch.
- 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.
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.
#!/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()
If you remember one thing.
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
What defenders are saying.
Crowdsourced verification outputs.
Results submitted by users who ran the verification payload against their environment.