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.
This commit is contained in:
71
src/Framework/ExceptionHandling/ErrorHandlingConfig.php
Normal file
71
src/Framework/ExceptionHandling/ErrorHandlingConfig.php
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user