Skip to main content
Version: 2026.1

Restricting Public Asset Access

By default, all data stored as Pimcore assets is publicly accessible via its URL (e.g. https://mydomain.com/my-assetfolder/my-asset.jpg) without login or other access restriction. Confidential data must not be stored as Pimcore assets without additional protection measures.

This default exists for performance reasons: delivering an asset directly via the web server requires significantly fewer resources than starting a PHP process for every asset request (especially for thumbnails).

If you need to restrict access, Pimcore provides two options:

OptionApproachPerformance Impact
Option 1Block access entirely at the web server levelNone (requests never reach PHP)
Option 2Route requests through a controller for permission checksSignificant (PHP process per request)

Choose Option 1 when assets should never be accessible publicly. Choose Option 2 when you need to check permissions at runtime (e.g. user-specific access control).

Option 1 - Restricting Access Completely

All confidential assets must be stored within one (or a few) folders, e.g. within /protected (this is also required for Pimcore permissions).

Protected FolderProtected FolderProtected Folder

Apache

In the .htaccess of the project, restrict access to this folder with an additional rewrite rule. Place this rule before the rewrite rule for asset delivery:

...
RewriteRule ^protected/.* - [F,L]
RewriteRule ^var/.*/protected(.*) - [F,L]
RewriteRule ^cache-buster\-[\d]+/protected(.*) - [F,L]

# ASSETS: check if request method is GET (because of WebDAV) and if the requested file (asset) exists on the filesystem, if both match, deliver the asset directly
...

Nginx

Add the following parts to your Nginx configuration directly after the index directive:

location ~ ^/protected/.* {
return 403;
}

location ~ ^/var/.*/protected(.*) {
return 403;
}

location ~ ^/cache-buster\-[\d]+/protected(.*) {
return 403;
}

A full configuration example can be found in the skeleton nginx.conf.

Because of this rule, all assets located within /protected (including all their thumbnails) are no longer delivered via the web server. As a consequence, direct download links and Pimcore-generated img tags for thumbnails cannot be used. All delivery of these assets must be done manually via a custom controller action.

Option 2 - Checking Permissions Before Delivery

This option does not restrict delivery entirely, but routes asset requests to a controller action that can check access permissions with custom business logic and then deliver the asset or not.

Again, all confidential assets must be stored within one (or a few) folders, e.g. within /protected.

Protected FolderProtected FolderProtected Folder

Apache

In the .htaccess of the project, route requests to assets in this folder to index.php. Place this rule before the rewrite rule for asset delivery:

...
RewriteRule ^protected/(.*) %{ENV:BASE}/index.php [L]
RewriteRule ^var/.*/protected(.*) - [F,L]
RewriteRule ^cache-buster\-[\d]+/protected(.*) - [F,L]

# ASSETS: check if request method is GET (because of WebDAV) and if the requested file (asset) exists on the filesystem, if both match, deliver the asset directly
...

Nginx

Add the following parts to your Nginx configuration directly after the index directive:

rewrite ^(/protected/.*) /index.php$is_args$args last;

location ~ ^/var/.*/protected(.*) {
return 403;
}

rewrite ^(/cache-buster-(?:\d+)/protected(?:.*)) /index.php$is_args$args last;

location ~ ^/cache-buster\-[\d]+/protected(.*) {
return 403;
}

A full configuration example can be found in the skeleton nginx.conf.

In the application, define a route in config/routes.yaml and a controller action that handles the request:

# config/routes.yaml

# important: these must be the top routes in the file!
asset_protect:
path: /protected/{path}
defaults: { _controller: App\Controller\MyAssetController::protectedAssetAction }
requirements:
path: '.*'

cache_buster_asset_protect:
path: /cache-buster-{id}/protected/{path}
defaults: { _controller: App\Controller\MyAssetController::protectedAssetAction }
requirements:
id: '\d+'
path: '.*'
<?php

namespace App\Controller;

use Pimcore\Controller\FrontendController;
use Pimcore\Model\Asset;
use Pimcore\Model\Asset\Service;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\StreamedResponse;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;

class MyAssetController extends FrontendController
{
public function protectedAssetAction(Request $request): Response
{
// IMPORTANT!
// Add your permission check here and throw AccessDeniedHttpException if denied.

// the following code is responsible to deliver asset & thumbnail contents
// modify it the way you need it for your use-case
$pathInfo = $request->getPathInfo();
$asset = Asset::getByPath($pathInfo);
if ($asset){
$stream = $asset->getStream();
return new StreamedResponse(function () use ($stream) {
fpassthru($stream);
}, 200, [
'Content-Type' => $asset->getMimeType(),
]);
} else {
return Asset\Service::getStreamedResponseByUri($pathInfo);
}

throw new AccessDeniedHttpException('Access denied.');
}
}

This option has significant impact on server load and delivery performance of assets (and thumbnails). It is not recommended to deliver all assets this way, only the confidential ones.