diff --git a/.ddev/nginx/fossbilling-security.conf b/.ddev/nginx/fossbilling-security.conf index c958217de..5c06b4fef 100644 --- a/.ddev/nginx/fossbilling-security.conf +++ b/.ddev/nginx/fossbilling-security.conf @@ -3,6 +3,53 @@ location = /config.php { deny all; } +# Deny PHP scripts that are not web entry points (index.php, ipn.php, installer). +# Exact matches take precedence over the PHP handler regardless of include order. +location = /cron.php { + deny all; +} + +location = /console.php { + deny all; +} + +location = /load.php { + deny all; +} + +location = /di.php { + deny all; +} + +location = /config-sample.php { + deny all; +} + +location = /config.old.php { + deny all; +} + +location = /install/session.php { + deny all; +} + +# Deny code directories. Prefix matches skip the PHP handler. +location ^~ /library/ { + deny all; +} + +location ^~ /modules/ { + deny all; +} + +location ^~ /locale/ { + deny all; +} + +location ^~ /install/sql/ { + deny all; +} + location ^~ /vendor/ { deny all; } diff --git a/src/cron.php b/src/cron.php index 7810bb5ee..2046e83fe 100644 --- a/src/cron.php +++ b/src/cron.php @@ -8,6 +8,10 @@ declare(strict_types=1); * @copyright FOSSBilling (https://www.fossbilling.org) * @license http://www.apache.org/licenses/LICENSE-2.0 Apache-2.0 */ +if (php_sapi_name() !== 'cli') { + exit('This script can only be run from the command line.'); +} + require_once __DIR__ . DIRECTORY_SEPARATOR . 'load.php'; use FOSSBilling\Environment; diff --git a/tests/Unit/WebServerSecurityConfigTest.php b/tests/Unit/WebServerSecurityConfigTest.php index 9dec7919f..7f3acaaf3 100644 --- a/tests/Unit/WebServerSecurityConfigTest.php +++ b/tests/Unit/WebServerSecurityConfigTest.php @@ -12,3 +12,19 @@ test('theme configuration directories are protected at any nesting depth', funct expect($configuration)->not->toBeFalse() ->and($configuration)->toContain('RewriteCond %{REQUEST_URI} ^/themes/(?:[^/]+/)+config/ [NC]'); }); + +test('cron entry point refuses non-CLI execution before bootstrapping', function (): void { + $cron = file_get_contents(Path::join(__DIR__, '..', '..', 'src/cron.php')); + + expect($cron)->not->toBeFalse() + ->and($cron)->toMatch( + "/if\s*\(\s*php_sapi_name\(\)\s*!==\s*'cli'\s*\)\s*\{\s*exit\s*\(/s" + ); + + $guardPosition = strpos((string) $cron, 'php_sapi_name()'); + $bootstrapPosition = strpos((string) $cron, 'require_once __DIR__'); + + expect($guardPosition)->not->toBeFalse() + ->and($bootstrapPosition)->not->toBeFalse() + ->and($guardPosition)->toBeLessThan($bootstrapPosition); +});