GHSA-GQ96-5PFX-F4VC
Vulnerability from github – Published: 2026-06-04 19:36 – Updated: 2026-06-04 19:36Summary
The /api/_action/media/external-link endpoint allows authenticated admin users to make server-side HTTP HEAD requests to arbitrary internal IP addresses. While the parallel uploadFromURL flow validates target IPs against private/reserved ranges via FileUrlValidator, the linkURL flow only performs a URL format check (regex for http:// or https:// prefix), allowing SSRF to internal network services and cloud metadata endpoints.
Details
The vulnerability is an inconsistency between two URL-handling flows in MediaUploadService.
Vulnerable path (external-link):
MediaUploadV2Controller::externalLink() at src/Core/Content/Media/Api/MediaUploadV2Controller.php:66 takes a user-supplied url parameter and passes it to MediaUploadService::linkURL() at src/Core/Content/Media/Upload/MediaUploadService.php:134.
linkURL() calls getContentSizeFromValidExternalUrl($url) at line 159, which only validates via validateExternalUrl():
// src/Core/Content/Media/Upload/MediaUploadService.php:207-212
public static function validateExternalUrl(string $url): void
{
if (!preg_match('/^https?:\/\/.+/', $url)) {
throw MediaException::invalidUrl($url);
}
}
Then makes a server-side HEAD request with no IP filtering:
// src/Core/Content/Media/Upload/MediaUploadService.php:292-300
private function getContentSizeFromValidExternalUrl(string $url): int
{
$this->validateExternalUrl($url);
$headers = $this->httpClient->request('HEAD', $url)->getHeaders();
if (!\array_key_exists('content-length', $headers)) {
throw MediaException::fileNotFound($url);
}
return (int) $headers['content-length'][0];
}
Protected path (upload_by_url):
In contrast, uploadFromURL uses FileFetcher::fetchFromURL() which calls FileUrlValidator::isValid():
// src/Core/Content/Media/File/FileFetcher.php:64
if ($this->enableUrlValidation && !$this->fileUrlValidator->isValid($url)) {
throw MediaException::illegalUrl($url);
}
FileUrlValidator::isValid() resolves the hostname via gethostbyname() and validates the IP against private and reserved ranges using filter_var() with FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE. This protection is entirely absent from the linkURL flow.
Impact
An authenticated admin user can:
- Probe cloud metadata services — HEAD requests to
169.254.169.254reveal whether cloud metadata endpoints exist and leak content-length values - Scan internal networks — Differentiate open/closed/filtered ports on internal hosts (10.x, 172.16.x, 192.168.x) based on response timing and error types
- Leak internal service information — The
fileSizefield stored in the database reflects thecontent-lengthheader from internal services - Redirect-based escalation — Symfony HttpClient follows redirects by default (max_redirects=20), allowing an attacker-controlled external server to redirect the HEAD request to arbitrary internal destinations
Impact is limited to information disclosure via HEAD requests. The admin authentication requirement (PR:H) reduces exploitability, but in multi-tenant or compromised-credential scenarios this allows network reconnaissance from the server's perspective.
Recommended Fix
Apply FileUrlValidator to the linkURL flow, consistent with the uploadFromURL flow. In MediaUploadService:
// src/Core/Content/Media/Upload/MediaUploadService.php
// Add constructor dependency:
private readonly FileUrlValidatorInterface $fileUrlValidator;
// In getContentSizeFromValidExternalUrl(), add IP validation:
private function getContentSizeFromValidExternalUrl(string $url): int
{
$this->validateExternalUrl($url);
if (!$this->fileUrlValidator->isValid($url)) {
throw MediaException::illegalUrl($url);
}
$headers = $this->httpClient->request('HEAD', $url)->getHeaders();
if (!\array_key_exists('content-length', $headers)) {
throw MediaException::fileNotFound($url);
}
return (int) $headers['content-length'][0];
}
Additionally, consider setting max_redirects: 0 on the HttpClient request to prevent redirect-based SSRF bypasses.
{
"affected": [
{
"package": {
"ecosystem": "Packagist",
"name": "shopware/core"
},
"ranges": [
{
"events": [
{
"introduced": "6.7.0.0"
},
{
"fixed": "6.7.10.1"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "Packagist",
"name": "shopware/platform"
},
"ranges": [
{
"events": [
{
"introduced": "6.7.0.0"
},
{
"fixed": "6.7.10.1"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-48013"
],
"database_specific": {
"cwe_ids": [
"CWE-918"
],
"github_reviewed": true,
"github_reviewed_at": "2026-06-04T19:36:07Z",
"nvd_published_at": null,
"severity": "MODERATE"
},
"details": "## Summary\n\nThe `/api/_action/media/external-link` endpoint allows authenticated admin users to make server-side HTTP HEAD requests to arbitrary internal IP addresses. While the parallel `uploadFromURL` flow validates target IPs against private/reserved ranges via `FileUrlValidator`, the `linkURL` flow only performs a URL format check (regex for `http://` or `https://` prefix), allowing SSRF to internal network services and cloud metadata endpoints.\n\n## Details\n\nThe vulnerability is an inconsistency between two URL-handling flows in `MediaUploadService`.\n\n**Vulnerable path** (`external-link`):\n\n`MediaUploadV2Controller::externalLink()` at `src/Core/Content/Media/Api/MediaUploadV2Controller.php:66` takes a user-supplied `url` parameter and passes it to `MediaUploadService::linkURL()` at `src/Core/Content/Media/Upload/MediaUploadService.php:134`.\n\n`linkURL()` calls `getContentSizeFromValidExternalUrl($url)` at line 159, which only validates via `validateExternalUrl()`:\n\n```php\n// src/Core/Content/Media/Upload/MediaUploadService.php:207-212\npublic static function validateExternalUrl(string $url): void\n{\n if (!preg_match(\u0027/^https?:\\/\\/.+/\u0027, $url)) {\n throw MediaException::invalidUrl($url);\n }\n}\n```\n\nThen makes a server-side HEAD request with no IP filtering:\n\n```php\n// src/Core/Content/Media/Upload/MediaUploadService.php:292-300\nprivate function getContentSizeFromValidExternalUrl(string $url): int\n{\n $this-\u003evalidateExternalUrl($url);\n\n $headers = $this-\u003ehttpClient-\u003erequest(\u0027HEAD\u0027, $url)-\u003egetHeaders();\n if (!\\array_key_exists(\u0027content-length\u0027, $headers)) {\n throw MediaException::fileNotFound($url);\n }\n\n return (int) $headers[\u0027content-length\u0027][0];\n}\n```\n\n**Protected path** (`upload_by_url`):\n\nIn contrast, `uploadFromURL` uses `FileFetcher::fetchFromURL()` which calls `FileUrlValidator::isValid()`:\n\n```php\n// src/Core/Content/Media/File/FileFetcher.php:64\nif ($this-\u003eenableUrlValidation \u0026\u0026 !$this-\u003efileUrlValidator-\u003eisValid($url)) {\n throw MediaException::illegalUrl($url);\n}\n```\n\n`FileUrlValidator::isValid()` resolves the hostname via `gethostbyname()` and validates the IP against private and reserved ranges using `filter_var()` with `FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE`. This protection is entirely absent from the `linkURL` flow.\n\n## Impact\n\nAn authenticated admin user can:\n\n1. **Probe cloud metadata services** \u2014 HEAD requests to `169.254.169.254` reveal whether cloud metadata endpoints exist and leak content-length values\n2. **Scan internal networks** \u2014 Differentiate open/closed/filtered ports on internal hosts (10.x, 172.16.x, 192.168.x) based on response timing and error types\n3. **Leak internal service information** \u2014 The `fileSize` field stored in the database reflects the `content-length` header from internal services\n4. **Redirect-based escalation** \u2014 Symfony HttpClient follows redirects by default (max_redirects=20), allowing an attacker-controlled external server to redirect the HEAD request to arbitrary internal destinations\n\nImpact is limited to information disclosure via HEAD requests. The admin authentication requirement (PR:H) reduces exploitability, but in multi-tenant or compromised-credential scenarios this allows network reconnaissance from the server\u0027s perspective.\n\n## Recommended Fix\n\nApply `FileUrlValidator` to the `linkURL` flow, consistent with the `uploadFromURL` flow. In `MediaUploadService`:\n\n```php\n// src/Core/Content/Media/Upload/MediaUploadService.php\n\n// Add constructor dependency:\nprivate readonly FileUrlValidatorInterface $fileUrlValidator;\n\n// In getContentSizeFromValidExternalUrl(), add IP validation:\nprivate function getContentSizeFromValidExternalUrl(string $url): int\n{\n $this-\u003evalidateExternalUrl($url);\n\n if (!$this-\u003efileUrlValidator-\u003eisValid($url)) {\n throw MediaException::illegalUrl($url);\n }\n\n $headers = $this-\u003ehttpClient-\u003erequest(\u0027HEAD\u0027, $url)-\u003egetHeaders();\n if (!\\array_key_exists(\u0027content-length\u0027, $headers)) {\n throw MediaException::fileNotFound($url);\n }\n\n return (int) $headers[\u0027content-length\u0027][0];\n}\n```\n\nAdditionally, consider setting `max_redirects: 0` on the HttpClient request to prevent redirect-based SSRF bypasses.",
"id": "GHSA-gq96-5pfx-f4vc",
"modified": "2026-06-04T19:36:07Z",
"published": "2026-06-04T19:36:07Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/shopware/shopware/security/advisories/GHSA-gq96-5pfx-f4vc"
},
{
"type": "PACKAGE",
"url": "https://github.com/shopware/shopware"
},
{
"type": "WEB",
"url": "https://github.com/shopware/shopware/releases/tag/v6.7.10.1"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:C/C:L/I:N/A:N",
"type": "CVSS_V3"
}
],
"summary": "Shopware: SSRF in Media External-Link Endpoint Bypasses IP Validation"
}
Sightings
| Author | Source | Type | Date | Other |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.