wordpress

WordPress is among the most popular content management systems.

Apache

wordpress_headers
    Header always set Referrer-Policy "strict-origin-when-cross-origin"
    Header always set X-Content-Type-Options "nosniff"
    Header always append X-Frame-Options SAMEORIGIN
# Header always unset X-Content-Powered-By
    Header always unset Link
    Header always set Permissions-Policy "accelerometer=(), camera=(), gyroscope=(), magnetometer=(), microphone=(), midi=(), usb=(), gamepad=(), serial=()"
    Header always set X-XSS-Protection "1; mode=block"
    Header always append Content-Security-Policy "frame-ancestors 'self'"
wordpress
# special MIME type for icons
    AddType image/vnd.microsoft.icon .ico
    AddType application/font-woff2   .woff2
# now we have icon MIME type, we can use it

# GZIP compression for text files: HTML, CSS, JS, Text, XML, fonts
    <IfModule mod_deflate.c>
	AddOutputFilterByType DEFLATE application/javascript
	AddOutputFilterByType DEFLATE application/rss+xml
	AddOutputFilterByType DEFLATE application/vnd.ms-fontobject
	AddOutputFilterByType DEFLATE application/x-font
	AddOutputFilterByType DEFLATE application/x-font-opentype
	AddOutputFilterByType DEFLATE application/x-font-otf
	AddOutputFilterByType DEFLATE application/x-font-truetype
	AddOutputFilterByType DEFLATE application/x-font-ttf
	AddOutputFilterByType DEFLATE application/x-javascript
	AddOutputFilterByType DEFLATE application/xhtml+xml
	AddOutputFilterByType DEFLATE application/xml
	AddOutputFilterByType DEFLATE font/opentype
	AddOutputFilterByType DEFLATE font/otf
	AddOutputFilterByType DEFLATE font/ttf
	AddOutputFilterByType DEFLATE image/svg+xml
	AddOutputFilterByType DEFLATE image/x-icon
	AddOutputFilterByType DEFLATE text/css
	AddOutputFilterByType DEFLATE text/html
	AddOutputFilterByType DEFLATE text/javascript
	AddOutputFilterByType DEFLATE text/plain
	AddOutputFilterByType DEFLATE text/xml
    </IfModule>

    ExpiresActive On
    ExpiresByType application/javascript        "access plus 49 hours"
    ExpiresByType application/json              "access plus 0 seconds"
    ExpiresByType application/x-javascript      "access plus 49 hours"
    ExpiresByType image/gif                     "access plus  5 weeks"
    ExpiresByType image/jpeg                    "access plus  5 weeks"
    ExpiresByType image/png                     "access plus  5 weeks"
    ExpiresByType image/svg			    "access plus  5 weeks"
    ExpiresByType text/css                      "access plus  1 weeks"
    ExpiresByType text/javascript               "access plus  1 weeks"
    ExpiresByType text/plain                    "access plus  1 weeks"
# my favicon doesn't change much
    ExpiresByType image/vnd.microsoft.icon	"access plus 3 months"
    ExpiresByType application/font-woff		"access plus  7 days"
    ExpiresByType application/font-woff2	"access plus  7 days"

    <Directory {{ webmut_basedir }}/{{ item.name }}/public_html/>
{% if item.apache_whitelist is defined %}
	<RequireAny>
{% for info in item.apache_whitelist_admin|default([]) + item.apache_whitelist %}
{% if info.name is defined %}
# {{ info.name }}
{% endif %}
	    Require ip {{ info.ip }}
{% endfor %}
	</RequireAny>
{% endif %}
    </Directory>

    <Directory {{ webmut_basedir }}/{{ item.name }}/public_html/wp-admin>
{% if item.apache_whitelist_admin is defined %}
	 <RequireAny>
{% for info in item.apache_whitelist_admin %}
{% if info.name is defined %}
# {{ info.name }}
{% endif %}
	      Require ip {{ info.ip }}
{% endfor %}
	 </RequireAny>
{% endif %}
    </Directory>
    <Location /wp-admin/admin-ajax.php>
        Order deny,allow
        Allow from all
        Satisfy any
    </Location>

    <Directory "{{ webmut_basedir }}/{{ item.name }}/public_html/wp-admin/css/>
	Order deny,allow
	Allow from all
	Satisfy any
    </Directory>

    <Location "/xmlrpc.php">
	<RequireAny>
{% for info in item.apache_whitelist_admin %}
{% if info.name is defined %}
# {{ info.name }}
{% endif %}
	    Require ip {{ info.ip }}
{% endfor %}
        </RequireAny>
    </Location>

{% if item.apache_whitelist_admin is defined %}
    <Location /wp-login.php>
	<RequireAny>
{% for info in item.apache_whitelist_admin %}
{% if info.name is defined %}
# {{ info.name }}
{% endif %}
	    Require ip {{ info.ip }}
{% endfor %}
        </RequireAny>
    </Location>
{% endif %}


    RewriteEngine on
# CGR 10/1/2020
    RewriteCond %{REQUEST_METHOD} POST
    RewriteCond %{HTTP_USER_AGENT} "HeadlessChrome" [NC]
    RewriteRule .* - [F]
#
    RewriteRule ^/.*license.txt$ -  [R=404,L]
    RewriteRule ^/.*readme.(html|txt)$ -  [R=404,L]
    RewriteRule ^/.*FAQ$ -  [R=404,L]
    RewriteRule ^/.*README(.md)?$ -  [R=404,L]
    RewriteRule /.well-known	-	[L]
    RewriteRule ^/phpMyAdmin    - [L]
    RewriteCond %{REQUEST_URI} !^/wp-admin [NC]
    RewriteCond %{QUERY_STRING} author=\d [NC,OR]
    RewriteCond %{QUERY_STRING} author=\{num 
    RewriteRule ^ - [L,R=403]
    RewriteRule ^(.*)wp-config(.*)\.php(.*)$ [R=404,L]
    RewriteRule ^/wp-admin$ /wp-admin/ [R=301,L]
    RewriteRule ^/wp-admin/includes/ - [F,L]
    RewriteRule ^/wp-content/uploads/.+\.php - [F,L]
    RewriteRule ^/wp-content/uploads/dlm_uploads/ - [F,L]
    RewriteRule ^/wp-cron.php - [R=404,L]
    RewriteRule ^/wp-includes/[^/]+\.php$ - [F,L]
    RewriteRule ^/wp-includes/js/tinymce/langs/.+\.php - [F,L]
    RewriteRule ^/wp-includes/theme-compat/ - [F,L]
#
    RewriteRule ^/wp-(content|admin|includes) - [L]
    RewriteRule ^/.*\.php$ - [L]
    RewriteRule ^/.*\.([a-zA-Z0-9]{1,3})$ - [L]
    RewriteRule ^/.*\.(html)$ - [L]
    RewriteRule ^/index\.php$ - [L]
    RewriteCond %{DOCUMENT_ROOT}/%{REQUEST_FILENAME} !-f
    RewriteCond %{DOCUMENT_ROOT}/%{REQUEST_FILENAME} !-d
    RewriteRule . /index.php [L]

Audit

wordpress_check.py
#!/usr/bin/python3
import requests
import random
site = 'https://www.globalsp.com/'
s = requests.session()

headers = {
    'Cache-Control': 'no-cache',
    'Pragma': 'no-cache'
}
for filename in [
    'license.txt',
    'readme.html',
    'wp-content/uploads/dlm_uploads/index.html',
    ]:
    url = site + filename
    status_code = s.get(url, allow_redirects=False, headers=headers, params={ 'rand': random.randint(1,65535)}).status_code
    print(filename.ljust(32), status_code, '✅' if status_code in (403, 404) else '❌')

for filename in [
    '/wp-admin/admin-ajax.php',
    ]:
    url = site + filename
    status_code = s.get(url, allow_redirects=False, headers=headers, params={ 'rand': random.randint(1,65535)}).status_code
    print(filename.ljust(32), status_code, '✅' if status_code == 200 else '❌')

for filename in [
    'wp-content/uploads/foobar.php',
    'wp-includes/class-feed.php',
    'wp-includes/block-patterns.php',
    ]:
    url = site + filename
    status_code = s.get(url, allow_redirects=False, headers=headers, params={ 'rand': random.randint(1,65535)}).status_code
    print(filename.ljust(32), status_code, '✅' if status_code == 403 else '❌')

wordpress_check.py output
license.txt                      404 ✅
readme.html                      404 ✅
wp-content/uploads/dlm_uploads/index.html 403 ✅
wp-content/uploads/foobar.php    403 ✅
wp-includes/class-feed.php       403 ✅
wp-includes/block-patterns.php   403 ✅