Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,12 @@ jobs:
fail-fast: false
matrix:
include:
- php-version: '8.1'
- php-version: '8.2'
dependencies: 'lowest'
- php-version: '8.4'
dependencies: 'highest'
- php-version: '8.5'
dependencies: 'highest'

steps:
- uses: actions/checkout@v5
Expand Down Expand Up @@ -61,7 +63,7 @@ jobs:
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.1'
php-version: '8.2'
extensions: mbstring, intl
coverage: none
tools: cs2pr, phpstan:1.12
Expand Down
12 changes: 6 additions & 6 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,17 @@
"homepage": "https://cakephp.org",
"require": {
"php": ">=8.1",
"cakephp/cakephp": "5.2.*",
"cakephp/migrations": "^4.0.0",
"cakephp/cakephp": "5.3.*",
"cakephp/migrations": "^5.0",
"cakephp/plugin-installer": "^2.0",
"mobiledetect/mobiledetectlib": "^4.8.03"
},
"require-dev": {
"cakephp/bake": "^3.0.0",
"cakephp/cakephp-codesniffer": "^5.0",
"cakephp/debug_kit": "^5.0.0",
"cakephp/bake": "^3.6",
"cakephp/cakephp-codesniffer": "^5.3",
"cakephp/debug_kit": "^5.2",
"josegonzalez/dotenv": "^4.0",
"phpunit/phpunit": "^10.5.5 || ^11.1.3 || ^12.1"
"phpunit/phpunit": "^11.1.3 || ^12.1 || ^13.0"
},
"suggest": {
"cakephp/repl": "Console tools for a REPL interface for CakePHP applications.",
Expand Down
2 changes: 1 addition & 1 deletion config/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export APP_DEFAULT_LOCALE="en_US"
export APP_DEFAULT_TIMEZONE="UTC"
# SECURITY: Set this to your domain to prevent Host Header Injection attacks
# This is REQUIRED in production for password resets and other security features
export APP_FULL_BASE_URL="https://yourdomain.com"
export APP_FULL_BASE_URL="https://example.com"
export SECURITY_SALT="__SALT__"

# Uncomment these to define cache configuration via environment variables.
Expand Down
2 changes: 1 addition & 1 deletion config/app.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
* IMPORTANT: This MUST be set in production to prevent Host Header Injection attacks
* that can compromise password reset and other security-critical features.
* Set this via APP_FULL_BASE_URL environment variable or directly in config.
* Example: 'https://yourdomain.com'
* Example: 'https://example.com'
* When not set, the application will throw an exception in production mode.
* - imageBaseUrl - Web path to the public images/ directory under webroot.
* - cssBaseUrl - Web path to the public css/ directory under webroot.
Expand Down
27 changes: 7 additions & 20 deletions config/bootstrap.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@
use Cake\Cache\Cache;
use Cake\Core\Configure;
use Cake\Core\Configure\Engine\PhpConfig;
use Cake\Core\Exception\CakeException;
use Cake\Datasource\ConnectionManager;
use Cake\Error\ErrorTrap;
use Cake\Error\ExceptionTrap;
Expand Down Expand Up @@ -146,37 +145,25 @@
}

/*
* SECURITY: Validate and set the full base URL.
* This URL is used as the base of all absolute links.
* Set the full base URL for the application.
*
* IMPORTANT: In production, App.fullBaseUrl MUST be explicitly configured to prevent
* Host Header Injection attacks. Relying on the HTTP_HOST header can allow attackers
* to hijack password reset tokens and other security-critical operations.
* SECURITY: In production, App.fullBaseUrl MUST be explicitly configured to prevent
* Host Header Injection attacks. The HostHeaderMiddleware enforces this requirement
* and validates incoming Host headers against the configured value.
*
* Set APP_FULL_BASE_URL in your environment variables or configure App.fullBaseUrl
* in config/app.php or config/app_local.php
*
* Example: APP_FULL_BASE_URL=https://yourdomain.com
* Example: APP_FULL_BASE_URL=https://example.com
*/
$fullBaseUrl = Configure::read('App.fullBaseUrl');
if (!$fullBaseUrl) {
$httpHost = env('HTTP_HOST');

/*
* Only enforce fullBaseUrl requirement when we're in a web request context.
* This allows CLI tools (like PHPStan) to load the bootstrap without throwing.
*/
if (!Configure::read('debug') && $httpHost) {
throw new CakeException(
'SECURITY: App.fullBaseUrl is not configured. ' .
'This is required in production to prevent Host Header Injection attacks. ' .
'Set APP_FULL_BASE_URL environment variable or configure App.fullBaseUrl in config/app.php',
);
}

/*
* Development mode fallback: Use HTTP_HOST for convenience.
* WARNING: This is ONLY safe in development. Never use this pattern in production!
* WARNING: This is ONLY safe in development. In production, the
* HostHeaderMiddleware will reject requests when fullBaseUrl is not configured.
*/
if ($httpHost) {
$s = null;
Expand Down
1 change: 0 additions & 1 deletion phpunit.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.1/phpunit.xsd">
<php>
<ini name="memory_limit" value="-1"/>
<ini name="apc.enable_cli" value="1"/>
</php>

<!-- Add any additional test suites you want to run here -->
Expand Down
11 changes: 10 additions & 1 deletion src/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
*/
namespace App;

use App\Middleware\HostHeaderMiddleware;
use Cake\Core\Configure;
use Cake\Core\ContainerInterface;
use Cake\Datasource\FactoryLocator;
Expand Down Expand Up @@ -51,7 +52,10 @@ public function bootstrap(): void

if (PHP_SAPI !== 'cli') {
// The bake plugin requires fallback table classes to work properly
FactoryLocator::add('Table', (new TableLocator())->allowFallbackClass(false));
FactoryLocator::add(
'Table',
(new TableLocator())->allowFallbackClass(false), // @phpstan-ignore argument.type
);
}
}

Expand All @@ -68,6 +72,11 @@ public function middleware(MiddlewareQueue $middlewareQueue): MiddlewareQueue
// and make an error page/response
->add(new ErrorHandlerMiddleware(Configure::read('Error'), $this))

// Validate Host header to prevent Host Header Injection attacks.
// In production, ensures App.fullBaseUrl is configured and validates
// the incoming Host header against it.
->add(new HostHeaderMiddleware())

// Handle plugin/theme assets like CakePHP normally does.
->add(new AssetMiddleware([
'cacheTime' => Configure::read('Asset.cacheTime'),
Expand Down
58 changes: 58 additions & 0 deletions src/Middleware/HostHeaderMiddleware.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<?php
declare(strict_types=1);

namespace App\Middleware;

use Cake\Core\Configure;
use Cake\Http\Exception\BadRequestException;
use Cake\Http\Exception\InternalErrorException;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;

/**
* Middleware to validate Host header and prevent Host Header Injection attacks.
*
* In production, this middleware ensures that App.fullBaseUrl is configured
* and validates incoming Host headers against it. This prevents attackers
* from manipulating password reset links and other security-critical URLs.
*
* @see https://owasp.org/www-project-web-security-testing-guide/latest/4-Web_Application_Security_Testing/07-Input_Validation_Testing/17-Testing_for_Host_Header_Injection
*/
class HostHeaderMiddleware implements MiddlewareInterface
{
/**
* Process the request and validate the Host header.
*
* @param \Psr\Http\Message\ServerRequestInterface $request The request.
* @param \Psr\Http\Server\RequestHandlerInterface $handler The request handler.
* @return \Psr\Http\Message\ResponseInterface A response.
*/
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
if (Configure::read('debug')) {
return $handler->handle($request);
}

$fullBaseUrl = Configure::read('App.fullBaseUrl');
if (!$fullBaseUrl) {
throw new InternalErrorException(
'SECURITY: App.fullBaseUrl is not configured. ' .
'This is required in production to prevent Host Header Injection attacks. ' .
'Set APP_FULL_BASE_URL environment variable or configure App.fullBaseUrl in config/app.php',
);
}

$configuredHost = parse_url($fullBaseUrl, PHP_URL_HOST);
$requestHost = $request->getUri()->getHost();

if ($configuredHost && $requestHost && strtolower($configuredHost) !== strtolower($requestHost)) {
throw new BadRequestException(
'Invalid Host header. Request host does not match configured application host.',
);
}

return $handler->handle($request);
}
}
5 changes: 4 additions & 1 deletion tests/TestCase/ApplicationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
namespace App\Test\TestCase;

use App\Application;
use App\Middleware\HostHeaderMiddleware;
use Cake\Core\Configure;
use Cake\Error\Middleware\ErrorHandlerMiddleware;
use Cake\Http\MiddlewareQueue;
Expand Down Expand Up @@ -78,8 +79,10 @@ public function testMiddleware()

$this->assertInstanceOf(ErrorHandlerMiddleware::class, $middleware->current());
$middleware->seek(1);
$this->assertInstanceOf(AssetMiddleware::class, $middleware->current());
$this->assertInstanceOf(HostHeaderMiddleware::class, $middleware->current());
$middleware->seek(2);
$this->assertInstanceOf(AssetMiddleware::class, $middleware->current());
$middleware->seek(3);
$this->assertInstanceOf(RoutingMiddleware::class, $middleware->current());
}
}
Loading