diff --git a/Containerfile b/Containerfile index a7b99d3..60fe44c 100644 --- a/Containerfile +++ b/Containerfile @@ -1,11 +1,15 @@ FROM php:8.4.14-apache # Enable Apache modules and custom config as root during build -RUN a2enmod rewrite +RUN a2enmod rewrite headers COPY apache.conf /etc/apache2/conf-available/custom.conf RUN a2enconf custom +# Override default security.conf settings +RUN sed -i 's/^ServerTokens OS/ServerTokens Prod/' /etc/apache2/conf-available/security.conf \ + && sed -i 's/^ServerSignature On/ServerSignature Off/' /etc/apache2/conf-available/security.conf + # Log to /proc/self/fd for container output RUN sed -i 's|ErrorLog.*|ErrorLog /proc/self/fd/2|' /etc/apache2/sites-available/000-default.conf \ && sed -i 's|CustomLog.*|CustomLog /proc/self/fd/1 combined|' /etc/apache2/sites-available/000-default.conf \ diff --git a/apache.conf b/apache.conf index d8adda7..7bf87bb 100644 --- a/apache.conf +++ b/apache.conf @@ -1,5 +1,13 @@ +# Minimize server version disclosure +ServerTokens Prod + +# Disable PHP version header and error display +php_flag expose_php Off +php_flag display_errors Off +php_flag log_errors On + - Options Indexes FollowSymLinks + Options FollowSymLinks AllowOverride All Require all granted diff --git a/content/.htaccess b/content/.htaccess index a788fe7..379c049 100644 --- a/content/.htaccess +++ b/content/.htaccess @@ -1,9 +1,31 @@ DirectorySlash Off +# Block direct access to content source files + + # Allow only the entry point + + Require all denied + + + +# Security headers + + Header set X-Content-Type-Options "nosniff" + Header set X-Frame-Options "DENY" + Header set Referrer-Policy "strict-origin-when-cross-origin" + Header set Permissions-Policy "camera=(), microphone=(), geolocation=()" + Header unset X-Powered-By + Header always unset X-Powered-By + + +# Restrict HTTP methods to GET, POST, HEAD RewriteEngine On RewriteBase / + RewriteCond %{REQUEST_METHOD} !^(GET|POST|HEAD)$ [NC] + RewriteRule .* - [F,L] + # Route /app requests to index.php RewriteCond %{REQUEST_URI} ^/app/ RewriteRule ^(.*)$ /index.php [L,QSA] diff --git a/content/.htaccess.base b/content/.htaccess.base index 5bbdf2e..ffeacd3 100644 --- a/content/.htaccess.base +++ b/content/.htaccess.base @@ -1,9 +1,31 @@ DirectorySlash Off +# Block direct access to content source files + + # Allow only the entry point + + Require all denied + + + +# Security headers + + Header set X-Content-Type-Options "nosniff" + Header set X-Frame-Options "DENY" + Header set Referrer-Policy "strict-origin-when-cross-origin" + Header set Permissions-Policy "camera=(), microphone=(), geolocation=()" + Header unset X-Powered-By + Header always unset X-Powered-By + + +# Restrict HTTP methods to GET, POST, HEAD RewriteEngine On RewriteBase / + RewriteCond %{REQUEST_METHOD} !^(GET|POST|HEAD)$ [NC] + RewriteRule .* - [F,L] + # Route /app requests to index.php RewriteCond %{REQUEST_URI} ^/app/ RewriteRule ^(.*)$ /index.php [L,QSA] diff --git a/content/index.php b/content/index.php index 8288ad2..a5c1763 100644 --- a/content/index.php +++ b/content/index.php @@ -5,5 +5,12 @@ if (str_starts_with($_SERVER['REQUEST_URI'], '/app/')) { exit; } +// Harden session cookie before any session starts +ini_set('session.cookie_httponly', '1'); +ini_set('session.cookie_samesite', 'Lax'); +if (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') { + ini_set('session.cookie_secure', '1'); +} + // All other requests go to router require __DIR__ . '/../app/router.php'; diff --git a/docs/security-cpanel.md b/docs/security-cpanel.md new file mode 100644 index 0000000..fc71801 --- /dev/null +++ b/docs/security-cpanel.md @@ -0,0 +1,56 @@ +# Security Hardening — cPanel Shared Hosting + +The container dev environment (Containerfile + apache.conf) handles most hardening automatically. On cPanel shared hosting, some settings must be configured manually since you don't control the Apache or PHP config directly. + +## What's handled by .htaccess (works everywhere) + +These are applied automatically via `content/.htaccess` (synced from `.htaccess.base`): + +- Block direct access to `.ini`, `.md`, `.html`, `.php` content files +- Security headers: `X-Content-Type-Options`, `X-Frame-Options`, `Referrer-Policy`, `Permissions-Policy` +- Strip `X-Powered-By` header +- Restrict HTTP methods to GET/POST/HEAD +- Rewrite rules routing all requests through `index.php` + +The `custom/.htaccess` and `custom/data/.htaccess` files also deploy automatically and block direct access to config files and data. + +## What needs manual cPanel configuration + +### 1. Disable display_errors + +Go to **MultiPHP INI Editor** (Home > Software > MultiPHP INI Editor): + +- Select the domain +- Set `display_errors` = **Off** +- Set `log_errors` = **On** +- Set `expose_php` = **Off** + +This prevents PHP errors from leaking server paths and internal details to visitors. + +### 2. PHP version + +Use **MultiPHP Manager** to ensure PHP 8.4+ is selected for the domain. + +### 3. Session cookie hardening + +Handled in `content/index.php` via `ini_set()` calls — no cPanel action needed. The entry point sets `HttpOnly`, `SameSite=Lax`, and `Secure` (when on HTTPS) before any session starts. + +### 4. Server version header + +On shared hosting you typically cannot change `ServerTokens` (it's a server-level directive). The `X-Powered-By` header is stripped by `.htaccess`, but the `Server: Apache/2.4.x` header may still show the full version. This is a low-risk issue on shared hosting since the Apache version is the hosting provider's responsibility. + +### 5. SSL/TLS + +Use **SSL/TLS** (Home > Security > SSL/TLS) or **AutoSSL** to ensure HTTPS is active. The session cookie `Secure` flag only activates over HTTPS. + +## Checklist + +- [ ] `.htaccess` deployed (copy `.htaccess.base` if needed, preserve cPanel-generated blocks) +- [ ] `display_errors` = Off in MultiPHP INI Editor +- [ ] `expose_php` = Off in MultiPHP INI Editor +- [ ] `log_errors` = On in MultiPHP INI Editor +- [ ] SSL certificate active +- [ ] `custom/smtp-config.php` exists but is NOT in git (check `.gitignore`) +- [ ] `custom/listmonk-config.php` exists but is NOT in git (check `.gitignore`) +- [ ] `custom/data/` directory writable by web server (`chmod 755` or `775`) +- [ ] `custom/data/.htaccess` present with `Require all denied`