Skip to content

Commit 819404a

Browse files
1 parent 4088f0c commit 819404a

2 files changed

Lines changed: 196 additions & 0 deletions

File tree

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
{
2+
"schema_version": "1.4.0",
3+
"id": "GHSA-6fx5-5cw5-4897",
4+
"modified": "2026-02-23T22:16:22Z",
5+
"published": "2026-02-23T22:16:22Z",
6+
"aliases": [
7+
"CVE-2026-27128"
8+
],
9+
"summary": "Craft CMS Race condition in Token Service potentially allows for token usage greater than the token limit",
10+
"details": "A Time-of-Check-Time-of-Use (TOCTOU) race condition exists in Craft CMS’s token validation service for tokens that explicitly set a limited usage. The `getTokenRoute()` method reads a token’s usage count, checks if it’s within limits, then updates the database in separate non-atomic operations. By sending concurrent requests, an attacker can use a single-use impersonation token multiple times before the database update completes.\n\nTo make this work, an attacker needs to obtain a valid user account impersonation URL with a non-expired token via some other means and exploit a race condition while bypassing any rate-limiting rules in place.\n\nFor this to be a privilege escalation, the impersonation URL must include a token for a user account with more permissions than the current user.\n\n## References\n\nhttps://github.com/craftcms/cms/commit/3e4afe18279951c024c64896aa2b93cda6d95fdf",
11+
"severity": [
12+
{
13+
"type": "CVSS_V4",
14+
"score": "CVSS:4.0/AV:N/AC:H/AT:P/PR:H/UI:N/VC:H/VI:N/VA:N/SC:H/SI:N/SA:N"
15+
}
16+
],
17+
"affected": [
18+
{
19+
"package": {
20+
"ecosystem": "Packagist",
21+
"name": "craftcms/cms"
22+
},
23+
"ranges": [
24+
{
25+
"type": "ECOSYSTEM",
26+
"events": [
27+
{
28+
"introduced": "4.5.0-RC1"
29+
},
30+
{
31+
"fixed": "4.16.19"
32+
}
33+
]
34+
}
35+
],
36+
"database_specific": {
37+
"last_known_affected_version_range": "<= 4.16.18"
38+
}
39+
},
40+
{
41+
"package": {
42+
"ecosystem": "Packagist",
43+
"name": "craftcms/cms"
44+
},
45+
"ranges": [
46+
{
47+
"type": "ECOSYSTEM",
48+
"events": [
49+
{
50+
"introduced": "5.0.0-RC1"
51+
},
52+
{
53+
"fixed": "5.8.23"
54+
}
55+
]
56+
}
57+
],
58+
"database_specific": {
59+
"last_known_affected_version_range": "<= 5.8.22"
60+
}
61+
}
62+
],
63+
"references": [
64+
{
65+
"type": "WEB",
66+
"url": "https://github.com/craftcms/cms/security/advisories/GHSA-6fx5-5cw5-4897"
67+
},
68+
{
69+
"type": "WEB",
70+
"url": "https://github.com/craftcms/cms/commit/3e4afe18279951c024c64896aa2b93cda6d95fdf"
71+
},
72+
{
73+
"type": "PACKAGE",
74+
"url": "https://github.com/craftcms/cms"
75+
}
76+
],
77+
"database_specific": {
78+
"cwe_ids": [
79+
"CWE-367"
80+
],
81+
"severity": "MODERATE",
82+
"github_reviewed": true,
83+
"github_reviewed_at": "2026-02-23T22:16:22Z",
84+
"nvd_published_at": null
85+
}
86+
}
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
{
2+
"schema_version": "1.4.0",
3+
"id": "GHSA-gp2f-7wcm-5fhx",
4+
"modified": "2026-02-23T22:16:01Z",
5+
"published": "2026-02-23T22:16:01Z",
6+
"aliases": [
7+
"CVE-2026-27127"
8+
],
9+
"summary": "Craft CMS has Cloud Metadata SSRF Protection Bypass via DNS Rebinding",
10+
"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",
11+
"severity": [
12+
{
13+
"type": "CVSS_V4",
14+
"score": "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"
15+
}
16+
],
17+
"affected": [
18+
{
19+
"package": {
20+
"ecosystem": "Packagist",
21+
"name": "craftcms/cms"
22+
},
23+
"ranges": [
24+
{
25+
"type": "ECOSYSTEM",
26+
"events": [
27+
{
28+
"introduced": "5.0.0-RC1"
29+
},
30+
{
31+
"fixed": "5.8.23"
32+
}
33+
]
34+
}
35+
],
36+
"database_specific": {
37+
"last_known_affected_version_range": "<= 5.8.22"
38+
}
39+
},
40+
{
41+
"package": {
42+
"ecosystem": "Packagist",
43+
"name": "craftcms/cms"
44+
},
45+
"ranges": [
46+
{
47+
"type": "ECOSYSTEM",
48+
"events": [
49+
{
50+
"introduced": "3.5.0"
51+
},
52+
{
53+
"fixed": "4.16.19"
54+
}
55+
]
56+
}
57+
],
58+
"database_specific": {
59+
"last_known_affected_version_range": "<= 4.16.18"
60+
}
61+
}
62+
],
63+
"references": [
64+
{
65+
"type": "WEB",
66+
"url": "https://github.com/craftcms/cms/security/advisories/GHSA-gp2f-7wcm-5fhx"
67+
},
68+
{
69+
"type": "WEB",
70+
"url": "https://github.com/craftcms/cms/security/advisories/GHSA-x27p-wfqw-hfcc"
71+
},
72+
{
73+
"type": "WEB",
74+
"url": "https://github.com/craftcms/cms/commit/a4cf3fb63bba3249cf1e2882b18a2d29e77a8575"
75+
},
76+
{
77+
"type": "WEB",
78+
"url": "https://curl.se/libcurl/c/CURLOPT_RESOLVE.html"
79+
},
80+
{
81+
"type": "PACKAGE",
82+
"url": "https://github.com/craftcms/cms"
83+
},
84+
{
85+
"type": "WEB",
86+
"url": "https://github.com/mogwailabs/DNSrebinder"
87+
},
88+
{
89+
"type": "WEB",
90+
"url": "https://github.com/nccgroup/singularity"
91+
},
92+
{
93+
"type": "WEB",
94+
"url": "https://github.com/taviso/rbndr"
95+
},
96+
{
97+
"type": "WEB",
98+
"url": "https://unit42.paloaltonetworks.com/dns-rebinding"
99+
}
100+
],
101+
"database_specific": {
102+
"cwe_ids": [
103+
"CWE-367"
104+
],
105+
"severity": "HIGH",
106+
"github_reviewed": true,
107+
"github_reviewed_at": "2026-02-23T22:16:01Z",
108+
"nvd_published_at": null
109+
}
110+
}

0 commit comments

Comments
 (0)