.htaccess for Apache and LiteSpeed

Last Revised: October 2, 2021

If you use Apache HTTPD or LiteSpeed or any system based on these web servers you may be interested in configuring the .htaccess file of the root of your WordPress configuration in a complex way (more than the one that comes by default).

Detailed configuration by blocks

HTTPS via Proxy

It is possible that there is a proxy in front of your website, so if so, we must inform the system to tell it that it is already activated and not to run certain elements.

SetEnvIf X-Forwarded-Proto https HTTPS=on

PageSpeed Module

Although it can be a good solution in certain cases, by default we will configure the PageSpeed Module inactively by default since there will be several configurations included that this module makes in our configuration.

<IfModule pagespeed_module>
  ModPagespeed off
</IfModule>

Locking server files

By default we will block external access to the configuration files, and this one.

<FilesMatch "^.ht">
  Deny from all
</FilesMatch>
<FilesMatch "^.ftp">
  Deny from all
</FilesMatch>
<FilesMatch "^php.ini$">
  Deny from all
</FilesMatch>
<FilesMatch "^.well-known">
  Allow from all
</FilesMatch>

Enable CORS for some types

In some cases font files need to be called from different

<FilesMatch ".(?:ttf|eot|woff|otf)$">
  Header set Access-Control-Allow-Origin "*"
</FilesMatch>

Disable directory listing

Some default systems allow that when accessing a folder that does not have a default execution file it shows all the contents. If this is the case, we can disable this option by default.

<IfModule mod_autoindex.c>
  Options -Indexes
</IfModule>

Default files

By default we will configure that the web server makes calls to the index.php, and ultimately, to call the main WordPress file. In addition to telling you to follow and use the SymLinks.

DirectoryIndex index.php index.html /index.php
Options None
Options FollowSymLinks
ServerSignature Off

Canonical domain

Although WordPress manages redirects and main domains well, in cases where we only use one domain, it is best to do that management controlled from the .htaccess. In this case we configure three elements.

We configure the main domain and perform/force its use. Later we check whether or not HTTPS is activated and, if the request is not being made, it is forced.

  RewriteCond %{HTTP_HOST} !www.example.com
  RewriteRule (.*) https://www.example.com%{REQUEST_URI} [L,R=301]
  RewriteCond %{HTTPS} !on
  RewriteRule (.*) https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]

Blocking “rare” requests

As a general rule, GET and POST requests are usually made, including HEAD. Other types of requests are usually strange and in very few cases will be necessary for the use of WordPress.

  RewriteCond %{REQUEST_METHOD} ^(TRACE|TRACK)
  RewriteRule .* - [F]

Blocking access to files with data

Some common files in WordPress include specific information about versions and some data. Thanks to this code we block access to reading or license files.

  RewriteRule readme.(html|txt) - [L,R=404]
  RewriteRule (licencia|license|LICENSE|olvasdel|lisenssi|liesmich).(html|txt) - [L,R=404]

Important WordPress files

There are some files that due to the information they contain or their lack of use may be interesting to block. The main one is the wp-config which includes a lot of information. Others are the wp-cron (which is recommended to run through a task manager, the WordPress installation files (once the installation is done these are not necessary to run) and a historical link management file (which most installations do not use).

  RewriteRule ^wp-config - [L,R=404]
  RewriteRule ^wp-cron.php - [L,R=404]
  RewriteRule ^wp-admin/(install|setup-config|upgrade).php - [L,R=404]
  RewriteRule ^wp-admin/maint/repair.php - [L,R=404]
  RewriteRule ^wp-links-opml.php$ - [L,R=404]

Blocking user listing

Although it is not a serious item that you know the list of users on a site, in some cases you may not want to access the tabs or pages in the list of users. To avoid this we can block the usual accesses that includes the system of permanent links itself.

  RewriteCond %{QUERY_STRING} ^author= [NC]
  RewriteRule .* - [F,L]
  RewriteRule ^author/ - [F,L]

Blocking folder listings

One of the ways of detecting the existence of elements (plugins, themes…) is the detection of the existence of folders. In order for pentesting tools to have more problems, it is better to apply a 404 code.

  RewriteRule ^wp-content/mu-plugins/$ - [L,R=404]
  RewriteRule ^wp-content/(plugins|themes)/(.+)/$ - [L,R=404]

Blocking “insecure” files

In some folders there are a number of elements that, as a general rule, do not have to be or executed.

  RewriteRule ^wp-content/(?:uploads|files)/.+.(html|js|php|shtml|swf)$ - [L,R=403]
  RewriteRule ^wp-content/plugins/.+.(aac|avi|bz2|cur|docx?|eot|exe|flv|gz|heic|htc|m4a|midi?|mov|mp3|mp4|mpe?g|ogg|ogv|otf|pdf|pptx?|rar|rtf|tar|tgz|tiff?|ttc|wav|wmv|xlsx?|zip) - [L,R=404]

Other blockages

Some general configuration files, logs and the like that as a general rule do not have to be accessible from outside.

  RewriteRule ^sftp-config.json - [L,R=404]
  RewriteRule (access|error)_log - [L,R=404]
  RewriteRule installer-log.txt - [L,R=404]
  RewriteRule wp-content/debug.log - [L,R=404]
  RewriteRule (^#.*#|.(bak|config|dist|fla|inc|ini|log|psd|sh|sql|sw[op])|~)$ - [L,R=404]

Mitigation CVE-2018-6389

As a general rule, it is better to block the possible attack that can be carried out easily with the loading of all the scripts. To avoid this it is better to block its charge.

<FilesMatch "load-(scripts|styles).php">
  Deny from all
</FilesMatch>

Blocking XML-RPC

If you do not use this technology it is better to block it and prevent attacks or inconvenient access. It is usually used with the WordPress App or to perform Pingbacks.

<FilesMatch "xmlrpc.php">
	Deny from all
</FilesMatch>

Blocking PHP execution in uploads

There should be no PHP files in the file upload folder, but it is a place where many attacks occur. In this case we will block its execution.

<FilesMatch "wp-content/uploads/(.+).php">
  Deny from all
</FilesMatch>

Controlling wp-includes

In the folder of the includes there are usually elements that do not usually run externally, and therefore, in general, it is better to block their execution.

  RewriteRule ^wp-admin/includes/ - [F,L]
  RewriteRule !^wp-includes/ - [S=3]
  RewriteRule ^wp-includes/[^/]+.php$ - [F,L]
  RewriteRule ^wp-includes/js/tinymce/langs/.+.php - [F,L]
  RewriteRule ^wp-includes/theme-compat/ - [F,L]

Simple control of XSS attacks

Avoid very simple cross-scripting attacks using the URL.

  RewriteCond %{QUERY_STRING} (<|%3C).*script.*(>|%3E) [NC,OR]
  RewriteCond %{QUERY_STRING} GLOBALS(=|[|%[0-9A-Z]{0,2}) [OR]
  RewriteCond %{QUERY_STRING} _REQUEST(=|[|%[0-9A-Z]{0,2})
  RewriteRule ^(.*)$ index.php [F,L]

An important detail is that if within the panel you manage advertising codes or other types of scripts, this system can be very aggressive according to the second line (the one that includes the scripts), so it could be eliminated.

Default WordPress Code

We integrate the code that recommends us and usually generates the WordPress itself.

  RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
  RewriteRule ^index.php$ - [L]
  RewriteCond %{REQUEST_FILENAME} !-f
  RewriteCond %{REQUEST_FILENAME} !-d
  RewriteRule . /index.php [L]

Improve insecure requests

With this, all internal requests made to the site from HTTP will automatically pass to HTTPS. In addition, other systems are added to force navigation to be done exclusively by HTTPS (HSTS).

<IfModule mod_headers.c>
  Header always set Referrer-Policy "strict-origin-when-cross-origin"
  Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
  Header always set Content-Security-Policy: upgrade-insecure-requests;
  Header always set X-XSS-Protection "1; mode=block"
  Header always set X-Content-Type-Options "nosniff"
  Header always set X-Frame-Options "DENY"
  Header unset Pragma
  Header always unset WP-Super-Cache
  Header always unset X-Pingback
</IfModule>

Static cache

We can activate a cache similar to that performed by CDNs in users’ browsers.

<IfModule mod_expires.c>
  ExpiresActive on
  #Varios
  ExpiresByType application/pdf A2592000
  ExpiresByType image/x-icon A2592000
  ExpiresByType image/vnd.microsoft.icon A2592000
  ExpiresByType image/svg+xml A2592000
  #Imagenes
  ExpiresByType image/jpg A2592000
  ExpiresByType image/jpeg A2592000
  ExpiresByType image/png A2592000
  ExpiresByType image/gif A2592000
  ExpiresByType image/webp A2592000
  #Media
  ExpiresByType video/ogg A2592000
  ExpiresByType audio/ogg A2592000
  ExpiresByType video/mp4 A2592000
  ExpiresByType video/webm A2592000
  #CSS/JS
  ExpiresByType text/css A2592000
  ExpiresByType text/javascript A2592000
  ExpiresByType application/javascript A2592000
  ExpiresByType application/x-javascript A2592000
  #Fuentes
  ExpiresByType application/x-font-ttf A2592000
  ExpiresByType application/x-font-woff A2592000
  ExpiresByType application/font-woff A2592000
  ExpiresByType application/font-woff2 A2592000
  ExpiresByType application/vnd.ms-fontobject A2592000
  ExpiresByType font/ttf A2592000
  ExpiresByType font/woff A2592000
  ExpiresByType font/woff2 A2592000
</IfModule>

Content compression

We can reduce the size of the files that can be compressed (mainly texts).

<IfModule mod_deflate.c>
  AddOutputFilterByType DEFLATE application/javascript
  AddOutputFilterByType DEFLATE application/x-javascript
  AddOutputFilterByType DEFLATE application/json
  AddOutputFilterByType DEFLATE application/ld+json
  AddOutputFilterByType DEFLATE application/manifest+json
  AddOutputFilterByType DEFLATE application/rdf+xml
  AddOutputFilterByType DEFLATE application/rss+xml
  AddOutputFilterByType DEFLATE application/schema+json
  AddOutputFilterByType DEFLATE application/vnd.geo+json
  AddOutputFilterByType DEFLATE application/x-web-app-manifest+json
  AddOutputFilterByType DEFLATE application/vnd.ms-fontobject
  AddOutputFilterByType DEFLATE application/x-font-ttf
  AddOutputFilterByType DEFLATE application/xhtml+xml
  AddOutputFilterByType DEFLATE application/xml
  AddOutputFilterByType DEFLATE font/opentype
  AddOutputFilterByType DEFLATE font/eot
  AddOutputFilterByType DEFLATE image/bmp
  AddOutputFilterByType DEFLATE image/svg+xml
  AddOutputFilterByType DEFLATE image/x-icon
  AddOutputFilterByType DEFLATE image/vnd.microsoft.icon
  AddOutputFilterByType DEFLATE text/javascript
  AddOutputFilterByType DEFLATE text/css
  AddOutputFilterByType DEFLATE text/html
  AddOutputFilterByType DEFLATE text/plain
  AddOutputFilterByType DEFLATE text/x-component
  AddOutputFilterByType DEFLATE text/xml
</IfModule>

Full content of .htaccess

The complete file would be as follows:

# BEGIN HTTPS via Proxy
SetEnvIf X-Forwarded-Proto https HTTPS=on
# END HTTPS via Proxy

# BEGIN ModPagespeed
<IfModule pagespeed_module>
  ModPagespeed off
</IfModule>
# END ModPagespeed

# START - [Seguridad] Bloqueado el acceso a ficheros ".ht"
<FilesMatch "^.ht">
  Deny from all
</FilesMatch>
<FilesMatch "^.ftp">
  Deny from all
</FilesMatch>
<FilesMatch "^php.ini$">
  Deny from all
</FilesMatch>
<FilesMatch "^.well-known">
  Allow from all
</FilesMatch>
# END

# BEGIN cors
<FilesMatch ".(?:ttf|eot|woff|otf)$">
  Header set Access-Control-Allow-Origin "*"
</FilesMatch>
#END

# BEGIN Directory browsing
<IfModule mod_autoindex.c>
  Options -Indexes
</IfModule>
# END Directory browsing

DirectoryIndex index.php index.html /index.php
Options None
Options FollowSymLinks
ServerSignature Off


# START Expresión regular de WordPress
<IfModule mod_rewrite.c>
  RewriteEngine On

  # [Seguridad] Configuracion del dominio canonico
  RewriteCond %{HTTP_HOST} !www.example.com
  RewriteRule (.*) https://www.example.com%{REQUEST_URI} [L,R=301]
  RewriteCond %{HTTPS} !on
  RewriteRule (.*) https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]

  # [Seguridad] Bloqueamos la peticiones TRACE y TRACK
  RewriteCond %{REQUEST_METHOD} ^(TRACE|TRACK)
  RewriteRule .* - [F]

  # [Seguridad] Ficheros estáticos generales
  RewriteRule readme.(html|txt) - [L,R=404]
  RewriteRule (licencia|license|LICENSE|olvasdel|lisenssi|liesmich).(html|txt) - [L,R=404]

  # [Seguridad] Ficheros propios de WordPress
  RewriteRule ^wp-config - [L,R=404]
  RewriteRule ^wp-cron.php - [L,R=404]
  RewriteRule ^wp-admin/(install|setup-config|upgrade).php - [L,R=404]
  RewriteRule ^wp-admin/maint/repair.php - [L,R=404]
  RewriteRule ^wp-links-opml.php$ - [L,R=404]

  # [Seguridad] Bloqueo del listing de usuarios
  RewriteCond %{QUERY_STRING} ^author= [NC]
  RewriteRule .* - [F,L]
  RewriteRule ^author/ - [F,L]

  # [Seguridad] Bloqueo de listados de carpetas
  RewriteRule ^wp-content/mu-plugins/$ - [L,R=404]
  RewriteRule ^wp-content/(plugins|themes)/(.+)/$ - [L,R=404]

  # [Seguridad] Bloqueo de ficheros inseguros
  RewriteRule ^wp-content/(?:uploads|files)/.+.(html|js|php|shtml|swf)$ - [L,R=403]
  RewriteRule ^wp-content/plugins/.+.(aac|avi|bz2|cur|docx?|eot|exe|flv|gz|heic|htc|m4a|midi?|mov|mp3|mp4|mpe?g|ogg|ogv|otf|pdf|pptx?|rar|rtf|tar|tgz|tiff?|ttc|wav|wmv|xlsx?|zip) - [L,R=404]

  # [Seguridad] Otros bloqueos
  RewriteRule ^sftp-config.json - [L,R=404]
  RewriteRule (access|error)_log - [L,R=404]
  RewriteRule installer-log.txt - [L,R=404]
  RewriteRule wp-content/debug.log - [L,R=404]
  RewriteRule (^#.*#|.(bak|config|dist|fla|inc|ini|log|psd|sh|sql|sw[op])|~)$ - [L,R=404]

</IfModule>
# END

# START - [Seguridad] Mitigation CVE-2018-6389
<FilesMatch "load-(scripts|styles).php">
  Deny from all
</FilesMatch>
# END

# START - [Seguridad] XML-RPC
<FilesMatch "xmlrpc.php">
	Deny from all
</FilesMatch>
# END

# START - [Seguridad] No ejecutar ficheros en Uploads
<FilesMatch "wp-content/uploads/(.+).php">
  Deny from all
</FilesMatch>
# END

# BEGIN WordPress
<IfModule mod_rewrite.c>
  RewriteEngine On
  RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]

  ### START WP includes
  RewriteRule ^wp-admin/includes/ - [F,L]
  RewriteRule !^wp-includes/ - [S=3]
  RewriteRule ^wp-includes/[^/]+.php$ - [F,L]
  RewriteRule ^wp-includes/js/tinymce/langs/.+.php - [F,L]
  RewriteRule ^wp-includes/theme-compat/ - [F,L]
  ### END WP includes

  ### START SQL Injection
  RewriteCond %{QUERY_STRING} (<|%3C).*script.*(>|%3E) [NC,OR]
  RewriteCond %{QUERY_STRING} GLOBALS(=|[|%[0-9A-Z]{0,2}) [OR]
  RewriteCond %{QUERY_STRING} _REQUEST(=|[|%[0-9A-Z]{0,2})
  RewriteRule ^(.*)$ index.php [F,L]
  ### END SQL Injection

  RewriteRule ^index.php$ - [L]
  RewriteCond %{REQUEST_FILENAME} !-f
  RewriteCond %{REQUEST_FILENAME} !-d
  RewriteRule . /index.php [L]
</IfModule>
# END WordPress

# BEGIN Security Headers
<IfModule mod_headers.c>
  Header always set Referrer-Policy "strict-origin-when-cross-origin"
  Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
  Header always set Content-Security-Policy: upgrade-insecure-requests;
  Header always set X-XSS-Protection "1; mode=block"
  Header always set X-Content-Type-Options "nosniff"
  Header always set X-Frame-Options "DENY"
  Header unset Pragma
  Header always unset WP-Super-Cache
  Header always unset X-Pingback
</IfModule>
# END Strict-Transport-Security

# BEGIN EXPIRES
<IfModule mod_expires.c>
  ExpiresActive on
  #Varios
  ExpiresByType application/pdf A2592000
  ExpiresByType image/x-icon A2592000
  ExpiresByType image/vnd.microsoft.icon A2592000
  ExpiresByType image/svg+xml A2592000
  #Imagenes
  ExpiresByType image/jpg A2592000
  ExpiresByType image/jpeg A2592000
  ExpiresByType image/png A2592000
  ExpiresByType image/gif A2592000
  ExpiresByType image/webp A2592000
  #Media
  ExpiresByType video/ogg A2592000
  ExpiresByType audio/ogg A2592000
  ExpiresByType video/mp4 A2592000
  ExpiresByType video/webm A2592000
  #CSS/JS
  ExpiresByType text/css A2592000
  ExpiresByType text/javascript A2592000
  ExpiresByType application/javascript A2592000
  ExpiresByType application/x-javascript A2592000
  #Fuentes
  ExpiresByType application/x-font-ttf A2592000
  ExpiresByType application/x-font-woff A2592000
  ExpiresByType application/font-woff A2592000
  ExpiresByType application/font-woff2 A2592000
  ExpiresByType application/vnd.ms-fontobject A2592000
  ExpiresByType font/ttf A2592000
  ExpiresByType font/woff A2592000
  ExpiresByType font/woff2 A2592000
</IfModule>
# END EXPIRES

# BEGIN HttpHeadersCompression
<IfModule mod_deflate.c>
  AddOutputFilterByType DEFLATE application/javascript
  AddOutputFilterByType DEFLATE application/x-javascript
  AddOutputFilterByType DEFLATE application/json
  AddOutputFilterByType DEFLATE application/ld+json
  AddOutputFilterByType DEFLATE application/manifest+json
  AddOutputFilterByType DEFLATE application/rdf+xml
  AddOutputFilterByType DEFLATE application/rss+xml
  AddOutputFilterByType DEFLATE application/schema+json
  AddOutputFilterByType DEFLATE application/vnd.geo+json
  AddOutputFilterByType DEFLATE application/x-web-app-manifest+json
  AddOutputFilterByType DEFLATE application/vnd.ms-fontobject
  AddOutputFilterByType DEFLATE application/x-font-ttf
  AddOutputFilterByType DEFLATE application/xhtml+xml
  AddOutputFilterByType DEFLATE application/xml
  AddOutputFilterByType DEFLATE font/opentype
  AddOutputFilterByType DEFLATE font/eot
  AddOutputFilterByType DEFLATE image/bmp
  AddOutputFilterByType DEFLATE image/svg+xml
  AddOutputFilterByType DEFLATE image/x-icon
  AddOutputFilterByType DEFLATE image/vnd.microsoft.icon
  AddOutputFilterByType DEFLATE text/javascript
  AddOutputFilterByType DEFLATE text/css
  AddOutputFilterByType DEFLATE text/html
  AddOutputFilterByType DEFLATE text/plain
  AddOutputFilterByType DEFLATE text/x-component
  AddOutputFilterByType DEFLATE text/xml
</IfModule>
# END HttpHeadersCompression

Seguir con Seguridad para WordPress


About this document

This document is regulated by the EUPL v1.2 license, published in WP SysAdmin and created by Javier Casares. Please, if you use this content in your website, your presentation or any material you distribute, remember to mention this site or its author, and having to put the material you create under EUPL license.