generated from bisco/codex-bootstrap
fix: harden WordPress against WPScan findings
This commit is contained in:
@@ -52,6 +52,7 @@ services:
|
||||
$$_SERVER['HTTPS'] = 'on';
|
||||
}
|
||||
define('DISALLOW_FILE_EDIT', true);
|
||||
define('DISABLE_WP_CRON', true);
|
||||
define('WP_AUTO_UPDATE_CORE', 'minor');
|
||||
define('WP_POST_REVISIONS', 10);
|
||||
if (getenv('WP_ENVIRONMENT_TYPE') === 'production') {
|
||||
|
||||
@@ -12,7 +12,7 @@ architecture and starts independently from `main`.
|
||||
|
||||
## Decision
|
||||
|
||||
Use pinned WordPress 7.0/PHP 8.3 with MariaDB 11.8 LTS. Build a dependency-free custom
|
||||
Use pinned WordPress 7.0.2/PHP 8.3 with MariaDB 11.8 LTS. Build a dependency-free custom
|
||||
classic theme for exact semantic markup and responsive design. Store homepage fields as
|
||||
sanitized theme modifications, while a must-use plugin owns Shows and Gallery custom
|
||||
post types so structured content is not lost when changing themes.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Architecture
|
||||
|
||||
NGINX is the only public entry point for `azionelab.org`. It proxies HTTP to the
|
||||
official WordPress 7.0/PHP 8.3 Apache image over the private `web` network. WordPress
|
||||
official WordPress 7.0.2/PHP 8.3 Apache image over the private `web` network. WordPress
|
||||
connects to MariaDB 11.8 LTS over a separate internal `data` network. Neither WordPress
|
||||
nor MariaDB publishes a host port; automated security checks guard this assumption.
|
||||
|
||||
|
||||
@@ -23,6 +23,11 @@ Review security releases routinely, update pins in a task branch, rebuild, run t
|
||||
test suite, and deploy. Production disables WordPress web-based file modifications, so
|
||||
image rebuilds are the update path.
|
||||
|
||||
External `wp-cron.php` requests are blocked to reduce public attack surface. If future
|
||||
content scheduling, maintenance jobs, or plugin features require WordPress cron, run it
|
||||
from a controlled host/container cron against the private WordPress service instead of
|
||||
leaving the public trigger enabled.
|
||||
|
||||
## Backup and restore
|
||||
|
||||
Create database and WordPress file backups in one maintenance window. The default host
|
||||
@@ -42,6 +47,8 @@ the database and file volume together, restart, and verify the homepage, media,
|
||||
owner or mode; run `./scripts/prepare-host-volumes.sh` after changing paths or image
|
||||
user IDs.
|
||||
- SMTP is not configured; WordPress password-reset email needs an external mail service.
|
||||
- WordPress cron is not publicly triggerable; scheduled jobs need an operator-managed
|
||||
cron runner if that feature becomes necessary.
|
||||
- Admin MFA and network allowlisting are deployment concerns and are not bundled.
|
||||
- WordPress plugins expand the attack surface; install only reviewed, maintained,
|
||||
necessary plugins.
|
||||
|
||||
+5
-3
@@ -12,9 +12,11 @@
|
||||
- File editing is always disabled. Production also disables web-based core, theme, and
|
||||
plugin changes; patched images are rebuilt and redeployed instead.
|
||||
- XML-RPC and comments are disabled. NGINX blocks PHP execution below uploads, dotfiles,
|
||||
and direct `wp-config.php` requests, and rate-limits login/public requests. Apache
|
||||
also denies uploaded PHP files and direct access to sensitive WordPress internals as
|
||||
defense in depth. Public REST user enumeration and author archives are disabled.
|
||||
direct `wp-config.php` requests, the WordPress readme/license files, direct
|
||||
installation entry points, public mu-plugin directory probing, and external
|
||||
`wp-cron.php` requests. It also rate-limits login/public requests. Apache also denies
|
||||
uploaded PHP files and direct access to sensitive WordPress internals as defense in
|
||||
depth. Public REST user enumeration and author archives are disabled.
|
||||
- Security headers include CSP, same-origin framing, content-type protection, a strict
|
||||
referrer policy, and a restrictive Permissions Policy. WordPress compatibility still
|
||||
requires inline style/script CSP allowances; do not treat this CSP as an XSS sanitizer.
|
||||
|
||||
@@ -18,6 +18,26 @@ location = /xmlrpc.php {
|
||||
return 403;
|
||||
}
|
||||
|
||||
location = /wp-cron.php {
|
||||
return 403;
|
||||
}
|
||||
|
||||
location = /readme.html {
|
||||
return 404;
|
||||
}
|
||||
|
||||
location = /license.txt {
|
||||
return 404;
|
||||
}
|
||||
|
||||
location = /wp-admin/install.php {
|
||||
return 404;
|
||||
}
|
||||
|
||||
location ^~ /wp-content/mu-plugins/ {
|
||||
return 404;
|
||||
}
|
||||
|
||||
location = /wp-login.php {
|
||||
limit_req zone=login burst=5 nodelay;
|
||||
proxy_pass http://wordpress_backend;
|
||||
|
||||
@@ -121,6 +121,11 @@ test("protects the edge and exposes the WordPress admin", async ({ page, request
|
||||
expect((await request.get("/xmlrpc.php")).status()).toBe(403);
|
||||
expect((await request.get("/.env")).status()).toBe(404);
|
||||
expect((await request.get("/wp-config.php")).status()).toBe(404);
|
||||
expect((await request.get("/readme.html")).status()).toBe(404);
|
||||
expect((await request.get("/license.txt")).status()).toBe(404);
|
||||
expect((await request.get("/wp-admin/install.php")).status()).toBe(404);
|
||||
expect((await request.get("/wp-content/mu-plugins/")).status()).toBe(404);
|
||||
expect((await request.get("/wp-cron.php")).status()).toBe(403);
|
||||
expect((await request.get("/wp-content/uploads/probe.php")).status()).toBe(403);
|
||||
expect((await request.get("/wp-json/wp/v2/users")).status()).toBe(404);
|
||||
expect((await request.get("/?post_type=azl_show&name=le-cose-che-restano")).status()).toBe(404);
|
||||
|
||||
@@ -22,12 +22,37 @@ grep -q 'azionelab-apache-hardening.conf' /workspace/wordpress/Dockerfile || {
|
||||
exit 1
|
||||
}
|
||||
|
||||
grep -q '^FROM wordpress:7\.0\.2-php8\.3-apache$' /workspace/wordpress/Dockerfile || {
|
||||
echo "The WordPress image must use the patched 7.0.2 PHP 8.3 Apache tag." >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
grep -q 'wp-content/(uploads|files)' /workspace/wordpress/apache-hardening.conf || {
|
||||
echo "Apache hardening must block PHP execution below uploads/files." >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
grep -q 'wp-content/mu-plugins' /workspace/wordpress/apache-hardening.conf || {
|
||||
echo "Apache hardening must block direct mu-plugin probing." >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
grep -q "'publicly_queryable'[[:space:]]*=>[[:space:]]*false" /workspace/wordpress/mu-plugins/azionelab-content.php || {
|
||||
echo "Structured show content must not be exposed as standalone public routes." >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
grep -q "location = /wp-cron.php" /workspace/nginx/proxy-routes.conf || {
|
||||
echo "NGINX must block external wp-cron.php requests." >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
grep -q "location = /readme.html" /workspace/nginx/proxy-routes.conf || {
|
||||
echo "NGINX must hide the WordPress readme.html file." >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
grep -q "location ^~ /wp-content/mu-plugins/" /workspace/nginx/proxy-routes.conf || {
|
||||
echo "NGINX must hide the mu-plugins directory from public probing." >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM wordpress:7.0.0-php8.3-apache
|
||||
FROM wordpress:7.0.2-php8.3-apache
|
||||
|
||||
COPY php.ini /usr/local/etc/php/conf.d/azionelab.ini
|
||||
COPY .htaccess /opt/azionelab/.htaccess
|
||||
|
||||
@@ -10,6 +10,14 @@ TraceEnable Off
|
||||
Require all denied
|
||||
</LocationMatch>
|
||||
|
||||
<LocationMatch "^/(readme\.html|license\.txt|wp-admin/install\.php|wp-cron\.php)$">
|
||||
Require all denied
|
||||
</LocationMatch>
|
||||
|
||||
<LocationMatch "^/wp-content/mu-plugins/">
|
||||
Require all denied
|
||||
</LocationMatch>
|
||||
|
||||
<LocationMatch "^/wp-admin/includes/">
|
||||
Require all denied
|
||||
</LocationMatch>
|
||||
|
||||
Reference in New Issue
Block a user