
Drupal 11.4.8 - High - Remote - SSRF - oEmbed discovery GETs an unscoped second-hop URL
abraxaslabs.tech · github.com/abraxas · @abraxas_null · [email protected] · drupal-oembed-ssrf
Class: SSRF Reach: Remote
Drupal core 11.4.8 (61044508) - Drupal
SA-CORE-2026-008 gated the discovery document host with media_oembed_discovery_trusted_host_patterns. UrlResolver::findUrl still returns the raw link href. ResourceFetcher::fetchResource then GETs that URL with no host or private-IP check. oEmbed thumbnail_url is the same class of GET. Default patterns are empty, so discovery is off until an admin enables it.
After an admin turns discovery on, a user who can create oEmbed media can point Drupal at an allowlisted page whose JSON oEmbed link href is loopback, RFC1918, or metadata.
| ID | no CVE yet |
| Class | SSRF (second-hop leftover of SA-CORE-2026-008) |
| Reach | Remote (authenticated media create, after admin sets discovery hosts) |
| CWE | CWE-918 |
| CVSS | Medium: 6.1 CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:C/C:H/I:N/A:N |
| Product | Drupal core |
| Affected | 11.4.8 (61044508071749629aaf3380aa85971e94df9e84) |
| Auth | authenticated media create, after an admin sets discovery hosts. AC:H is that admin toggle. |
| License | GNU Affero GPL v3.0 |
| Lab | 127.0.0.1 only |
Only after an admin has set discovery trusted-host patterns. A media creator is enough after that. A complete stranger with no account cannot do this on default. This is SSRF, not RCE and not an admin login.
| Privilege | Method / sink | Kind | Lab-proven or inferred |
|---|---|---|---|
| Make Drupal GET an attacker-chosen URL (loopback in this lab) | UrlResolver::findUrl raw href then ResourceFetcher::fetchResource GET | recon (server-side fetch) | Lab-proven. http://127.0.0.1:18080/oembed returned DRUPAL-OEMBED-SSRF-WITNESS |
| Read the HTTP body of that URL into PHP | Same GET; JSON/XML parsed as oEmbed | recon | Lab-proven. Witness was in the 200 body |
Same GET for thumbnail_url after a resource is built | OEmbed::getLocalThumbnailUri ~417-456 | recon | Inferred from the pin. This lab stopped at the discovered href |
| Reach RFC1918 / link-local / metadata if the PHP netns can | Unscoped Guzzle GET | recon | Inferred. Lab used loopback, not 169.254.169.254 |
Closed on this leftover: first hop when patterns are empty (lab-proven); first hop when the HTML host does not match (lab-proven); unauthenticated media create on Standard without a media role; RCE; admin session theft.
I pinned Drupal 11.4.8 after SA-CORE-2026-008. The first hop is gated:
$patterns = \array_map(static fn ($hostPattern) => \sprintf('{%s}i', $hostPattern), Settings::get('media_oembed_discovery_trusted_host_patterns', []));
// ...
if (!$allowed) {
return FALSE;
}
$response = $this->httpClient->get($url);
return $this->findUrl($xpath, 'json') ?: $this->findUrl($xpath, 'xml');
findUrl returns the raw href. ResourceFetcher GETs it with a timeout and nothing else:
$result = $xpath->query("//link[@type='application/$format+oembed']");
return $result->length ? $result->item(0)->getAttribute('href') : FALSE;
$response = $this->httpClient->request('GET', $url, [
RequestOptions::TIMEOUT => $this->timeout,
]);
The lab is a faithful gadget of those 11.4.8 functions, not a live drupal:11.4.8 site install. Last line uses how=gadget. Empty patterns did not fetch. A non-matching host did not fetch. With ^firsthop$ allowed, discoverResourceUrl returned http://127.0.0.1:18080/oembed and the unscoped GET returned the witness.
Wrong turns already recorded: treating default-off as a miss (it is the intended first-hop gate; the leftover is hop two); calling a live Standard install the oracle (this pack is the pin functions); claiming RCE because Guzzle ran. Theatre: a reverse shell. The oracle is default-off plus wrong-host blocked plus the second-hop witness.
Same pin leftovers: CKEditor 5 FileIsImage skip.
python3 drupal-oembed-ssrf-Abraxas-Labs.py
or
cd lab
./run.sh
Loopback only: first-hop HTML on http://127.0.0.1:19510. Second hop is http://127.0.0.1:18080/oembed inside the gadget netns.
SUCCESS drupal-oembed-ssrf how=gadget pin=11.4.8 first-hop=allowed second-hop=127.0.0.1:18080 default-off=blocked DRUPAL-OEMBED-SSRF-WITNESS
Run the same host / private-IP policy on the discovered endpoint and on thumbnail_url. Do not fetch an href whose host would have failed the first hop.
61044508071749629aaf3380aa85971e94df9e84)UrlResolver.php · ResourceFetcher.phpGNU Affero GPL v3.0. See LICENSE. Loopback lab only. No warranty.