.htaccess para Apache y LiteSpeed

Última revisión: 2 de octubre de 2021

Si utilizas Apache HTTPD o LiteSpeed o cualquier sistema basado en estos servidores web quizá te interese configurar el fichero .htaccess de la raíz de la configuración de tu WordPress de una manera compleja (más de la que viene por defecto).

Configuración detallada por bloques

HTTPS via Proxy

Es posible que haya un proxy por delante de tu sitio web, por lo que si es así, debemos informar al sistema para decirle que ya está activado y que no ejecute determinados elementos.

SetEnvIf X-Forwarded-Proto https HTTPS=on

Módulo PageSpeed

Aunque puede ser una buena solución en determinados casos, por defecto configuraremos el PageSpeed Module de forma inactiva por defecto ya que habrá varias configuraciones incluidas que hace este módulo en nuestra configuración.

<IfModule pagespeed_module>
  ModPagespeed off
</IfModule>

Bloqueo de ficheros de servidor

Por defecto bloquearemos los accesos externos a los ficheros de configuración, y este mismo.

<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>

Activar el CORS para algunos tipos

En algunos casos los ficheros de fuentes necesitan ser llamados desde distintas

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

Desactivar el listado de directorios

Algunos sistemas por defecto permiten que al acceder a una carpeta que no tenga un fichero de ejecución por defecto muestre todos los contenidos. Si este es el caso, podemos desactivar esta opción por defecto.

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

Ficheros por defecto

Por defecto configuraremos que el servidor web haga llamadas al index.php, y en última instancia, que llame al fichero principal de WordPress. Además de decirle que siga y use los SymLinks.

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

Dominio canónico

Aunque WordPress gestiona bien las redirecciones y los dominios principales, en los casos en los que solo utilicemos un dominio, lo mejor es hacer esa gestión controlada desde el .htaccess. En este caso configuramos tres elementos.

Configuramos el dominio principal y realizamos / forzamos su uso. Posteriormente verificamos si está o no activado el HTTPS y, si no se está haciendo la petición, se fuerza.

  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]

Bloqueo de peticiones «raras»

Por norma general suelen realizarse peticiones GET y POST, incluso HEAD. Otros tipos de peticiones suelen ser extrañas y en muy pocos casos serán necesarias para el uso de WordPress.

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

Bloqueo de accesos a ficheros con datos

Algunos ficheros habituales en WordPress incluyen información concreta sobre versiones y algunos datos. Gracias a este código bloqueamos los accesos a ficheros de lectura o de licencias.

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

Ficheros importantes de WordPress

Existen algunos ficheros que por la información que contienen o por su falta de uso puede ser interesenta bloquear. El principal es el wp-config que incluye mucha información. Otros son el wp-cron (que se recomienda ejecutar mediante un administrador de tareas, los ficheros de instalación de WordPress (una vez hecha la instalación estos no son necesarios ejecutar) y un fichero histórico de gestión de enlaces (que la mayoría de instalaciones no utilizan).

  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]

Bloqueo de listado de usuarios

Aunque no es un elemento grave que se sepa la lista de usuarios de un sitio, en algunos casos es posible que no se quiera tener acceso a las fichas o páginas de la lista de usuarios. Para evitar esto podemos bloquear los accesos habituales que incluye el propio sistema de enlaces permanentes.

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

Bloqueo de listados de carpetas

Una de las formas de detección de la existencia de elementos (plugins, themes…) es la detección de la existencia de carpetas. Para que las herramientas de pentesting tengan más problemas, es mejor aplicar un código 404.

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

Bloqueo de ficheros «inseguros»

En algunas carpetas existen una serie de elementos que, por norma general, no tienen porqué estar o ejecutarse.

  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]

Otros bloqueos

Algunos ficheros generales de configuración, de logs y similares que por norma general no tienen que ser accesibles desde fuera.

  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

Por norma general es mejor bloquear el posible ataque que se puede llevar a cabo de forma sencilla con la carga de todos los scripts. Para evitarlo es mejor bloquear su carga.

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

Bloqueo del XML-RPC

Si no usas esta tecnología es mejor bloquearla e impedir ataques o accesos inconvenientes. Suele usarse con la App de WordPress o para realizar Pingbacks.

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

Bloqueo de la ejecución de PHP en los «uploads»

En la carpeta de subida de ficheros no debería haber ficheros PHP, pero es un lugar donde muchos ataques se producen. En este caso bloquearemos su ejecución.

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

Control de los wp-includes

En la carpeta de los includes suelen haber elementos que no suelen ejecutarse de forma externa, y por esto, en general, es mejor bloquear su ejecución.

  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]

Control simple de ataques XSS

Evita ataques muy sencillos de cross-scripting mediante la 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]

Un detalle importante es que si dentro del panel gestionas códigos de publicidad u otros tipos de scripts, este sistema puede ser muy agresivo según la segunda línea (la que incluye los scripts), por lo que se podría eliminar.

Código por defecto de WordPress

Integramos el código que nos recomienda y suele generar el propio WordPress.

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

Mejorar las peticiones inseguras

Con esto todas las peticiones internas que se hagan del sitio desde HTTP pasarán automáticamente a HTTPS. Además se añaden otros sistemas para forzar que la navegación se haga exclusivamente por 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>

Caché de estáticos

Podemos activar una caché similar a la que realizan las CDN en los navegadores de los usuarios.

<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>

Compresión de los contenidos

Podemos reducir el tamaño de los ficheros que se pueden comprimir (principalmente textos).

<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>

Contenido completo del .htaccess

El fichero completo quedaría de la siguiente manera:

# 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


Sobre este documento

Este documento está regulado por la licencia EUPL v1.2, publicado en WP SysAdmin y creado por Javier Casares. Por favor, si utilizas este contenido en tu sitio web, tu presentación o cualquier material que distribuyas, recuerda hacer una mención a este sitio o a su autor, y teniendo que poner el material que crees bajo licencia EUPL.