Skip to main content
Version: 2026.2

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.