nextcloud/server · warning · OCSBadRequestException

Untrusted media source

Error message

Untrusted media source

What it means

OCSBadRequestException (HTTP 400) thrown by DiscoverController::mediaImage when checkCanDownloadMedia($fileName) returns false. The discover-media proxy only fetches remote media from trusted sources, so a media URL outside the allowlist is rejected before any HTTP request is made — this is a deliberate SSRF guard, logged as a warning ('Tried to load media files ... from untrusted source').

Source

Thrown at apps/appstore/lib/Controller/DiscoverController.php:127

		if ($file === false) {
			$user = $session->getUser();
			// this route is not public thus we can assume a user is logged-in
			assert($user !== null);
			// Register a user request to throttle fetching external data
			// this will prevent using the server for DoS of other systems.
			$limiter->registerUserRequest(
				'settings-discover-media',
				// allow up to 24 media requests per hour
				// this should be a sane default when a completely new section is loaded
				// keep in mind browsers request all files from a source-set
				24,
				60 * 60,
				$user,
			);

			if (!$this->checkCanDownloadMedia($fileName)) {
				$this->logger->warning('Tried to load media files for app discover section from untrusted source');
				throw new OCSBadRequestException('Untrusted media source');
			}

			try {
				$client = $this->clientService->newClient();
				$fileResponse = $client->get($fileName);
				$contentType = $fileResponse->getHeader('Content-Type');
				$extension = $info['extension'] ?? '';
				$file = $folder->newFile($hashName . '.' . base64_encode($contentType) . '.' . $extension, $fileResponse->getBody());
			} catch (\Throwable $e) {
				$this->logger->warning('Could not load media file for app discover section', ['media_src' => $fileName, 'exception' => $e]);
				throw new OCSNotFoundException('Media file not found');
			}
		} else {
			// File was found so we can get the content type from the file name
			$contentType = base64_decode(explode('.', $file->getName())[1] ?? '');
		}

		$response = new FileDisplayResponse($file, Http::STATUS_OK, ['Content-Type' => $contentType]);

View on GitHub (pinned to ecdeb153ff)

Solutions

  1. Use media URLs that come from the official app store data (they are inside the trusted source list)
  2. If you operate a custom app store, make sure its media domain is included in the server's trusted source configuration for discover media
  3. As an app publisher, host release media on the appstore-provided locations rather than arbitrary hosts
  4. Do not attempt to use this endpoint as a generic image proxy — by design it will 400
Defensive patterns

Strategy: validation

Validate before calling

// Sender/publisher side: only reference trusted appstore media hosts
const TRUSTED = ['cdn.nextcloud.com', 'apps.nextcloud.com']
const host = new URL(mediaSrc).hostname
if (!TRUSTED.includes(host)) {
	// do not use this URL as media_src — the server will reject it with 'Untrusted media source'
}

Try / catch

try {
	const blob = await axios.get(mediaProxyUrl, { params: { fileName: mediaSrc } })
} catch (e) {
	if (e?.response?.status === 400) {
		// 'Untrusted media source' — SSRF guard: use a trusted appstore URL or adjust the server's trusted-source config
	} else throw e
}

Prevention

When it happens

Trigger: GET the discover media endpoint with a media_src whose host is not in the trusted set: an app release advertising screenshots on a custom/renamed CDN, a self-hosted or third-party app store whose domain is not allowlisted, or a manipulated request passing an arbitrary URL as fileName.

Common situations: Third-party appstore deployments whose media hosts are unknown to the server; app developers pointing media_src at their own servers; Nextcloud version changes tightening the allowlist after an upgrade.

Related errors


AI-assisted analysis of nextcloud/server@ecdeb153ff (2026-08-17). Data as JSON: /api/errors/e0dcb71d0da58c00. Report an issue: GitHub.