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
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user