CVE-2026-47170
Description
An SSRF vulnerability in Garlic-Hub's uploadFromUrl endpoint lets authenticated users scan internal services and access internal HTTP responses stored in the public media pool.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
An SSRF vulnerability in Garlic-Hub's uploadFromUrl endpoint lets authenticated users scan internal services and access internal HTTP responses stored in the public media pool.
Vulnerability
An authenticated server-side request forgery (SSRF) vulnerability exists in the uploadFromUrl endpoint of Garlic-Hub, a self-hosted digital signage management interface, prior to version 1.1. The endpoint fails to validate or restrict the URLs provided by users, allowing the server to issue arbitrary HTTP requests to internal services. This affects all versions before the patch in commit 076b6d7 [1][2].
Exploitation
An attacker with valid authentication to the Garlic-Hub web interface can supply a crafted URL (e.g., http://127.0.0.1:8080 or http://internal.server/) to the uploadFromUrl endpoint. The server then makes an HTTP request to that URL using the GuzzleHttp client. No user interaction beyond authentication is required, and the attacker does not need any special privileges beyond a standard user account [2].
Impact
Successful exploitation allows the attacker to perform internal port scanning, fingerprint internal services, and retrieve HTTP responses from internal hosts. These responses are stored in the publicly accessible media pool, making them visible to anyone with access to that pool. This can lead to disclosure of sensitive information about the internal network and services [1][2].
Mitigation
The vulnerability is patched in Garlic-Hub version 1.1. The fix introduces an SsrfValidator class that validates and resolves URLs before making requests, preventing requests to private IP ranges. Users should upgrade to the latest version. If upgrading is not immediately possible, the recommended workaround is to disable the uploadFromUrl endpoint entirely by blocking access to it at the web server or application level [2].
AI Insight generated on Jun 11, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected products
2(expand)+ 1 more
- (no CPE)
- (no CPE)range: <1.1
Patches
1076b6d70a43dFix SSRF vulnerability in uploadFromUrl reported by Naif aka kn1ph
4 files changed · +75 −6
config/services/mediapool.php+6 −0 modified@@ -39,6 +39,7 @@ use App\Modules\Mediapool\Services\UploadService; use App\Modules\Mediapool\Utils\ImagickFactory; use App\Modules\Mediapool\Utils\MediaHandlerFactory; +use App\Modules\Mediapool\Utils\SsrfValidator; use App\Modules\Mediapool\Utils\ZipFilesystemFactory; use GuzzleHttp\Client; use Psr\Container\ContainerInterface; @@ -85,6 +86,10 @@ ) ); }); +$dependencies[SsrfValidator::class] = DI\factory(function (ContainerInterface $container) { + return new SsrfValidator(); +}); + $dependencies[UploadService::class] = DI\factory(function (ContainerInterface $container) { return new UploadService( @@ -93,6 +98,7 @@ new Client(), new FilesRepository($container->get('SqlConnection')), $container->get(MimeTypeService::class), + $container->get(SsrfValidator::class), $container->get('ModuleLogger') ); });
src/Modules/Mediapool/Services/UploadService.php+7 −2 modified@@ -27,6 +27,7 @@ use App\Modules\Mediapool\Repositories\FilesRepository; use App\Modules\Mediapool\Utils\AbstractMediaHandler; use App\Modules\Mediapool\Utils\MediaHandlerFactory; +use App\Modules\Mediapool\Utils\SsrfValidator; use Doctrine\DBAL\Exception; use GuzzleHttp\Client; use GuzzleHttp\Exception\GuzzleException; @@ -50,6 +51,7 @@ public function __construct( private Client $client, private FilesRepository $mediaRepository, private MimeTypeService $mimeTypeDetector, + private SsrfValidator $ssrfValidator, private LoggerInterface $logger) { } @@ -121,8 +123,11 @@ public function uploadExternalMedia(int $nodeId, int $UID, string $externalLink, { try { + $this->checkNodeUploadable($nodeId, $UID); - $response = $this->client->head($externalLink); + $resolved = $this->ssrfValidator->validateAndResolveUrl($externalLink); + $curlOpts = ['curl' => [CURLOPT_RESOLVE => ["{$resolved['host']}:{$resolved['port']}:{$resolved['ip']}"]]]; + $response = $this->client->head($externalLink, $curlOpts); $preMimeType = $response->getHeaderLine('Content-Type'); // workaround if no content-type sent if ($preMimeType === '' && (str_contains($externalLink, 'mp4') || @@ -132,7 +137,7 @@ public function uploadExternalMedia(int $nodeId, int $UID, string $externalLink, $contentLength = $response->getHeaderLine('Content-Length'); $mediaHandler = $this->mediaHandlerFactory->createMediaHandler($preMimeType); $mediaHandler->checkFileBeforeUpload((int) $contentLength); - $uploadPath = $mediaHandler->uploadFromExternal($this->client, $externalLink); + $uploadPath = $mediaHandler->uploadFromExternal($this->client, $externalLink, $curlOpts); $ret = $this->insertDataset($mediaHandler, $uploadPath, $nodeId, $UID, $extMetadata); }
src/Modules/Mediapool/Utils/AbstractMediaHandler.php+7 −4 modified@@ -22,7 +22,6 @@ namespace App\Modules\Mediapool\Utils; use App\Framework\Core\Config\Config; -use App\Framework\Exceptions\CoreException; use App\Framework\Exceptions\ModuleException; use GuzzleHttp\Client; use GuzzleHttp\Exception\GuzzleException; @@ -63,7 +62,6 @@ abstract class AbstractMediaHandler /** * @param Config $config * @param Filesystem $filesystem - * @throws CoreException */ public function __construct(Config $config, Filesystem $filesystem) { @@ -157,9 +155,10 @@ public function uploadFromLocal(UploadedFileInterface $uploadedFile): string } /** + * @param array{curl: array<int, mixed>} $curlOpts * @throws GuzzleException */ - public function uploadFromExternal(Client $client, string $fileURI): string + public function uploadFromExternal(Client $client, string $fileURI, array $curlOpts): string { /** @var array{path: string} $parsedUrl */ $parsedUrl = parse_url($fileURI); @@ -168,7 +167,7 @@ public function uploadFromExternal(Client $client, string $fileURI): string $pathInfo = pathinfo($parsedUrl['path']); $targetPath = strtolower('/'. $this->originalPath .'/'. $pathInfo['basename']); - $client->request('GET', $fileURI, ['sink' => $this->getAbsolutePath($targetPath)]); + $client->request('GET', $fileURI, array_merge(['sink' => $this->getAbsolutePath($targetPath)], $curlOpts)); return $targetPath; } @@ -201,6 +200,10 @@ public function determineNewFilePath(string $oldFilePath, string $filehash, stri return $fileInfo['dirname']. '/'.$filehash.'.'.$ext; } + /** + * @throws ModuleException + * @throws FilesystemException + */ public function writeBinaryString(string $filePath, string $binaryContent): void { if ($binaryContent === '')
src/Modules/Mediapool/Utils/SsrfValidator.php+55 −0 added@@ -0,0 +1,55 @@ +<?php +/* + garlic-hub: Digital Signage Management Platform + + Copyright (C) 2026 Nikolaos Sagiadinos <garlic@saghiadinos.de> + This file is part of the garlic-hub source code + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License, version 3, + as published by the Free Software Foundation. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ +declare(strict_types=1); + + +namespace App\Modules\Mediapool\Utils; + +class SsrfValidator +{ + /** + * @return array{ip: string, host: string, port: int, path: string} + */ + public function validateAndResolveUrl(string $url): array + { + $parsed = parse_url($url); + + if (!isset($parsed['scheme'], $parsed['host'])) + throw new \InvalidArgumentException('Invalid URL.'); + + if (!in_array($parsed['scheme'], ['https', 'http'], true)) + throw new \InvalidArgumentException('URL scheme not allowed.'); + + $host = $parsed['host']; + $port = $parsed['port'] ?? ($parsed['scheme'] === 'https' ? 443 : 80); + $ip = gethostbyname($host); + + if ($ip === $host) + throw new \InvalidArgumentException('DNS resolution failed.'); + + if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) === false) + throw new \InvalidArgumentException('URL resolves to forbidden IP range.'); + + if (str_starts_with($ip, '169.254.')) + throw new \InvalidArgumentException('URL resolves to forbidden IP range.'); + + return ['ip' => $ip, 'host' => $host, 'port' => $port, 'path' => $parsed['path'] ?? '/']; + } +} \ No newline at end of file
Vulnerability mechanics
Root cause
"Missing server-side validation of the target URL in the uploadFromUrl endpoint allows the server to make HTTP requests to arbitrary internal addresses."
Attack vector
An authenticated attacker submits a URL pointing to an internal service (e.g., `http://127.0.0.1:8080/admin`) via the `uploadFromUrl` endpoint. The server fetches that URL and stores the response in the publicly accessible media pool, allowing the attacker to scan internal ports, fingerprint services, and retrieve sensitive internal HTTP responses. This is a classic Server-Side Request Forgery (SSRF) attack.
Affected code
The vulnerability resides in the `uploadFromUrl` endpoint, specifically in `UploadService::uploadExternalMedia()` which called `AbstractMediaHandler::uploadFromExternal()` without validating the target URL. The patch introduces a new `SsrfValidator` class and integrates it into the upload flow to block requests to internal IP ranges.
What the fix does
The patch adds a new `SsrfValidator` class that parses the URL, resolves the hostname to an IP, and rejects the request if the IP falls within private or link-local ranges (`FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE` and a check for `169.254.`). It also uses `CURLOPT_RESOLVE` to pin the resolved IP, preventing DNS rebinding attacks. The validator is injected into `UploadService` and its result is passed as cURL options to both the HEAD and GET requests.
Preconditions
- authThe attacker must have a valid authenticated session on the Garlic-Hub instance.
- networkThe attacker must be able to reach the Garlic-Hub server over the network.
- inputThe attacker provides a URL with an internal or loopback IP address as the `externalLink` parameter.
Generated on Jun 11, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
2News mentions
0No linked articles in our index yet.