CVEReports
CVEReports

Automated vulnerability intelligence platform. Comprehensive reports for high-severity CVEs generated by AI.

Product

  • Home
  • Sitemap
  • RSS Feed

Company

  • About
  • Contact
  • Privacy Policy
  • Terms of Service

© 2026 CVEReports. All rights reserved.

Made with love by Amit Schendel & Alon Barad



CVE-2026-25494
6.9

Craft CMS: The Art of Hexing Your Way to AWS Metadata

Amit Schendel
Amit Schendel
Senior Security Researcher

Feb 10, 2026·6 min read·10 visits

PoC Available

Executive Summary (TL;DR)

Craft CMS's GraphQL `saveAsset` mutation tries to block internal IPs using `filter_var`, which doesn't understand Hex/Octal IPs. Linux/cURL does. Attackers can request `http://0xa9fea9fe` to hit `169.254.169.254`, bypassing the check and stealing cloud credentials.

A high-severity Server-Side Request Forgery (SSRF) vulnerability in Craft CMS allows attackers to bypass IP blocklists using alternative IP notations (hexadecimal). By abusing the discrepancy between PHP's validation logic and the underlying system's DNS resolver, attackers can trick the application into fetching sensitive internal resources—most notably cloud instance metadata—via the GraphQL API.

The Hook: GraphQL is a Feature, Not a Bug (Usually)

Modern CMS platforms are obsessed with 'headless' capabilities, and Craft CMS is no exception. It offers a robust GraphQL API that lets developers fetch content, manage users, and—crucially for us—upload assets. The saveAsset mutation is designed to be helpful: you give it a URL, and the server fetches the file and saves it for you. It's the classic 'fetch from URL' feature that has kept bug bounty hunters fed for a decade.

Here's the problem: whenever you let a user tell a server where to go, you are inviting SSRF (Server-Side Request Forgery) to the party. The developers at Craft CMS aren't novices; they knew this. They implemented a blocklist to prevent users from accessing local network resources. They specifically tried to stop you from hitting the loopback address (127.0.0.1) or the infamous cloud metadata service (169.254.169.254).

But blocklists are like screen doors on a submarine. They look solid until you apply a little pressure. In this case, the pressure comes in the form of alternative IP notations—a concept as old as the internet standards themselves, yet consistently forgotten by PHP developers relying on standard library functions.

The Flaw: The Babel Fish Problem

The core of this vulnerability lies in a fundamental disagreement between two parts of the technology stack: the validator and the resolver. This is a classic 'Time-of-Check Time-of-Use' (TOCTOU) logic error, specifically purely semantic.

The Validator: Craft CMS used PHP's built-in filter_var($url, FILTER_VALIDATE_IP) function. This function is strict. It expects standard dotted-decimal notation for IPv4 (e.g., 192.168.1.1). If you feed it a hexadecimal string like 0xa9fea9fe, filter_var looks at it, scoffs, and says, "That's not an IP address, that's a domain name." It returns false (not an IP).

The Resolver: Since the validator decides the input is a domain name, the code allows the request to proceed to the HTTP client (Guzzle). Guzzle hands the hostname off to the operating system's resolver (getaddrinfo in libc). The OS resolver is POSIX-compliant and speaks many dialects. It sees 0xa9fea9fe, recognizes it as a valid hexadecimal representation of an IP address, and happily converts it to 169.254.169.254.

So, the application explicitly allows the request because it thinks it's safe, while the OS executes the request because it knows it's an IP. The security check is bypassed not by breaking it, but by speaking a language it doesn't understand.

The Code: Anatomy of a Bypass

Let's look at the logic flow that allowed this to happen. The simplified vulnerable logic looked something like this:

// The Vulnerable Logic
$hostname = parse_url($url, PHP_URL_HOST);
 
// Check 1: Is it a valid IP according to PHP?
if (filter_var($hostname, FILTER_VALIDATE_IP)) {
    // It's an IP. Is it on our blocklist (e.g., private ranges)?
    if (IpHelper::isInternalIp($hostname)) {
        throw new ForbiddenException("No internal IPs allowed!");
    }
}
 
// Check 2: If it's NOT an IP, we assume it's a domain and allow it.
// ... Proceed to fetch URL ...

When we send http://0xa9fea9fe/, filter_var returns false. The code skips the isInternalIp check entirely because it assumes the input is a domain name.

The fix implemented in versions 4.16.18 and 5.8.22 involves normalizing the hostname before checking it. They added a routine to detect 0x prefixes and convert them to decimal:

// The Fix (simplified from commit d49e93e)
$hostname = Collection::make(explode('.', $hostname))
    ->map(function(string $chunk) {
        // If it looks like hex, turn it into decimal
        if (str_starts_with($chunk, '0x')) {
             $octets = str_split(substr($chunk, 2), 2);
             return implode('.', array_map('hexdec', $octets));
        }
        return $chunk;
    })
    ->join('.');
 
// Now run the validation on the normalized string
if (filter_var($hostname, FILTER_VALIDATE_IP)) { ... }

> [!NOTE] > While this fixes the Hex bypass, it is a game of whack-a-mole. What about Octal IPs (0177.0.0.1)? What about Dword notation (2130706433)? Blocklists that try to sanitize input rather than validating the resolved destination IP are historically prone to failure.

The Exploit: Extracting AWS Credentials

To exploit this, we don't need fancy tools. We just need a GraphQL client (or curl) and a hexadecimal calculator. Our target is the AWS Instance Metadata Service (IMDSv1), which resides at 169.254.169.254.

First, we convert the target IP to hex:

  1. 169 -> 0xa9
  2. 254 -> 0xfe
  3. 169 -> 0xa9
  4. 254 -> 0xfe Result: 0xa9fea9fe (or 0xa9.0xfe.0xa9.0xfe—both work).

Now, we construct the GraphQL mutation. We want to save an 'asset' that is actually the AWS security credentials.

mutation {
  saveAsset(fileInformation: {
    url: "http://0xa9fea9fe/latest/meta-data/iam/security-credentials/admin-role"
  }) {
    id
    filename
    url  # The CMS will give us a link to view the 'file' it just downloaded
  }
}

The Execution Flow:

If successful, Craft CMS downloads the JSON blob containing the AccessKeyId, SecretAccessKey, and Token, saving it as a file in the public assets folder. The attacker simply downloads the file, exports the keys locally, and pivots into the cloud environment.

The Impact: Why This Matters

SSRF is often dismissed as 'just network scanning,' but in cloud environments, it is a critical severity issue. Access to the metadata service (IMDS) effectively grants the attacker the same privileges as the server itself.

If the Craft CMS server has an IAM role allowing it to access S3 buckets (for asset storage) or RDS databases, the attacker inherits those permissions. We have seen this exact pattern lead to massive data breaches (e.g., the Capital One breach).

Beyond cloud metadata, this allows probing the internal network. An attacker could scan for internal admin panels, unauthenticated Redis instances (http://0x7f000001:6379), or other services that assume 'internal network' means 'trusted user'. The hex bypass makes standard firewall logging harder to audit, as the initial request logs might show the hex string rather than the resolved IP.

The Fix: Stopping the Bleeding

The immediate remediation is to upgrade Craft CMS to version 4.16.18 or 5.8.22. These versions include the logic to normalize hexadecimal strings before validation.

However, as a security researcher, I strongly advise going further than the patch:

  1. Disable IMDSv1: AWS supports IMDSv2, which requires a session token header (X-aws-ec2-metadata-token). SSRF vulnerabilities usually cannot inject headers, making IMDSv2 immune to this specific attack vector.
  2. Network Segmentation: Why can your web server initiate outbound connections to your internal network? Use egress filtering (iptables or VPC Security Groups) to block traffic to 169.254.169.254 and 10.0.0.0/8 unless explicitly required.
  3. Disable Unused Features: If you aren't using the saveAsset mutation or public GraphQL access, disable it. An attack surface that doesn't exist cannot be exploited.

The patch fixes the bug, but architectural changes fix the vulnerability.

Official Patches

Craft CMSOfficial Release Notes for 5.8.22

Fix Analysis (1)

Technical Appendix

CVSS Score
6.9/ 10
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N

Affected Systems

Craft CMS 4.x (< 4.16.18)Craft CMS 5.x (< 5.8.22)

Affected Versions Detail

Product
Affected Versions
Fixed Version
Craft CMS
Pixel & Tonic
>= 4.0.0-RC1, < 4.16.184.16.18
Craft CMS
Pixel & Tonic
>= 5.0.0-RC1, < 5.8.225.8.22
AttributeDetail
CWE IDCWE-918
Attack VectorNetwork
CVSS v4.06.9
ImpactConfidentiality (High - Cloud Credential Theft)
Exploit StatusPoC Available
ProtocolGraphQL / HTTP

MITRE ATT&CK Mapping

T1190Exploit Public-Facing Application
Initial Access
T1552.005Cloud Instance Metadata API
Credential Access
CWE-918
Server-Side Request Forgery (SSRF)

The application does not validate or incorrectly validates input that can affect the destination of a server-side HTTP request, allowing an attacker to force the server to connect to arbitrary or internal destinations.

Known Exploits & Detection

ManualGraphQL mutation utilizing hex-encoded IP address to access internal metadata.

Vulnerability Timeline

Initial SSRF protections merged into 5.x
2025-12-30
Hex normalization fix committed (d49e93e)
2026-01-05
CVE-2026-25494 Published
2026-02-09

References & Sources

  • [1]GHSA-m5r2-8p9x-hp5m
  • [2]CWE-918 SSRF

Attack Flow Diagram

Press enter or space to select a node. You can then use the arrow keys to move the node around. Press delete to remove it and escape to cancel.
Press enter or space to select an edge. You can then press delete to remove it or escape to cancel.