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').

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.

Appendix: 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)