From 7785e65d0884f686768292c42860fe20a9ed4901 Mon Sep 17 00:00:00 2001 From: Michael Schiemer Date: Tue, 25 Nov 2025 15:01:40 +0100 Subject: [PATCH] fix(security): prevent debug error pages on staging/production Root cause: ExceptionHandlingInitializer attempted to autowire EnvironmentType directly, but it was never registered in the DI container. This caused the debug mode resolution to fail silently. Changes: - Use TypedConfiguration instead of EnvironmentType for proper DI - Create ErrorHandlingConfig value object to centralize config - Access debug mode via AppConfig.isDebugEnabled() which respects both APP_DEBUG env var AND EnvironmentType.isDebugEnabled() - Register ErrorHandlingConfig as singleton in container - Remove diagnostic logging from ResponseErrorRenderer This ensures that staging/production environments (where EnvironmentType != DEV) will not display stack traces, code context, or file paths in error responses. --- .../ExceptionHandling/ErrorHandlingConfig.php | 71 +++++++++++++++++++ .../ExceptionHandling/ErrorKernel.php | 18 +++-- .../ErrorRendererFactory.php | 20 ++++-- .../ExceptionHandlingInitializer.php | 28 +++++--- .../Renderers/ResponseErrorRenderer.php | 3 - 5 files changed, 117 insertions(+), 23 deletions(-) create mode 100644 src/Framework/ExceptionHandling/ErrorHandlingConfig.php diff --git a/src/Framework/ExceptionHandling/ErrorHandlingConfig.php b/src/Framework/ExceptionHandling/ErrorHandlingConfig.php new file mode 100644 index 00000000..40970d53 --- /dev/null +++ b/src/Framework/ExceptionHandling/ErrorHandlingConfig.php @@ -0,0 +1,71 @@ +isDebugEnabled(); + + return new self( + debugMode: $isDebug, + logErrors: true, + displayErrors: $isDebug + ); + } + + /** + * Create config for development environment + */ + public static function forDevelopment(): self + { + return new self( + debugMode: true, + logErrors: true, + displayErrors: true + ); + } + + /** + * Create config for production environment + */ + public static function forProduction(): self + { + return new self( + debugMode: false, + logErrors: true, + displayErrors: false + ); + } + + /** + * Check if debug information should be displayed + */ + public function shouldDisplayDebugInfo(): bool + { + return $this->debugMode && $this->displayErrors; + } +} diff --git a/src/Framework/ExceptionHandling/ErrorKernel.php b/src/Framework/ExceptionHandling/ErrorKernel.php index 0ea1e9c2..2c265722 100644 --- a/src/Framework/ExceptionHandling/ErrorKernel.php +++ b/src/Framework/ExceptionHandling/ErrorKernel.php @@ -25,6 +25,8 @@ use Throwable; */ final readonly class ErrorKernel { + private ErrorHandlingConfig $config; + public function __construct( private ErrorRendererFactory $rendererFactory, private ?Reporter $reporter, @@ -34,8 +36,10 @@ final readonly class ErrorKernel private ?ExceptionRateLimiter $rateLimiter = null, private ?ExecutionContext $executionContext = null, private ?ConsoleOutput $consoleOutput = null, - private bool $isDebugMode = false - ) {} + ?ErrorHandlingConfig $config = null + ) { + $this->config = $config ?? new ErrorHandlingConfig(); + } /** * Context-aware exception handler @@ -71,7 +75,7 @@ final readonly class ErrorKernel $this->errorAggregator->processError( $e, $this->contextProvider, - $this->isDebugMode + $this->config->debugMode ); } @@ -122,7 +126,7 @@ final readonly class ErrorKernel $this->errorAggregator->processError( $exception, $this->contextProvider, - $this->isDebugMode + $this->config->debugMode ); } } @@ -147,7 +151,7 @@ final readonly class ErrorKernel $this->errorAggregator->processError( $exception, $this->contextProvider, - $this->isDebugMode + $this->config->debugMode ); } } @@ -169,13 +173,13 @@ final readonly class ErrorKernel ?bool $isDebugMode = null ): HttpResponse { // Use provided debug mode or instance default - $debugMode = $isDebugMode ?? $this->isDebugMode; + $debugMode = $isDebugMode ?? $this->config->debugMode; // Get renderer from factory $renderer = $this->rendererFactory->getRenderer(); // If renderer is ResponseErrorRenderer and debug mode changed, create new one with correct debug mode - if ($renderer instanceof ResponseErrorRenderer && $debugMode !== $this->isDebugMode) { + if ($renderer instanceof ResponseErrorRenderer && $debugMode !== $this->config->debugMode) { $renderer = $this->rendererFactory->createHttpRenderer($debugMode); } diff --git a/src/Framework/ExceptionHandling/ErrorRendererFactory.php b/src/Framework/ExceptionHandling/ErrorRendererFactory.php index 3d669994..deab618d 100644 --- a/src/Framework/ExceptionHandling/ErrorRendererFactory.php +++ b/src/Framework/ExceptionHandling/ErrorRendererFactory.php @@ -16,12 +16,16 @@ use App\Framework\View\Engine; */ final readonly class ErrorRendererFactory { + private ErrorHandlingConfig $config; + public function __construct( private ExecutionContext $executionContext, private Engine $engine, private ?ConsoleOutput $consoleOutput = null, - private bool $isDebugMode = false - ) {} + ?ErrorHandlingConfig $config = null + ) { + $this->config = $config ?? new ErrorHandlingConfig(); + } /** * Get appropriate renderer for current execution context @@ -34,7 +38,7 @@ final readonly class ErrorRendererFactory return new ConsoleErrorRenderer($output); } - return new ResponseErrorRenderer($this->engine, $this->isDebugMode); + return new ResponseErrorRenderer($this->engine, $this->config->debugMode); } /** @@ -48,7 +52,15 @@ final readonly class ErrorRendererFactory */ public function createHttpRenderer(?bool $debugMode = null): ResponseErrorRenderer { - $debugMode = $debugMode ?? $this->isDebugMode; + $debugMode = $debugMode ?? $this->config->debugMode; return new ResponseErrorRenderer($this->engine, $debugMode); } + + /** + * Get the current error handling configuration + */ + public function getConfig(): ErrorHandlingConfig + { + return $this->config; + } } diff --git a/src/Framework/ExceptionHandling/ExceptionHandlingInitializer.php b/src/Framework/ExceptionHandling/ExceptionHandlingInitializer.php index 2a66abcf..40bdc542 100644 --- a/src/Framework/ExceptionHandling/ExceptionHandlingInitializer.php +++ b/src/Framework/ExceptionHandling/ExceptionHandlingInitializer.php @@ -4,7 +4,7 @@ declare(strict_types=1); namespace App\Framework\ExceptionHandling; -use App\Framework\Config\EnvironmentType; +use App\Framework\Config\TypedConfiguration; use App\Framework\Console\ConsoleOutput; use App\Framework\Context\ExecutionContext; use App\Framework\ErrorAggregation\ErrorAggregatorInterface; @@ -28,15 +28,25 @@ final readonly class ExceptionHandlingInitializer #[Initializer] public function initialize( Container $container, - EnvironmentType $environmentType, + TypedConfiguration $typedConfig, ExecutionContext $executionContext, Engine $engine, ?ConsoleOutput $consoleOutput = null, ?Logger $logger = null ): void { - $isDebugMode = $environmentType->isDebugEnabled(); - // DIAGNOSTIC: Log debug mode determination (remove after verification) - error_log("[ExceptionHandlingInitializer] environmentType={$environmentType->value}, isDebugEnabled={$isDebugMode}"); + // Get debug mode from AppConfig - this respects both APP_DEBUG env var AND EnvironmentType + $isDebugMode = $typedConfig->app->isDebugEnabled(); + $environmentType = $typedConfig->app->type; + + // Create centralized error handling configuration + $errorConfig = new ErrorHandlingConfig( + debugMode: $isDebugMode, + logErrors: true, + displayErrors: $isDebugMode + ); + + // Register ErrorHandlingConfig as singleton for other components + $container->singleton(ErrorHandlingConfig::class, $errorConfig); // ConsoleOutput - only create if CLI context and not already provided // For HTTP context, null is acceptable (ConsoleErrorRenderer won't be used) @@ -54,11 +64,11 @@ final readonly class ExceptionHandlingInitializer executionContext: $executionContext, engine: $engine, consoleOutput: $consoleOutput, // null for HTTP context, ConsoleOutput for CLI context - isDebugMode: $isDebugMode + config: $errorConfig )); - // ErrorKernel - bind singleton with explicit debug flag so HTTP renderers respect environment - $container->singleton(ErrorKernel::class, static function (Container $c) use ($isDebugMode, $executionContext, $consoleOutput): ErrorKernel { + // ErrorKernel - bind singleton with ErrorHandlingConfig so HTTP renderers respect environment + $container->singleton(ErrorKernel::class, static function (Container $c) use ($errorConfig, $executionContext, $consoleOutput): ErrorKernel { $errorAggregator = $c->has(ErrorAggregatorInterface::class) ? $c->get(ErrorAggregatorInterface::class) : null; $contextProvider = $c->has(ExceptionContextProvider::class) ? $c->get(ExceptionContextProvider::class) : null; $auditLogger = $c->has(ExceptionAuditLogger::class) ? $c->get(ExceptionAuditLogger::class) : null; @@ -73,7 +83,7 @@ final readonly class ExceptionHandlingInitializer rateLimiter: $rateLimiter, executionContext: $executionContext, consoleOutput: $consoleOutput, - isDebugMode: $isDebugMode + config: $errorConfig ); }); diff --git a/src/Framework/ExceptionHandling/Renderers/ResponseErrorRenderer.php b/src/Framework/ExceptionHandling/Renderers/ResponseErrorRenderer.php index 2d84ad38..52f482de 100644 --- a/src/Framework/ExceptionHandling/Renderers/ResponseErrorRenderer.php +++ b/src/Framework/ExceptionHandling/Renderers/ResponseErrorRenderer.php @@ -278,9 +278,6 @@ final readonly class ResponseErrorRenderer implements ErrorRenderer ); $debugInfo = ''; - // SECURITY FIX: Only show debug info in development mode - // Log to error_log for diagnostic purposes (can be removed after verification) - error_log("[ResponseErrorRenderer] isDebugMode={$this->isDebugMode}, APP_ENV=" . ($_ENV['APP_ENV'] ?? 'unknown')); if ($this->isDebugMode) { $debugInfo = $this->generateDebugSection($exception, $contextProvider); }