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
+