diff --git a/README.md b/README.md index a68344f2b..87757d246 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,39 @@ You don't need to have a PHP environment with composer installed. * Pimcore Studio, using the credentials from `.env` (default `admin` / `admin`): +## Symfony profiler (dev environment) + +In the `dev` environment (`APP_ENV=dev`) the skeleton ships with two safe defaults for the Symfony web profiler: + +* The **debug toolbar is off**. +* The **`/_profiler/phpinfo` page is blocked**. It prints `phpinfo()` - including environment variables and secrets - to + anyone who can reach the instance, and Symfony has no setting to switch it off separately. + +The profiler itself keeps collecting data and stays available at `/_profiler`. + +Only activate the following on instances that untrusted users cannot reach, and never in production (use `APP_ENV=prod` +there). After each change, clear the cache - the change does not show up before that - and reload the page: +`bin/console cache:clear` (with Docker: `docker compose exec php bin/console cache:clear`). + +### Activate the toolbar + +Set `web_profiler.toolbar` to `true` in `config/packages/dev/config.yaml`: + +```yaml +web_profiler: + toolbar: true +``` + +### Activate the `/_profiler/phpinfo` page + +Delete the `_profiler_phpinfo` route (including its comment) at the end of `config/routes/dev/routes.yaml`, so that only +the `_pimcore_dev` import remains: + +```yaml +_pimcore_dev: + resource: "@PimcoreCoreBundle/config/routing_dev.yaml" +``` + ## Pimcore Platform Version By default, Pimcore Platform Version is added as a dependency which ensures installation of compatible and in combination with each other tested versions of additional Pimcore modules. More information about the Platform Version can be found in the diff --git a/config/packages/dev/config.yaml b/config/packages/dev/config.yaml index d77183c87..3d89b4a1e 100644 --- a/config/packages/dev/config.yaml +++ b/config/packages/dev/config.yaml @@ -1,2 +1,7 @@ imports: - { resource: ../../config.yaml } + +# The Symfony debug toolbar is off by default, so a dev instance that is reachable from the network does not +# advertise the profiler on every page. Set it to `true` locally when you want it. +web_profiler: + toolbar: false diff --git a/config/routes/dev/routes.yaml b/config/routes/dev/routes.yaml index 0bec5d898..cb01f7785 100644 --- a/config/routes/dev/routes.yaml +++ b/config/routes/dev/routes.yaml @@ -1,2 +1,11 @@ _pimcore_dev: resource: "@PimcoreCoreBundle/config/routing_dev.yaml" + +# Symfony's profiler route `/_profiler/phpinfo` prints phpinfo() - including environment variables and secrets - to +# anyone who can reach the application, and it is not affected by `web_profiler.toolbar`. Redefining the route +# under the same name replaces it with one that never matches, so the URL falls through to the profiler's token +# route, which only reports an unknown token, while the rest of the profiler keeps working. Delete this route to +# get the page back. +_profiler_phpinfo: + path: /_profiler/phpinfo + condition: "false" diff --git a/tests/Functional/Dev/ProfilerTest.php b/tests/Functional/Dev/ProfilerTest.php new file mode 100644 index 000000000..21abac24a --- /dev/null +++ b/tests/Functional/Dev/ProfilerTest.php @@ -0,0 +1,130 @@ +useTestDatabase(); + + // The profiler is only available in the dev environment, so boot that one instead of the test environment. + self::bootKernel(['environment' => 'dev', 'debug' => true]); + } + + protected function tearDown(): void + { + parent::tearDown(); + + $this->restoreDatabaseUrl(); + } + + public function testPhpinfoRouteIsNotMatched(): void + { + $match = self::$kernel->getContainer()->get('router')->match('/_profiler/phpinfo'); + + // The URL must fall through to the profiler's token route instead of reaching the phpinfo action. + self::assertNotSame('_profiler_phpinfo', $match['_route']); + self::assertSame('_profiler', $match['_route']); + self::assertSame('phpinfo', $match['token']); + } + + public function testPhpinfoIsNotServed(): void + { + $response = self::$kernel->handle(Request::create('/_profiler/phpinfo')); + $content = (string) $response->getContent(); + + // The profiler's token route answers instead, reporting an unknown token. + self::assertStringNotContainsString('PHP Version', $content); + self::assertStringNotContainsString('PHP Credits', $content); + self::assertStringContainsString('phpinfo', $content); + } + + public function testProfilerRoutesStillWork(): void + { + $router = self::$kernel->getContainer()->get('router'); + + // Only phpinfo is blocked: the profiler itself and its links to the phpinfo route keep working. + self::assertSame('_profiler_search_results', $router->match('/_profiler/abc123/search/results')['_route']); + self::assertSame('/_profiler/phpinfo', $router->generate('_profiler_phpinfo')); + } + + public function testToolbarIsDisabledByDefault(): void + { + $container = self::$kernel->getContainer(); + + // The parameter is only registered while the toolbar (or redirect interception) is enabled. + $enabled = $container->hasParameter('web_profiler.debug_toolbar.mode') + && WebDebugToolbarListener::ENABLED === $container->getParameter('web_profiler.debug_toolbar.mode'); + + self::assertFalse($enabled); + } + + /** + * The dev environment does not apply the test database override of config/packages/test, so point the + * default connection at the test database explicitly instead of the development one. + */ + private function useTestDatabase(): void + { + $dsn = $_SERVER['PIMCORE_TEST_DB_DSN'] ?? $_ENV['PIMCORE_TEST_DB_DSN'] ?? getenv('PIMCORE_TEST_DB_DSN'); + if (!is_string($dsn) || '' === $dsn) { + return; + } + + $processValue = getenv('DATABASE_URL'); + $this->originalDatabaseUrl = [ + 'server' => array_key_exists('DATABASE_URL', $_SERVER) ? ['value' => $_SERVER['DATABASE_URL']] : null, + 'env' => array_key_exists('DATABASE_URL', $_ENV) ? ['value' => $_ENV['DATABASE_URL']] : null, + 'process' => false === $processValue ? null : ['value' => $processValue], + ]; + + $_SERVER['DATABASE_URL'] = $_ENV['DATABASE_URL'] = $dsn; + putenv('DATABASE_URL=' . $dsn); + } + + private function restoreDatabaseUrl(): void + { + if (null === $this->originalDatabaseUrl) { + return; + } + + if (null === $this->originalDatabaseUrl['server']) { + unset($_SERVER['DATABASE_URL']); + } else { + $_SERVER['DATABASE_URL'] = $this->originalDatabaseUrl['server']['value']; + } + + if (null === $this->originalDatabaseUrl['env']) { + unset($_ENV['DATABASE_URL']); + } else { + $_ENV['DATABASE_URL'] = $this->originalDatabaseUrl['env']['value']; + } + + putenv('DATABASE_URL' . (null === $this->originalDatabaseUrl['process'] ? '' : '=' . $this->originalDatabaseUrl['process']['value'])); + + $this->originalDatabaseUrl = null; + } +}