Seguridad WordPress desde Cloudflare

Como ya adelantábamos en la entrada anterior «Seguridad WordPress en tres niveles», hoy vamos a hablar como securizar un poco más nuestra instalación de WordPress, además de que también la podemos hacer más rápida con el uso de su CDN y otras configuraciones.

Abarcar todas las opciones que nos ofrece Cloudflare se sale totalmente del alcance de este artículo, ya que podría ser un libro completo o un curso. Podríamos englobar desde opciones de la Web3, aplicaciones utilizando su API, los workers, Stream o Zero Trust para configurar nuestras aplicaciones y redes privadas sin usar VPN. Todo un mundo de posibilidades que cada día presenta algo nuevo.

Pero vamos a lo que nos interesa, la seguridad.

Mejorar la puntuación de nuestro certificado

Una de las primeras cosas que realizo cuando me encargo de una web es comprobar su «puntuación» del certificado desde el servicio de SSL Labs https://www.ssllabs.com/ssltest/index.html

Aquí si tienes menos de un A+, deberías realizar algunas mejoras.

Certificado grado B

Para eso, dentro de nuestra cuenta de Cloudflare y seleccionando el dominio, vamos a SSL/TLS -> Información general y en el modo de encriptación SSL/TLS seleccionamos Completo (estricto). Esta opción es la más segura, aunque evidentemente deberemos tener en el origen un certificado válido. En realidad esta configuración no va a influir en la puntuación final, pero sí que es la más adecuada y segura.

Después, en Certificados de perímetro seleccionamos Usar siempre HTTPS para que cualquier petición al protocolo http sea redirigida a https.

Configuramos la Seguridad de transporte estricta de HTTP (HSTS) con una edad máxima de 6 meses, desactivamos los subdominios si no tenemos webs instaladas en los mismos y activamos la precarga. Antes de realizar estos cambios debemos asegurarnos durante al menos unos días que nuestra web funciona perfectamente bajo https o podremos quedar sin acceso a la misma.

En la versión mínima de TLS seleccionamos TLS 1.2 y activamos la encriptación oportunista y TLS 1.3.

También activamos las reescrituras automáticas HTTPS que cambiará cualquier petición a un recurso http a su equivalente https y lo que también nos evitara el uso de un plugin para esto (que además hará a un nivel inferior como es el servidor o incluso desde PHP, por ejemplo el archiconocido Really Simple SSL).

Aunque tengamos activadas estas reescrituras a nivel de DNS, también deberíamos buscar en la Base de datos recursos de nuestro dominio con http y realizar el cambio por los correctos con https. Esto es algo muy fácil con WP CLI o con el siguiente script en PHP https://interconnectit.com/search-and-replace-for-wordpress-databases/

La siguiente opción a marcar Supervisión de transparencia de certificados es totalmente opcional, si la seleccionamos, recibiremos un correo cada vez que se emita o renueve un certificado para nuestro dominio o subdominios.

En cuando a SSL/TLS no tenemos que cambiar nada más. Ahora podemos volver a comprobar la puntuación de nuestro certificado.

Certificado grado A

Reglas de página y cabeceras

Voy a saltarme la pestaña de seguridad que dejo para el final y vamos ahora a ver la pestaña Reglas, comenzando con las reglas de página.

En la cuenta gratuita podemos crear hasta tres reglas de página. Si tenéis un formulario de contacto en el que queréis reducir el SPAM, podemos activar una regla con la dirección del mismo en el que activamos la integridad del navegador.

Regla contacto

Otra regla que suelo activar a la mayoría de clientes es la que securiza un poco más el login, para eso creamos una nueva regla y en la URL ponemos *nuestrodominio.com/wp-login.php* para que entre en funcionamiento en la versión con y sin www y aunque la URL tenga parámetros. En la configuración seleccionamos de nuevo la Comprobación de integridad del navegador y la activamos, después Nivel de seguridad y lo ponemos en I’m Under Attack y por último Desactivar Rendimiento.

Regla login

Con estas opciones evitaremos muchos intentos de ataques automatizados, de bots, etc. y cada vez que vayamos a conectarnos al panel de administración veremos la pantalla de comprobación de integridad del navegador de Cloudflare.

Comprobación de navegador de Cloudflare

Ahora vamos al apartado Reglas de transformación (10 disponibles en la versión gratuita) y pulsamos en Transformaciones administradas en la que marcaremos Eliminar los encabezados «X-Powered-by» y Agregar encabezados de seguridad.

Encabezados de seguridaad administrados

A continuación seleccionamos Crear Regla de transformación y en el desplegable Modificar el encabezado de respuesta.

Aquí le damos el nombre a nuestra regla y vamos a ver/crear tres:

  1. Nombre de la regla: Add Security headers
    • Expresión: (http.request.uri contains «» and not http.request.full_uri contains «.css» and not http.request.full_uri contains «.js» and not http.request.uri contains «.jpg» and not http.request.uri contains «.gif» and not http.request.uri contains «.png» and not http.request.uri contains «.webp»)
    • Y añadimos:
      • Set Static con el encabezado access-control-allow-credentials y valor a true
      • Set Static con el encabezado access-control-allow-headers y valor a *
      • Set Static con el encabezado access-control-allow-methods y valor a GET, POST, OPTIONS
      • Set Static con el encabezado access-control-allow-origin y valor a *
      • Set Static con el encabezado permissions-policy y valor a accelerometer=(), autoplay=(), camera=(), cross-origin-isolated=(), display-capture=(), document-domain=(), encrypted-media=(), fullscreen=(self), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), midi=(), payment=(), picture-in-picture=(), publickey-credentials-get=(), screen-wake-lock=(), sync-xhr=(self), usb=(), web-share=(), xr-spatial-tracking=()
      • Set Static con el encabezado referrer-policy y valor a strict-origin-when-cross-origin
      • Set Static con el encabezado x-permitted-cross-domain-policies y valor a none
  2. Nombre de la regla: Remove link header
    • Expresión: (http.request.uri contains «» and not http.request.full_uri contains «.css» and not http.request.full_uri contains «.js» and not http.request.uri contains «.jpg» and not http.request.uri contains «.gif» and not http.request.uri contains «.png» and not http.request.uri contains «.webp»)
    • Y añadimos Remove y en nombre del encabezado link
  3. Nombre de la regla: Rules Author
    • Expresión: (http.request.uri contains «»), es decir, lo aplicamos a todas las URL
    • Y añadimos:
      • Set Static con el encabezado x-cl-rules-author y valor a https://jclc.me/codeable
Modificar encabezados

Con estas acciones hemos configurado acciones como añadir cabeceras de seguridad XSS:

  • X-Content-Type-Options: nosniff
  • X-XSS-Protection: 1; mode=block
  • X-Frame-Options: SAMEORIGIN
  • Referrer-Policy: same-origin
  • Expect-CT: max-age=86400, enforce

y eliminación de las cabeceras que identifican el servidor con X-Powered-by.

Además, en las reglas de transformación hemos añadido algunas reglas adicionales de seguridad con la primera regla. Tenemos que tener muy claro lo que hacemos y saber cómo solucionarlo antes fallos, ya que estamos limitando determinadas peticiones, que en algunas web pueden ser necesarias. O por ejemplo, la web puede necesitar la localización que estamos limitando con permissions-policy y el valor geolocation=(). Podemos adaptarlo con https://www.permissionspolicy.com/, pero repito, que siempre, SIEMPRE, debemos saber lo que estamos haciendo.

En la segunda regla estamos eliminando varias cabeceras link que nos llevan a la API y otros servicios que en la gran mayoría de los casos no son necesarios, aunque utilicemos dichos servicios y lo único que aportan son nuevas pistas a atacantes.

La tercera como podéis ver no aporta nada a la seguridad y solo es una cabecera personalizada donde podéis añadir vuestro nombre, enlace, etc. 😉

Seguridad

Y por último vamos a la pestaña seguridad. En la opción de Configuración ajustamos el nivel de Seguridad a Medio y el Pasaje de desafío a 30 minutos. Habilitamos comprobación de integridad del navegador. También habilitamos Compatibilidad con Privacy Pass.

A continuación vamos a crear algunas reglas de seguridad, para lo que dentro de seguridad vamos a WAF (Web Application Firewall) y en reglas de Firewall tenemos la posibilidad de crear hasta 5 en la versión gratuita, pero veremos que podemos agrupar varias en una sola regla.

Lo primero que vamos a tener en cuenta es si usamos Redsys. En caso afirmativo lo primero que hacemos dentro de WAF es ir a Herramientas y ahí vamos añadiendo los tres rangos de IPs de Redsys (193.16.243.0/24, 194.224.159.0/24 y 195.76.9.0/24), con la acción permitir, seleccionando Este sitio web en Zona y poniendo en Notas Rango de IPs Redsys.

Como podemos ver, las reglas de Accesos IP se ejecutan antes que las reglas de firewall (y después de las reglas de página).

Reglas de acceso IP

Ahora vamos a Reglas de firewall y en el caso de utilizar Redsys, vamos a crear la siguiente regla:

Nombre: Permitir peticiones ASN Redsys
Expresión: (ip.geoip.asnum eq 31627)
Acción: Omitir -> Comprobación de integridad del navegador

Podemos utilizar el generador de expresiones para las reglas más sencillas (por defecto), o para copiar y pegar, editar las expresiones en formato texto.

Regla ASN de Redsys

La siguiente regla será la de bloquear peticiones al protocolo XML-RPC que ya vimos en el artículo anterior de Seguridad WordPress en tres niveles, para lo que creamos la siguiente regla:

Nombre: Bloquear peticiones XML-RPC
Expresión: (http.request.uri.path contains "/xmlrpc.php")
Acción: Bloquear

Creamos una nueva regla para bloquear todos los intentos de acceso a archivos PHP dentro del directorio wp-content. Ningún plugin o tema debería ejecutar código directamente en dicho directorio, si es así, es que está mal realizado o tiene que haber un motivo muy concreto y especial. La regla sería:

Nombre: Acceso PHP a wp-content
Expresión: (http.request.uri.path contains "/wp-content/" and http.request.uri.path contains ".php")
Acción: Bloquear

Si nuestro servidor web es Litespeed, entonces su propio plugin de caché, ejecuta un archivo PHP en el directorio (algo que deberían evitar), por lo que crearemos una excepción para dicho archivo y cambiamos la expresión a la siguiente: (http.request.uri.path contains "/wp-content/" and http.request.uri.path contains ".php" and not http.request.uri.path contains "/wp-content/plugins/litespeed-cache/guest.vary.php")

Como vemos, el procedimiento es sencillo y podemos encadenar reglas con and y or. Pero además podemos agrupar varias reglas siempre y cuando la acción sea la misma. Por ejemplo, las dos reglas anteriores tienen la acción de Bloquear, por lo que podemos agruparlas con el uso de paréntesis y la regla quedaría así:

(http.request.uri.path contains "/xmlrpc.php") or
(http.request.uri.path contains "/wp-content/" and http.request.uri.path contains ".php" and not http.request.uri.path contains "/wp-content/plugins/litespeed-cache/guest.vary.php")

Como vemos, esto nos da una gran potencia y vamos a verlo con la siguiente regla para evitar accesos no deseados:

Nombre: Bad access
Expresión: (cf.threat_score gt 14) or
(cf.threat_score gt 10 and cf.client.bot) or
(http.request.uri.query contains "author_name=" and not http.request.uri.path contains "/wp-admin/") or
(http.request.uri.query contains "author=" and not http.request.uri.path contains "/wp-admin/") or
(http.request.uri contains "/wp-json/wp/v2/users/" and not http.referer contains "/wp-admin/") or
(http.request.uri contains "wp-config.") or (http.request.uri contains "setup-config.") or
(http.request.uri.path contains ".js.map") or
(lower(http.request.uri.path) contains "phpmyadmin") or
(lower(http.request.uri.path) contains "thinkphp") or
(http.request.uri.path contains "/phpunit") or
(raw.http.request.uri contains "../") or (raw.http.request.uri contains "..%2F") or
(http.request.uri contains "passwd") or
(http.request.uri contains "/dfs/") or
(http.request.uri contains "/autodiscover/") or
(http.request.uri contains "/wpad.") or
(http.request.uri contains "/wallet.dat") or
(http.request.uri contains "webconfig.txt") or
(http.request.uri contains "vuln.") or
(http.request.uri contains ".env") or (http.request.uri contains ".ini") or (http.request.uri contains ".log") or (http.request.uri contains ".sql") or
(http.request.uri.query contains "bin.com/") or (http.request.uri.query contains "bin.net/") or (raw.http.request.uri.query contains "?%00") or
(http.request.uri.query contains "eval(") or (http.request.uri.query contains "base64") or (http.request.uri.query contains "var_dump") or
(http.request.uri.query contains "<script") or (raw.http.request.uri.query contains "%3Cscript") or
(http.request.full_uri contains "<?php") or
(http.cookie contains "<?php") or
(http.cookie contains "<script") or (http.referer contains "%3Cscript") or
(http.cookie contains "base64") or (http.cookie contains "var_dump") or
(upper(http.request.uri.query) contains "$_GLOBALS[") or
(upper(http.request.uri.query) contains "$_REQUEST[") or
(upper(http.request.uri.query) contains "$_POST[")

Acción: Bloquear

En el ejemplo anterior estamos evitando el acceso a archivos .sql (volcados de la base de datos con datos de vital importancia), archivos de logs, de configuraciones, intentos de ejecución de scripts, accesos de bots, etc.

También podemos bloquear todo el tráfico de un país en concreto, de varios países ((ip.geoip.country eq "RU") or (ip.geoip.country eq "CN")) o de todos los países, excepto los que marquemos en nuestra regla ((not ip.geoip.country in {"ES" "PT"})) y miles de posibilidades más.

Otra regla que podemos crear es comprobar que los accesos al archivo de comentarios de la web tiene el referente de nuestro dominio (http.request.uri.path contains "/wp-comments-post.php" and not http.referer contains "midominio.com")

También podemos bloquear el acceso a nuestra administración y solo permitir su acceso a una determinada IP (siempre nos debemos acordar de crear la excepción para /wp-admin/admin-ajax.php) o bloquearlo para todos y meter nuestra IP en la lista blanca al igual que hicimos con los rangos de IP de Redsys.

Otra reglas que podemos crear y combinar con las anteriores:

Para bloquear SPAM en formularios de contacto: (http.request.version in {"HTTP/1.0" "HTTP/1.1" "HTTP/1.2"} and http.request.uri eq "/contacto/" and not http.user_agent contains "Googlebot" and not http.user_agent contains "Bingbot" and not http.user_agent contains "DuckDuckBot" and not http.user_agent contains "facebot" and not http.user_agent contains "Slurp" and not http.user_agent contains "Alexa")

Bloquear SPAM de formularios de contacto y en comentarios: (http.request.uri contains "/wp-admin/admin-ajax.php" and http.request.method eq "POST" and not http.referer contains "tusitioweb.com") or (http.request.uri contains "/wp-comments-post.php" and http.request.method eq "POST" and not http.referer contains "tusitioweb.com").

Como hemos visto, podemos generar múltiples combinaciones, utilizar IPs, rangos de IPs, países, continentes, cookies, URLs, referers, métodos de solicitud, etc., convertir a mayúscula o minúscula, ver si contiene algo, si no lo contiene, si es igual o distinto. Además de poderlas «programar mediante cron» o realizar otras acciones mediante la API como en el siguiente ejemplo donde vacío la caché.

¿Están funcionando las reglas?

Es muy fácil comprobar si están funcionando las reglas que has creado, por ejemplo puedes probar la de XML-RPC visitando la URL de tu web que debe estar bloqueada en https://tuweb.com/xmlrpc.php

O en el caso de la regla que bloquea diferentes intentos de acceso a tu web https://tuweb.com/prueba/volcado.sql y en ambos casos deberás ver la página de Cloudflare de Acceso denegado, donde además nos indica un Ray ID que nos sirve para buscar el evento en concreto.

Cloudflare acceso denegado

A continuación en seguridad podemos ir a información general, eventos de firewall y ver todos los eventos:

Eventos de firewall

Ahí podemos editar las columnas y por ejemplo mostrar el Ray ID, o aún mejor, filtrar y mostrar solo los eventos que coincidan con nuestro ID que copiamos de la página anterior:

Filtrado por Ray ID

Aquí podemos ver que se ha intentado acceder a un archivo de volcado de base de datos (.sql), la fecha, IP, ruta de acceso, la regla que ha actuado, así como otros datos del intento de acceso (que en este caso ha sido una prueba mía).

Si tenemos muchos intentos de acceso de atacantes de una IP, al igual que habilitamos la IP para Redsys, podemos denegar una determinada IP o rango de IPs.

¿Y ahora?

Como ves y ya te advertí al principio, sobre las diferentes opciones de Cloudflare podemos escribir un libro completo. No hemos entrado en las opciones de CDN y aceleración de la web.

Otra opción muy interesante es Zaraz, desde la que por ejemplo podemos enviar el script de Google Analytics (entre otros muchos) a la web sin insertarlo desde el servidor, o las diferentes mejoras del CDN o el uso de Early Hints para sacarle unos segundo más a la optimización, o incluso ocultar partes de nuestra web como algunos textos a visitantes de reputación dudosa.

Zaraz
Script inyectado de Zaraz

Pero eso ya es harina de otro costal. Si quieres tener todas las reglas del WAF que he nombrado aquí y la mejoras que vaya realizando sobre las mismas, puedes seguir este repositorio donde las he puesto: https://github.com/CarlosLongarela/WordPress-Cloudflare-WAF-rules

Espero todos vuestros comentarios.

P.D.: Primer artículo: ¿Es mi sitio web seguro?.

Segundo artículo: Seguridad WordPress en tres niveles.

2 comentarios en «Seguridad WordPress desde Cloudflare»

  1. Hola: Estaba implementado las medidas de seguridad que pones en tu artículo en mi Cloudflare y cuando hago la primera Regla de transformación que propones me da el siguiente error y no tengo ni idea a qué es debido ya que he copiado lo que pones en el artículo:
    ‘(http.request.uri contains «» and not http.request.full_uri contains «.css» and not http.request.full_uri contains «.js» and not http.request.uri contains «.jpg» and not http.request.uri contains «.gif» and not http.request.uri contains «.png» and not http.request.uri contains «.webp»)’ is not a valid value for expression because the expression is invalid: Filter parsing error (1:28): (http.request.uri contains «» and not http.request.full_uri contains «.css» and not http.request.full_uri contains «.js» and not http.request.uri contains «.jpg» and not http.request.uri contains «.gif» and not http.request.uri contains «.png» and not http.request.uri contains «.webp») ^^^^ invalid digit found in string while parsing with radix 16 (Code: 20127)

    Las tres reglas de transformación que indicas me dan error.

    Responder
    • Hola Pachi, eso es debido a que WordPress cambia algunas comillas, cambia las comillas tipográficas « y » por las rectas normales.

      Responder

Deja un comentario