From 683c10c892b2cabe1189c9da90d7ee20a762f691 Mon Sep 17 00:00:00 2001 From: Stefan Hagspiel Date: Mon, 16 Sep 2024 08:31:55 +0200 Subject: [PATCH] add asset protection --- README.md | 36 ++++-- composer.json | 2 +- config/pimcore/config.yaml | 6 + config/services.yaml | 2 +- config/services/.gitkeep | 1 - config/services/maintenance.yaml | 14 +++ config/services/messenger.yaml | 11 ++ src/DependencyInjection/Configuration.php | 42 +++++-- src/Flysystem/Adapter/SecuredAdapter.php | 14 ++- .../ProtectPublicDirectoriesTask.php | 77 ++++++++++++ src/Messenger/Middleware/GuardMiddleware.php | 116 ++++++++++++++++++ 11 files changed, 302 insertions(+), 19 deletions(-) create mode 100644 config/pimcore/config.yaml delete mode 100644 config/services/.gitkeep create mode 100644 config/services/maintenance.yaml create mode 100644 config/services/messenger.yaml create mode 100644 src/Maintenance/ProtectPublicDirectoriesTask.php create mode 100644 src/Messenger/Middleware/GuardMiddleware.php diff --git a/README.md b/README.md index 26640a83..3a5e660b 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ | Release | Supported Pimcore Versions | Supported Symfony Versions | Release Date | Maintained | Branch | |---------|----------------------------|----------------------------|--------------|----------------|--------| -| **1.x** | `^11.2` | `6.2` | -- | Feature Branch | master | +| **1.x** | `^11.3` | `6.2` | -- | Feature Branch | master | ## Installation @@ -44,6 +44,8 @@ Encrypt/Decrypt assets on the fly! ## Configuration +### File Encryption + ```yaml secure_storage: encrypter: @@ -54,12 +56,8 @@ secure_storage: secured_fly_system_storages: # form builder (if you want to encrypt form builder data) - - - storage: form_builder.chunk.storage - paths: null - - - storage: form_builder.files.storage - paths: null + - storage: form_builder.chunk.storage + - storage: form_builder.files.storage # pimcore - @@ -69,11 +67,33 @@ secure_storage: - /formdata ``` -## Custom Encrypter +#### Custom Encrypter TBD *** +### Asset Protection + +```yaml +secure_storage: + + pimcore_asset_protection: + + # protects: + # - public/var/assets [pimcore.asset.storage] + # - public/tmp/asset-cache [pimcore.asset_cache.storage] + # - public/tmp/thumbnails [pimcore.thumbnail.storage] + htaccess_protection_public_directories: + paths: + - /secure-storage + + omit_backend_search_indexing: + paths: + - /secure-storage +``` + +*** + ## Copyright and license Copyright: [DACHCOM.DIGITAL](http://dachcom-digital.ch) For licensing details please visit [LICENSE.md](LICENSE.md) diff --git a/composer.json b/composer.json index cf1d6dcb..20d18bcc 100755 --- a/composer.json +++ b/composer.json @@ -31,7 +31,7 @@ } }, "require": { - "pimcore/pimcore": "^11.0" + "pimcore/pimcore": "^11.3" }, "require-dev": { "codeception/codeception": "^5.0", diff --git a/config/pimcore/config.yaml b/config/pimcore/config.yaml new file mode 100644 index 00000000..168e284e --- /dev/null +++ b/config/pimcore/config.yaml @@ -0,0 +1,6 @@ +framework: + messenger: + buses: + messenger.bus.pimcore-core: + middleware: + - SecureStorageBundle\Messenger\Middleware\GuardMiddleware diff --git a/config/services.yaml b/config/services.yaml index ee78c90f..98761ca7 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -1,2 +1,2 @@ imports: - - { resource: services/*.yaml } \ No newline at end of file + - { resource: services/*.yaml } diff --git a/config/services/.gitkeep b/config/services/.gitkeep deleted file mode 100644 index 8b137891..00000000 --- a/config/services/.gitkeep +++ /dev/null @@ -1 +0,0 @@ - diff --git a/config/services/maintenance.yaml b/config/services/maintenance.yaml new file mode 100644 index 00000000..61b4736c --- /dev/null +++ b/config/services/maintenance.yaml @@ -0,0 +1,14 @@ +services: + _defaults: + autowire: true + autoconfigure: true + public: false + + SecureStorageBundle\Maintenance\ProtectPublicDirectoriesTask: + autowire: true + autoconfigure: true + arguments: + $secureStorageConfig: '%pimcore.secure_storage.config%' + $locator: !tagged_locator { tag: flysystem.storage } + tags: + - { name: pimcore.maintenance.task, type: secure_storage_protect_public_directories } \ No newline at end of file diff --git a/config/services/messenger.yaml b/config/services/messenger.yaml new file mode 100644 index 00000000..e2535078 --- /dev/null +++ b/config/services/messenger.yaml @@ -0,0 +1,11 @@ +services: + _defaults: + autowire: true + autoconfigure: true + public: false + + SecureStorageBundle\Messenger\Middleware\GuardMiddleware: + arguments: + $secureStorageConfig: '%pimcore.secure_storage.config%' + tags: + - { name: messenger.middleware } diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php index 0d060eac..a232f3ab 100644 --- a/src/DependencyInjection/Configuration.php +++ b/src/DependencyInjection/Configuration.php @@ -22,22 +22,50 @@ public function getConfigTreeBuilder(): TreeBuilder ->variableNode('options')->defaultValue([])->end() ->end() ->end() + ->arrayNode('secured_fly_system_storages') ->prototype('array') + ->addDefaultsIfNotSet() + ->children() + ->scalarNode('storage') + ->isRequired() + ->validate() + ->ifNull() + ->thenInvalid('Invalid storage %s') + ->end() + ->end() + ->arrayNode('paths') + ->prototype('scalar')->end() + ->end() + ->booleanNode('allow_asset_preview_image_generation')->defaultFalse()->end() + ->booleanNode('allow_asset_update_preview_image_generation')->defaultFalse()->end() + ->booleanNode('allow_image_optimizing')->defaultFalse()->end() + ->end() + ->end() + ->end() + + ->arrayNode('pimcore_asset_protection') ->addDefaultsIfNotSet() ->children() - ->scalarNode('storage') - ->isRequired() - ->validate() - ->ifNull() - ->thenInvalid('Invalid storage %s') + ->arrayNode('htaccess_protection_public_directories') + ->addDefaultsIfNotSet() + ->children() + ->arrayNode('paths') + ->prototype('scalar')->end() + ->end() ->end() ->end() - ->arrayNode('paths') - ->prototype('scalar')->end() + ->arrayNode('omit_backend_search_indexing') + ->addDefaultsIfNotSet() + ->children() + ->arrayNode('paths') + ->prototype('scalar')->end() + ->end() + ->end() ->end() ->end() ->end() + ->end(); return $treeBuilder; diff --git a/src/Flysystem/Adapter/SecuredAdapter.php b/src/Flysystem/Adapter/SecuredAdapter.php index b3af509e..e3f2fec5 100644 --- a/src/Flysystem/Adapter/SecuredAdapter.php +++ b/src/Flysystem/Adapter/SecuredAdapter.php @@ -26,7 +26,7 @@ private function isSecuredPath(string $path): bool return true; } - foreach($this->securedPaths as $securedPath) { + foreach ($this->securedPaths as $securedPath) { if (str_starts_with($path, ltrim($securedPath, '/'))) { return true; } @@ -47,6 +47,12 @@ public function directoryExists(string $path): bool public function write(string $path, string $contents, Config $config): void { + if ($config->get('bypass_secured_adapter') === true) { + $this->inner->write($path, $contents, $config); + + return; + } + if (!$this->isSecuredPath($path)) { $this->inner->write($path, $contents, $config); @@ -64,6 +70,12 @@ public function write(string $path, string $contents, Config $config): void public function writeStream(string $path, $contents, Config $config): void { + if ($config->get('bypass_secured_adapter') === true) { + $this->inner->write($path, $contents, $config); + + return; + } + if (!$this->isSecuredPath($path)) { $this->inner->writeStream($path, $contents, $config); diff --git a/src/Maintenance/ProtectPublicDirectoriesTask.php b/src/Maintenance/ProtectPublicDirectoriesTask.php new file mode 100644 index 00000000..ecf94199 --- /dev/null +++ b/src/Maintenance/ProtectPublicDirectoriesTask.php @@ -0,0 +1,77 @@ +secureStorageConfig['pimcore_asset_protection']['htaccess_protection_public_directories']; + + if (count($assetProtectionConfig['paths']) === 0) { + return; + } + + foreach ($assetProtectionConfig['paths'] as $protectedPath) { + $this->protectPath($protectedPath); + } + } + + private function protectPath(string $protectedPath): void + { + $data = implode(PHP_EOL, $this->getProtectionLines()); + + $storages = [ + 'pimcore.asset.storage', + 'pimcore.asset_cache.storage', + 'pimcore.thumbnail.storage' + ]; + + foreach ($storages as $storageName) { + + $storage = $this->getStorage($storageName); + + if ($protectedPath === '/') { + + $secureFilePath = '.htaccess'; + if (!$storage->fileExists($secureFilePath)) { + $storage->write($secureFilePath, $data, ['bypass_secured_adapter' => true]); + } + + continue; + } + + $secureFilePath = sprintf('%s/.htaccess', $protectedPath); + if ($storage->directoryExists($protectedPath) && !$storage->fileExists($secureFilePath)) { + $storage->write($secureFilePath, $data, ['bypass_secured_adapter' => true]); + } + } + } + + private function getStorage(string $name): FilesystemOperator + { + return $this->locator->get($name); + } + + private function getProtectionLines(): array + { + $data = []; + + $data[] = 'RewriteEngine On'; + $data[] = 'RewriteCond %{HTTP_HOST}==%{HTTP_REFERER} !^(.*?)==https?://\1/admin/ [OR]'; + $data[] = 'RewriteCond %{HTTP_COOKIE} !^.*pimcore_admin_sid.*$ [NC]'; + $data[] = 'RewriteRule ^ - [L,F]'; + + return $data; + } +} diff --git a/src/Messenger/Middleware/GuardMiddleware.php b/src/Messenger/Middleware/GuardMiddleware.php new file mode 100644 index 00000000..a3128a5d --- /dev/null +++ b/src/Messenger/Middleware/GuardMiddleware.php @@ -0,0 +1,116 @@ +isGuardedMessage($envelope)) { + return $stack->next()->handle($envelope, $stack); + } + + return $envelope; + } + + private function isGuardedMessage(Envelope $envelope): bool + { + $omitMessage = match (get_class($envelope->getMessage())) { + SearchBackendMessage::class => $this->omitByBackendSearch($envelope), + //AssetPreviewImageMessage::class => false, + //AssetUpdateTasksMessage::class => false, + //OptimizeImageMessage::class => false, + default => false + }; + + return $omitMessage === true; + } + + private function omitByBackendSearch(Envelope $envelope): bool + { + $config = $this->secureStorageConfig['pimcore_asset_protection']['omit_backend_search_indexing']; + + if (count($config['paths']) === 0) { + return false; + } + + $path = $this->extractElementPath($envelope); + + if ($path === null) { + return false; + } + + return $this->checkPaths($path, $config['paths']); + } + + private function checkPaths(string $path, array $securePaths): bool + { + foreach ($securePaths as $securedPath) { + if (str_starts_with($path, $securedPath)) { + return true; + } + } + + return false; + } + + private function extractElementPath(Envelope $envelope): ?string + { + $asset = null; + + // restriction type: allow_backend_search_indexing + if (get_class($envelope->getMessage()) === SearchBackendMessage::class) { + /** @var SearchBackendMessage $message */ + $message = $envelope->getMessage(); + if ($message->getType() === 'asset') { + $asset = Asset::getById($message->getId()); + } + } + + // restriction type: allow_asset_preview_image_generation + if (get_class($envelope->getMessage()) === AssetPreviewImageMessage::class) { + /** @var AssetPreviewImageMessage $message */ + $message = $envelope->getMessage(); + $asset = Asset::getById($message->getId()); + } + + // restriction type: allow_asset_update_preview_image_generation + if (get_class($envelope->getMessage()) === AssetUpdateTasksMessage::class) { + /** @var AssetUpdateTasksMessage $message */ + $message = $envelope->getMessage(); + $asset = Asset::getById($message->getId()); + } + + // restriction type: allow_image_optimizing + if (get_class($envelope->getMessage()) === OptimizeImageMessage::class) { + /** @var OptimizeImageMessage $message */ + $message = $envelope->getMessage(); + $asset = sprintf('/%s', ltrim($message->getPath(), '/')); + } + + if (is_string($asset)) { + return $asset; + } + + if ($asset instanceof Asset) { + return $asset->getPath(); + } + + return null; + } +} \ No newline at end of file