VYPR
High severity7.7NVD Advisory· Published Jun 11, 2026· Updated Jun 11, 2026

CVE-2026-47170

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

Patches

1
076b6d70a43d

Fix SSRF vulnerability in uploadFromUrl reported by Naif aka kn1ph

https://github.com/garlic-signage/garlic-hubsagiadinosMay 17, 2026via nvd-ref
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

2

News mentions

0

No linked articles in our index yet.