diff --git a/assets/.thumbs/.htaccess b/assets/.thumbs/.htaccess index 6c1cc6e2da..785fc3b368 100644 --- a/assets/.thumbs/.htaccess +++ b/assets/.thumbs/.htaccess @@ -1,5 +1,10 @@ IndexIgnore */* - Order Deny,Allow - Deny from all + + Require all denied + + + Order deny,allow + Deny from all + diff --git a/assets/backup/.htaccess b/assets/backup/.htaccess index ff2beb844b..fb1de45bdb 100755 --- a/assets/backup/.htaccess +++ b/assets/backup/.htaccess @@ -1,2 +1,7 @@ -order deny,allow -deny from all + + Require all denied + + + Order deny,allow + Deny from all + diff --git a/assets/cache/.htaccess b/assets/cache/.htaccess index ff2beb844b..fb1de45bdb 100644 --- a/assets/cache/.htaccess +++ b/assets/cache/.htaccess @@ -1,2 +1,7 @@ -order deny,allow -deny from all + + Require all denied + + + Order deny,allow + Deny from all + diff --git a/assets/cache/images/.htaccess b/assets/cache/images/.htaccess index 83c6d382fb..1d92c1dc81 100755 --- a/assets/cache/images/.htaccess +++ b/assets/cache/images/.htaccess @@ -1,2 +1,7 @@ -order deny,allow -allow from all + + Require all granted + + + Order deny,allow + Allow from all + diff --git a/assets/docs/.htaccess b/assets/docs/.htaccess index 1fce156e5a..f097ca5458 100755 --- a/assets/docs/.htaccess +++ b/assets/docs/.htaccess @@ -1,9 +1,19 @@ IndexIgnore */* - Order Allow,Deny - Deny from all + + Require all denied + + + Order deny,allow + Deny from all + - Order Deny,Allow - Deny from all + + Require all denied + + + Order deny,allow + Deny from all + diff --git a/assets/export/.htaccess b/assets/export/.htaccess index 6c1cc6e2da..785fc3b368 100644 --- a/assets/export/.htaccess +++ b/assets/export/.htaccess @@ -1,5 +1,10 @@ IndexIgnore */* - Order Deny,Allow - Deny from all + + Require all denied + + + Order deny,allow + Deny from all + diff --git a/assets/files/.htaccess b/assets/files/.htaccess index 6c1cc6e2da..785fc3b368 100644 --- a/assets/files/.htaccess +++ b/assets/files/.htaccess @@ -1,5 +1,10 @@ IndexIgnore */* - Order Deny,Allow - Deny from all + + Require all denied + + + Order deny,allow + Deny from all + diff --git a/assets/images/.htaccess b/assets/images/.htaccess index 6c1cc6e2da..785fc3b368 100644 --- a/assets/images/.htaccess +++ b/assets/images/.htaccess @@ -1,5 +1,10 @@ IndexIgnore */* - Order Deny,Allow - Deny from all + + Require all denied + + + Order deny,allow + Deny from all + diff --git a/assets/import/.htaccess b/assets/import/.htaccess index 6c1cc6e2da..785fc3b368 100644 --- a/assets/import/.htaccess +++ b/assets/import/.htaccess @@ -1,5 +1,10 @@ IndexIgnore */* - Order Deny,Allow - Deny from all + + Require all denied + + + Order deny,allow + Deny from all + diff --git a/assets/js/.htaccess b/assets/js/.htaccess index 6c1cc6e2da..785fc3b368 100755 --- a/assets/js/.htaccess +++ b/assets/js/.htaccess @@ -1,5 +1,10 @@ IndexIgnore */* - Order Deny,Allow - Deny from all + + Require all denied + + + Order deny,allow + Deny from all + diff --git a/assets/js/fileapi/.htaccess b/assets/js/fileapi/.htaccess index 6c1cc6e2da..785fc3b368 100755 --- a/assets/js/fileapi/.htaccess +++ b/assets/js/fileapi/.htaccess @@ -1,5 +1,10 @@ IndexIgnore */* - Order Deny,Allow - Deny from all + + Require all denied + + + Order deny,allow + Deny from all + diff --git a/assets/js/jeditable/.htaccess b/assets/js/jeditable/.htaccess index 6c1cc6e2da..785fc3b368 100755 --- a/assets/js/jeditable/.htaccess +++ b/assets/js/jeditable/.htaccess @@ -1,5 +1,10 @@ IndexIgnore */* - Order Deny,Allow - Deny from all + + Require all denied + + + Order deny,allow + Deny from all + diff --git a/assets/site/.htaccess b/assets/site/.htaccess index 6c1cc6e2da..785fc3b368 100644 --- a/assets/site/.htaccess +++ b/assets/site/.htaccess @@ -1,5 +1,10 @@ IndexIgnore */* - Order Deny,Allow - Deny from all + + Require all denied + + + Order deny,allow + Deny from all + diff --git a/core/.htaccess b/core/.htaccess index ff2beb844b..fb1de45bdb 100644 --- a/core/.htaccess +++ b/core/.htaccess @@ -1,2 +1,7 @@ -order deny,allow -deny from all + + Require all denied + + + Order deny,allow + Deny from all + diff --git a/core/config/session.php b/core/config/session.php index 6434fea78d..e59d1819b5 100644 --- a/core/config/session.php +++ b/core/config/session.php @@ -151,8 +151,15 @@ | to the server if the browser has a HTTPS connection. This will keep | the cookie from being sent to you if it can not be done securely. | + | Unset, it follows the current request: an HTTPS site gets the Secure + | flag without configuration. Set it explicitly behind a TLS-terminating + | proxy that talks plain HTTP to PHP. + | */ - 'secure' => env('SESSION_SECURE_COOKIE', false), + 'secure' => env('SESSION_SECURE_COOKIE') ?? ( + (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') + || (int)($_SERVER['SERVER_PORT'] ?? 0) === 443 + ), /* |-------------------------------------------------------------------------- diff --git a/core/tests/Unit/Security/ApacheConfigHardeningTest.php b/core/tests/Unit/Security/ApacheConfigHardeningTest.php new file mode 100644 index 0000000000..030b098ef4 --- /dev/null +++ b/core/tests/Unit/Security/ApacheConfigHardeningTest.php @@ -0,0 +1,80 @@ +toContain('RewriteRule ^(core|views|vendor|tmp)(/|$) - [F,L]') + ->toContain('RewriteRule ^assets/(cache|backup|export|import)(/|$) - [F,L]') + ->toContain('RewriteRule ^assets/.*\.php$ - [F,L,NC]') + ->toContain('RewriteRule \.(sql|sqlite|db|log|bak|old|orig|save|swp|swo|tpl|inc|ini|env|dist|example|yml|yaml|lock|tar)(\.(gz|bz2|xz|zip|tgz))?$ - [F,L,NC]') + ->toContain('RewriteRule ^(composer\.(json|lock)|phpstan\.neon|publiccode\.yml|AGENTS\.md|README\.md|ht\.access|ng\.inx|config\.php(\.example)?)$ - [F,L]'); + + // the deny rules must run before the assets/manager passthrough that ends rewriting + expect(strpos($htaccess, 'RewriteRule ^(core|views|vendor|tmp)')) + ->toBeLessThan(strpos($htaccess, 'RewriteRule ^(manager|assets|js|css|images|img)/.*$ - [L]')); +}); + +it('writes every shipped per-directory .htaccess in both the 2.2 and the 2.4 dialect', function (string $file) { + $source = evoFile($file); + + if (preg_match('/^\s*(Order|Deny|Allow|Require)\b/mi', $source) !== 1) { + expect(true)->toBeTrue(); // rewrite-only file, nothing to check + return; + } + + expect($source) + ->toContain('') + ->toContain('') + ->and(preg_match_all('/Require all (denied|granted)/', $source)) + ->toBe(preg_match_all('/(Deny|Allow) from all/i', $source)); + + // a bare 2.2 directive outside its guard is a 500 on a 2.4 server without mod_access_compat + expect(preg_match('/^\s*(Order|Deny from|Allow from)\b/mi', preg_replace('/.*?<\/IfModule>/s', '', $source))) + ->toBe(0); +})->with(function () { + $root = dirname(__DIR__, 4); + $out = []; + foreach (explode("\n", trim((string)shell_exec('git -C ' . escapeshellarg($root) . ' ls-files'))) as $path) { + if (preg_match('~(^|/)\.htaccess$~', $path)) { + $out[] = $path; + } + } + + return $out; +}); + +it('does not ship an installer .htaccess that strips the CSP the installer sets', function () { + expect(file_exists(dirname(__DIR__, 4) . '/install/.htaccess'))->toBeFalse(); +}); + +it('no longer switches mod_security off for the manager', function () { + expect(evoFile('manager/.htaccess'))->not->toContain('SecFilterEngine'); +}); + +it('marks the session cookie Secure on https unless told otherwise', function () { + $source = evoFile('core/config/session.php'); + + expect($source) + ->toContain("'secure' => env('SESSION_SECURE_COOKIE') ?? (") + ->toContain("\$_SERVER['HTTPS'] !== 'off'") + ->and($source)->not->toContain("env('SESSION_SECURE_COOKIE', false)"); +}); diff --git a/ht.access b/ht.access index febb638ee9..555c6952d9 100644 --- a/ht.access +++ b/ht.access @@ -61,6 +61,22 @@ RewriteRule .* - [F,L] #RewriteCond %{THE_REQUEST} \s/+index\.php\?q=([^\s&]+) [NC] #RewriteRule ^ /%1? [R=301,L] +# Paths that must never be reachable over HTTP (mirrors ng.inx). +# core, views, vendor, tmp - CMS core (config, .env, storage), Blade layouts, deps, scratch +# assets/(cache|backup|export|import) - generated and writable areas; the per-directory +# .htaccess files there only work with AllowOverride +RewriteRule ^(core|views|vendor|tmp)(/|$) - [F,L] +RewriteRule ^assets/(cache|backup|export|import)(/|$) - [F,L] + +# Root-level files that fingerprint the install or describe its layout. +RewriteRule ^(composer\.(json|lock)|phpstan\.neon|publiccode\.yml|AGENTS\.md|README\.md|ht\.access|ng\.inx|config\.php(\.example)?)$ - [F,L] + +# Backups, dumps, editor leftovers and PHP include fragments anywhere, including compressed copies. +RewriteRule \.(sql|sqlite|db|log|bak|old|orig|save|swp|swo|tpl|inc|ini|env|dist|example|yml|yaml|lock|tar)(\.(gz|bz2|xz|zip|tgz))?$ - [F,L,NC] + +# No PHP execution under assets - uploads live here. +RewriteRule ^assets/.*\.php$ - [F,L,NC] + # Exclude /assets and /manager directories and images from rewrite rules RewriteRule ^(manager|assets|js|css|images|img)/.*$ - [L] RewriteRule \.(jpg|jpeg|png|gif|ico)$ - [L] diff --git a/install/.htaccess b/install/.htaccess deleted file mode 100644 index ee0d892062..0000000000 --- a/install/.htaccess +++ /dev/null @@ -1,3 +0,0 @@ - - Header always unset Content-Security-Policy - \ No newline at end of file diff --git a/install/assets/.htaccess b/install/assets/.htaccess index 3a42882788..fb1de45bdb 100644 --- a/install/assets/.htaccess +++ b/install/assets/.htaccess @@ -1 +1,7 @@ -Deny from all + + Require all denied + + + Order deny,allow + Deny from all + diff --git a/install/src/.htaccess b/install/src/.htaccess index 3a42882788..fb1de45bdb 100644 --- a/install/src/.htaccess +++ b/install/src/.htaccess @@ -1 +1,7 @@ -Deny from all + + Require all denied + + + Order deny,allow + Deny from all + diff --git a/manager/.htaccess b/manager/.htaccess index 79e352df19..a98fff4be3 100644 --- a/manager/.htaccess +++ b/manager/.htaccess @@ -8,11 +8,6 @@ RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-d RewriteRule ^(.*)$ index.php [L,QSA] - -# Turn the filtering engine Off -SecFilterEngine Off - - ExpiresActive off diff --git a/manager/media/browser/mcpuk/core/.htaccess b/manager/media/browser/mcpuk/core/.htaccess index d61b264c5e..5f01affee9 100755 --- a/manager/media/browser/mcpuk/core/.htaccess +++ b/manager/media/browser/mcpuk/core/.htaccess @@ -1,4 +1,9 @@ -Order allow,deny -Deny from all + + Require all denied + + + Order deny,allow + Deny from all + diff --git a/manager/media/browser/mcpuk/doc/.htaccess b/manager/media/browser/mcpuk/doc/.htaccess index d61b264c5e..5f01affee9 100755 --- a/manager/media/browser/mcpuk/doc/.htaccess +++ b/manager/media/browser/mcpuk/doc/.htaccess @@ -1,4 +1,9 @@ -Order allow,deny -Deny from all + + Require all denied + + + Order deny,allow + Deny from all + diff --git a/manager/media/browser/mcpuk/lang/.htaccess b/manager/media/browser/mcpuk/lang/.htaccess index d61b264c5e..5f01affee9 100755 --- a/manager/media/browser/mcpuk/lang/.htaccess +++ b/manager/media/browser/mcpuk/lang/.htaccess @@ -1,4 +1,9 @@ -Order allow,deny -Deny from all + + Require all denied + + + Order deny,allow + Deny from all + diff --git a/manager/media/browser/mcpuk/lib/.htaccess b/manager/media/browser/mcpuk/lib/.htaccess index d61b264c5e..5f01affee9 100755 --- a/manager/media/browser/mcpuk/lib/.htaccess +++ b/manager/media/browser/mcpuk/lib/.htaccess @@ -1,4 +1,9 @@ -Order allow,deny -Deny from all + + Require all denied + + + Order deny,allow + Deny from all + diff --git a/manager/media/browser/mcpuk/tpl/.htaccess b/manager/media/browser/mcpuk/tpl/.htaccess index 7484f13a00..7b313dc35d 100755 --- a/manager/media/browser/mcpuk/tpl/.htaccess +++ b/manager/media/browser/mcpuk/tpl/.htaccess @@ -1,4 +1,9 @@ -Order allow,deny -Deny from all + + Require all denied + + + Order deny,allow + Deny from all + \ No newline at end of file diff --git a/views/.htaccess b/views/.htaccess index ff2beb844b..fb1de45bdb 100755 --- a/views/.htaccess +++ b/views/.htaccess @@ -1,2 +1,7 @@ -order deny,allow -deny from all + + Require all denied + + + Order deny,allow + Deny from all +