GitHub Security Advisory: GHSA-v2gc-rm6g-wrw9
Package: craftcms/cms (COMPOSER)
Severity: MODERATE (CVSS 0.0)
CWE: CWE-918: Server-Side Request Forgery (SSRF)
Vulnerable versions: >= 3.5.0, <= 4.16.18
Patched in: 4.16.19

Summary: Craft CMS: Cloud Metadata SSRF Protection Bypass via IPv6 Resolution

Details:
The SSRF validation in Craft CMS’s GraphQL Asset mutation uses `gethostbyname()`, which only resolves IPv4 addresses. When a hostname has only AAAA (IPv6) records, the function returns the hostname string itself, causing the blocklist comparison to always fail and completely bypassing SSRF protection.

This is a bypass of the security fix for CVE-2025-68437 ([GHSA-x27p-wfqw-hfcc](https://github.com/craftcms/cms/security/advisories/GHSA-x27p-wfqw-hfcc)).

## Required Permissions

Exploitation requires GraphQL schema permissions for:
- Edit assets in the `<VolumeName>` volume
- Create assets in the `<VolumeName>` volume

These permissions may be granted to:
- Authenticated users with appropriate GraphQL schema access
- Public Schema (if misconfigured with write permissions)

---

## Technical Details

### Root Cause

From PHP documentation: *"gethostbyname - Get the IPv4 address corresponding to a given Internet host name"*

When no IPv4 (A record) exists, `gethostbyname()` returns the hostname string unchanged.

### Bypass Mechanism

```
+-----------------------------------------------------------------------------+
| Step 1: Attacker provides URL                                               |
|         http://fd00-ec2--254.sslip.io/latest/meta-data/                     |
+-----------------------------------------------------------------------------+
| Step 2: Validation calls gethostbyname('fd00-ec2--254.sslip.io')            |
|         -> No A record exists                                               |
|         -> Returns: "fd00-ec2--254.sslip.io" (string, not an IP!)           |
+-----------------------------------------------------------------------------+
| Step 3: Blocklist check                                                     |
|         in_array("fd00-ec2--254.sslip.io", ['169.254.169.254', ...])       |
|         -> FALSE (string != IPv4 addresses)                                 |
|         -> VALIDATION PASSES                                                |
+-----------------------------------------------------------------------------+
| Step 4: Guzzle makes HTTP request                                           |
|         -> Resolves DNS (including AAAA records)                            |
|         -> Gets IPv6: fd00:ec2::254                                         |
|         -> Connects to AWS IMDS IPv6 endpoint                               |
|         -> CREDENTIALS STOLEN                                               |
+-----------------------------------------------------------------------------+
```

---

## Bypass Payloads

### Blocked IPv4 Addresses and Their IPv6 Bypass Equivalents

| Cloud Provider | Blocked IPv4 | IPv6 Equivalent | Bypass Payload |
|----------------|--------------|-----------------|----------------|
| **AWS EC2 IMDS** | `169.254.169.254` | `fd00:ec2::254` | `http://fd00-ec2--254.sslip.io/` |
| **AWS ECS** | `169.254.170.2` | `fd00:ec2::254` (via IMDS) | `http://fd00-ec2--254.sslip.io/` |
| **Go