+ "details": "## Summary\n\nThe SSRF validation in Craft CMS’s GraphQL Asset mutation performs DNS resolution **separately** from the HTTP request. This Time-of-Check-Time-of-Use (TOCTOU) vulnerability enables DNS rebinding attacks, where an attacker’s DNS server returns different IP addresses for validation compared to the actual request.\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)) that allows access to all blocked IPs, not just IPv6 endpoints.\n\n## Severity\n\nBypass of cloud metadata SSRF protection for all blocked IPs\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### Vulnerable Code Flow\n\nThe code at `src/gql/resolvers/mutations/Asset.php` performs two separate DNS lookups:\n\n```php\n// VALIDATION PHASE: First DNS resolution at time T1\nprivate function validateHostname(string $url): bool\n{\n $hostname = parse_url($url, PHP_URL_HOST);\n $ip = gethostbyname($hostname); // DNS Lookup #1 - Returns safe IP\n\n if (in_array($ip, [\n '169.254.169.254', // AWS, GCP, Azure IMDS\n '169.254.170.2', // AWS ECS metadata\n '100.100.100.200', // Alibaba Cloud\n '192.0.0.192', // Oracle Cloud\n ])) {\n return false; // Check passes - IP looks safe\n }\n return true;\n}\n\n// ... time gap between validation and request ...\n\n// REQUEST PHASE: Second DNS resolution at time T2 (inside Guzzle)\n$response = $client->get($url); // DNS Lookup #2 - Guzzle resolves DNS AGAIN\n // Now returns 169.254.169.254!\n```\n\n### Root Cause\n\nTwo separate DNS lookups occur:\n1. **Validation**: `gethostbyname()` in `validateHostname()`\n2. **Request**: Guzzle's internal DNS resolution via libcurl\n\nAn attacker controlling a DNS server can return different IPs for each query.\n\n### Bypass Mechanism\n\n```\n+-----------------------------------------------------------------------------+\n| Attacker's DNS Server: evil.attacker.com |\n+-----------------------------------------------------------------------------+\n| Query 1 (Validation - T1): |\n| Request: A record for evil.attacker.com |\n| Response: 1.2.3.4 (safe IP, TTL: 0) |\n| Result: Validation PASSES |\n+-----------------------------------------------------------------------------+\n| Query 2 (Guzzle Request - T2): |\n| Request: A record for evil.attacker.com |\n| Response: 169.254.169.254 (metadata IP, TTL: 0) |\n| Result: Request goes to blocked IP -> CREDENTIALS STOLEN |\n+-----------------------------------------------------------------------------+\n```\n\n---\n\n## Target Endpoints via DNS Rebinding\n\nDNS rebinding allows access to all blocked IPs:\n\n| Target | Rebind To | Impact |\n|--------|-----------|--------|\n| **AWS IMDS** | `169.254.169.254` | IAM credentials, instance identity |\n| **AWS ECS** | `169.254.170.2` | Container credentials |\n| **GCP Metadata** | `169.254.169.254` | Service account tokens |\n| **Azure Metadata** | `169.254.169.254` | Managed identity tokens |\n| **Alibaba Cloud** | `100.100.100.200` | Instance credentials |\n| **Oracle Cloud** | `192.0.0.192` | Instance metadata |\n| **Internal Services** | `127.0.0.1`, `10.x.x.x` | Internal APIs, databases |\n\n---\n\n### Attack Scenario\n\n1. Attacker sets up DNS server with alternating responses\n2. Attacker sends mutation with `url: \"http://evil.attacker.com/latest/meta-data/\"`\n3. First DNS query returns safe IP (e.g., `1.2.3.4`) → validation passes\n4. Second DNS query returns metadata IP (`169.254.169.254`) → request to metadata\n5. Attacker retrieves credentials from ANY cloud provider\n6. **Attacker can now achieve code execution by creating new instances with their SSH key**\n\n---\n\n## Remediation\n\n### Fix: DNS Pinning with CURLOPT_RESOLVE\n\nPin the DNS resolution - use the same resolved IP for both validation and request:\n\n```php\nprivate function validateHostname(string $url): bool\n{\n $hostname = parse_url($url, PHP_URL_HOST);\n\n // Resolve once\n $ip = gethostbyname($hostname);\n\n // Validate the resolved IP\n if (in_array($ip, [\n '169.254.169.254', '169.254.170.2',\n '100.100.100.200', '192.0.0.192',\n ])) {\n return false;\n }\n\n // Store for later use\n $this->pinnedDNS[$hostname] = $ip;\n\n return true;\n}\n\n// When making the request - CRITICAL: Use pinned IP\nprotected function makeRequest(string $url): ResponseInterface\n{\n $hostname = parse_url($url, PHP_URL_HOST);\n $ip = $this->pinnedDNS[$hostname] ?? null;\n\n $options = [];\n if ($ip) {\n // Force Guzzle/curl to use the SAME IP we validated\n $options['curl'] = [\n CURLOPT_RESOLVE => [\n \"$hostname:80:$ip\",\n \"$hostname:443:$ip\"\n ]\n ];\n }\n\n return $this->client->get($url, $options);\n}\n```\n\n### Alternative: Single Resolution with Immediate Use\n\n```php\n// Resolve to IP and use IP directly in URL\n$ip = gethostbyname($hostname);\n\nif (in_array($ip, $blockedIPs)) {\n return false;\n}\n\n// Make request directly to IP with Host header\n$client->get(\"http://$ip\" . parse_url($url, PHP_URL_PATH), [\n 'headers' => [\n 'Host' => $hostname\n ]\n]);\n```\n\n### Additional Mitigations\n\n| Mitigation | Description |\n|------------|-------------|\n| DNS Pinning (CURLOPT_RESOLVE) | Force same IP for validation and request |\n| Single IP-based request | Use resolved IP directly in URL |\n| Implement IMDSv2 | Requires token header (infrastructure-level) |\n| Network egress filtering | Block metadata IPs at network level |\n\n---\n\n## Resources\n\n- https://github.com/craftcms/cms/commit/a4cf3fb63bba3249cf1e2882b18a2d29e77a8575\n- [GHSA-x27p-wfqw-hfcc](https://github.com/craftcms/cms/security/advisories/GHSA-x27p-wfqw-hfcc) - Original SSRF vulnerability (CVE-2025-68437)\n- [DNSrebinder](https://github.com/mogwailabs/DNSrebinder) - Lightweight Python DNS server for testing DNS rebinding vulnerabilities; responds with legitimate IP for first N queries, then rebinds to target IP\n- [Singularity DNS Rebinding Tool](https://github.com/nccgroup/singularity)\n- [rbndr DNS Rebinding Service](https://github.com/taviso/rbndr)\n- [DNS Rebinding Attacks Explained](https://unit42.paloaltonetworks.com/dns-rebinding/)\n- [CURLOPT_RESOLVE Documentation](https://curl.se/libcurl/c/CURLOPT_RESOLVE.html)\n- OWASP SSRF Prevention Cheat Sheet",
0 commit comments