fix(security): prevent debug error pages on staging/production
All checks were successful
Test Runner / test-basic (push) Successful in 8s
Test Runner / test-php (push) Successful in 7s
Deploy Application / deploy (push) Successful in 1m28s

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.
This commit is contained in:
2025-11-25 15:01:40 +01:00
parent 520d082393
commit 7785e65d08
5 changed files with 117 additions and 23 deletions

View File

@@ -0,0 +1,71 @@
<?php
declare(strict_types=1);
namespace App\Framework\ExceptionHandling;
use App\Framework\Config\EnvironmentType;
/**
* Configuration for the error handling system
*
* This value object centralizes all error handling configuration,
* ensuring debug mode and other settings are consistently passed
* through the error handling pipeline.
*/
final readonly class ErrorHandlingConfig
{
public function __construct(
public bool $debugMode = false,
public bool $logErrors = true,
public bool $displayErrors = false
) {}
/**
* Create config from EnvironmentType
*
* Uses the environment's debug setting to determine error display behavior.
*/
public static function fromEnvironment(EnvironmentType $environmentType): self
{
$isDebug = $environmentType->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;
}
}

View File

@@ -25,6 +25,8 @@ use Throwable;
*/ */
final readonly class ErrorKernel final readonly class ErrorKernel
{ {
private ErrorHandlingConfig $config;
public function __construct( public function __construct(
private ErrorRendererFactory $rendererFactory, private ErrorRendererFactory $rendererFactory,
private ?Reporter $reporter, private ?Reporter $reporter,
@@ -34,8 +36,10 @@ final readonly class ErrorKernel
private ?ExceptionRateLimiter $rateLimiter = null, private ?ExceptionRateLimiter $rateLimiter = null,
private ?ExecutionContext $executionContext = null, private ?ExecutionContext $executionContext = null,
private ?ConsoleOutput $consoleOutput = null, private ?ConsoleOutput $consoleOutput = null,
private bool $isDebugMode = false ?ErrorHandlingConfig $config = null
) {} ) {
$this->config = $config ?? new ErrorHandlingConfig();
}
/** /**
* Context-aware exception handler * Context-aware exception handler
@@ -71,7 +75,7 @@ final readonly class ErrorKernel
$this->errorAggregator->processError( $this->errorAggregator->processError(
$e, $e,
$this->contextProvider, $this->contextProvider,
$this->isDebugMode $this->config->debugMode
); );
} }
@@ -122,7 +126,7 @@ final readonly class ErrorKernel
$this->errorAggregator->processError( $this->errorAggregator->processError(
$exception, $exception,
$this->contextProvider, $this->contextProvider,
$this->isDebugMode $this->config->debugMode
); );
} }
} }
@@ -147,7 +151,7 @@ final readonly class ErrorKernel
$this->errorAggregator->processError( $this->errorAggregator->processError(
$exception, $exception,
$this->contextProvider, $this->contextProvider,
$this->isDebugMode $this->config->debugMode
); );
} }
} }
@@ -169,13 +173,13 @@ final readonly class ErrorKernel
?bool $isDebugMode = null ?bool $isDebugMode = null
): HttpResponse { ): HttpResponse {
// Use provided debug mode or instance default // Use provided debug mode or instance default
$debugMode = $isDebugMode ?? $this->isDebugMode; $debugMode = $isDebugMode ?? $this->config->debugMode;
// Get renderer from factory // Get renderer from factory
$renderer = $this->rendererFactory->getRenderer(); $renderer = $this->rendererFactory->getRenderer();
// If renderer is ResponseErrorRenderer and debug mode changed, create new one with correct debug mode // 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); $renderer = $this->rendererFactory->createHttpRenderer($debugMode);
} }

View File

@@ -16,12 +16,16 @@ use App\Framework\View\Engine;
*/ */
final readonly class ErrorRendererFactory final readonly class ErrorRendererFactory
{ {
private ErrorHandlingConfig $config;
public function __construct( public function __construct(
private ExecutionContext $executionContext, private ExecutionContext $executionContext,
private Engine $engine, private Engine $engine,
private ?ConsoleOutput $consoleOutput = null, private ?ConsoleOutput $consoleOutput = null,
private bool $isDebugMode = false ?ErrorHandlingConfig $config = null
) {} ) {
$this->config = $config ?? new ErrorHandlingConfig();
}
/** /**
* Get appropriate renderer for current execution context * Get appropriate renderer for current execution context
@@ -34,7 +38,7 @@ final readonly class ErrorRendererFactory
return new ConsoleErrorRenderer($output); 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 public function createHttpRenderer(?bool $debugMode = null): ResponseErrorRenderer
{ {
$debugMode = $debugMode ?? $this->isDebugMode; $debugMode = $debugMode ?? $this->config->debugMode;
return new ResponseErrorRenderer($this->engine, $debugMode); return new ResponseErrorRenderer($this->engine, $debugMode);
} }
/**
* Get the current error handling configuration
*/
public function getConfig(): ErrorHandlingConfig
{
return $this->config;
}
} }

View File

@@ -4,7 +4,7 @@ declare(strict_types=1);
namespace App\Framework\ExceptionHandling; namespace App\Framework\ExceptionHandling;
use App\Framework\Config\EnvironmentType; use App\Framework\Config\TypedConfiguration;
use App\Framework\Console\ConsoleOutput; use App\Framework\Console\ConsoleOutput;
use App\Framework\Context\ExecutionContext; use App\Framework\Context\ExecutionContext;
use App\Framework\ErrorAggregation\ErrorAggregatorInterface; use App\Framework\ErrorAggregation\ErrorAggregatorInterface;
@@ -28,15 +28,25 @@ final readonly class ExceptionHandlingInitializer
#[Initializer] #[Initializer]
public function initialize( public function initialize(
Container $container, Container $container,
EnvironmentType $environmentType, TypedConfiguration $typedConfig,
ExecutionContext $executionContext, ExecutionContext $executionContext,
Engine $engine, Engine $engine,
?ConsoleOutput $consoleOutput = null, ?ConsoleOutput $consoleOutput = null,
?Logger $logger = null ?Logger $logger = null
): void { ): void {
$isDebugMode = $environmentType->isDebugEnabled(); // Get debug mode from AppConfig - this respects both APP_DEBUG env var AND EnvironmentType
// DIAGNOSTIC: Log debug mode determination (remove after verification) $isDebugMode = $typedConfig->app->isDebugEnabled();
error_log("[ExceptionHandlingInitializer] environmentType={$environmentType->value}, isDebugEnabled={$isDebugMode}"); $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 // ConsoleOutput - only create if CLI context and not already provided
// For HTTP context, null is acceptable (ConsoleErrorRenderer won't be used) // For HTTP context, null is acceptable (ConsoleErrorRenderer won't be used)
@@ -54,11 +64,11 @@ final readonly class ExceptionHandlingInitializer
executionContext: $executionContext, executionContext: $executionContext,
engine: $engine, engine: $engine,
consoleOutput: $consoleOutput, // null for HTTP context, ConsoleOutput for CLI context 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 // ErrorKernel - bind singleton with ErrorHandlingConfig so HTTP renderers respect environment
$container->singleton(ErrorKernel::class, static function (Container $c) use ($isDebugMode, $executionContext, $consoleOutput): ErrorKernel { $container->singleton(ErrorKernel::class, static function (Container $c) use ($errorConfig, $executionContext, $consoleOutput): ErrorKernel {
$errorAggregator = $c->has(ErrorAggregatorInterface::class) ? $c->get(ErrorAggregatorInterface::class) : null; $errorAggregator = $c->has(ErrorAggregatorInterface::class) ? $c->get(ErrorAggregatorInterface::class) : null;
$contextProvider = $c->has(ExceptionContextProvider::class) ? $c->get(ExceptionContextProvider::class) : null; $contextProvider = $c->has(ExceptionContextProvider::class) ? $c->get(ExceptionContextProvider::class) : null;
$auditLogger = $c->has(ExceptionAuditLogger::class) ? $c->get(ExceptionAuditLogger::class) : null; $auditLogger = $c->has(ExceptionAuditLogger::class) ? $c->get(ExceptionAuditLogger::class) : null;
@@ -73,7 +83,7 @@ final readonly class ExceptionHandlingInitializer
rateLimiter: $rateLimiter, rateLimiter: $rateLimiter,
executionContext: $executionContext, executionContext: $executionContext,
consoleOutput: $consoleOutput, consoleOutput: $consoleOutput,
isDebugMode: $isDebugMode config: $errorConfig
); );
}); });

View File

@@ -278,9 +278,6 @@ final readonly class ResponseErrorRenderer implements ErrorRenderer
); );
$debugInfo = ''; $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) { if ($this->isDebugMode) {
$debugInfo = $this->generateDebugSection($exception, $contextProvider); $debugInfo = $this->generateDebugSection($exception, $contextProvider);
} }