Skip to content

SSRF in GQL asset mutation - incomplete IP denylist + post-fetch validation

Moderate
angrybrad published GHSA-2mx8-9ww7-p27x Jul 25, 2026

Package

composer craftcms/cms (Composer)

Affected versions

>= 5.0.0-RC1, < 5.10.6
>= 4.0.0-RC1, < 4.18.2

Patched versions

5.10.6
4.18.2

Description

The GraphQL mutation save<Volume>Asset(_file: { url: ... }) fetches an attacker-supplied URL server-side. The anti-SSRF logic introduced by prior advisories (GHSA-gp2f-7wcm-5fhx, GHSA-v2gc-rm6g-wrw9, GHSA-3m9m-24vh-39wx) is incomplete, so a GraphQL token scoped only to volumes.<uid>:create can:

  1. Read internal HTTP responses from any target whose IP address passes validateIp() but is not reachable by the attacker directly. validateIp() relies on FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE, which does not cover CGNAT 100.64.0.0/10 (RFC 6598) or NAT64 64:ff9b::/96. Requests to those ranges are accepted, and the response is saved as the asset, and the attacker downloads it — full content disclosure.

  2. Force internal GET requests to any host (RFC1918/loopback/metadata included). The only IP check runs in Guzzle’s on_stats callback, which fires after cURL has already connected and written the response body to disk. Rejection aborts the asset save, but the outbound request has already been issued.

  3. Blindly enumerate internal hosts/ports. The four distinct, client-safe error/success outcomes (returned even with devMode off) plus connect/timeout timing from an oracle for targets whose bodies are withheld.

This is a privilege-boundary violation: a token authorized only to create assets in one volume should not be able to drive arbitrary server-side HTTP requests or read internal services.

Impact

A low-privilege, asset-creation-only authorization is escalated into arbitrary outbound server-side requests and disclosure of internal HTTP content - a privilege/trust-boundary violation in native Craft code.

Severity

Moderate

CVE ID

No known CVE

Weaknesses

No CWEs