Ocultar la versión de WordPress y endurecer las cabeceras HTTP

Ocultar la versión de WordPress y endurecer las cabeceras HTTP

Última verificación: 15 de octubre de 2026
21 min de lectura
Guía
Auditor de seguridad

En ciberseguridad existe un concepto conocido como reconocimiento.

Antes de que un atacante (o un bot automatizado) ataque tu sitio, lo escanea. Busca la fruta al alcance de la mano. Si tu web anuncia “WordPress 5.8.1” en el código fuente, el escáner sabe exactamente qué vulnerabilidades publicadas afectaban a la 5.8.1 y qué parches puedes haber pasado por alto.

Esta guía va más allá de los plugins de “ocultar versión”. Explica la seguridad por oscuridad (y por qué es solo la primera capa de defensa), las cabeceras que el propio WordPress envía y, después, un endurecimiento real a nivel de servidor con cabeceras HTTP, protección del login y permisos de archivos.


#Basta la seguridad por oscuridad en WordPress

#Qué es la seguridad por oscuridad

La seguridad por oscuridad consiste en ocultar información sobre tu sistema para que a los atacantes les cueste más encontrar vulnerabilidades. Es un enfoque discutido en el mundo de la seguridad, pero combinado con medidas adecuadas añade una capa extra de defensa.

El debate:

  • Los críticos dicen: “Un sistema debería ser seguro aunque los atacantes lo sepan todo sobre él”
  • Los profesionales dicen: “Por qué facilitarles el trabajo anunciando tus vulnerabilidades”

En la práctica: la seguridad por oscuridad no basta por sí sola, pero es una primera capa útil. No detendrá a un atacante decidido, pero saca tu sitio de los escaneos masivos más simples, los que comparan una cadena de versión con una lista de CVE.

#Por qué importa ocultar la versión de WordPress

Las vulnerabilidades del core de WordPress están documentadas públicamente. Cuando sale una versión de seguridad, las notas de la versión indican qué versiones están afectadas. Al mostrar tu número de versión, les dices a los atacantes:

  1. Qué vulnerabilidades conocidas podrían existir en tu sitio
  2. Si has aplicado los últimos parches de seguridad
  3. Qué código de exploit concreto podría funcionar contra ti

Puntos habituales de fuga de versión:

  • Etiqueta meta generator en el HTML
  • Etiquetas generator en los feeds RSS/Atom
  • Query strings de scripts y estilos (?ver=6.7.1)
  • Archivo readme.html en el directorio raíz

#Cómo ocultar la versión de WordPress

#La etiqueta meta generator

Por defecto, WordPress inyecta una etiqueta meta en el <head> de tu HTML:

<meta name="generator" content="WordPress 6.7.1" />

No aporta nada al SEO y es peligrosa para la seguridad. El core la imprime con la función wp_generator enganchada a wp_head, y la misma función the_generator se ejecuta en la cabecera de cada feed (rss2_head, commentsrss2_head, rss_head, rdf_header, atom_head, comments_atom_head, opml_head, app_head), según la lista de wp-includes/default-filters.php.

#Código PHP para quitar la versión de WordPress

No instales un plugin pesado solo para esto. Pon el código en un plugin must-use (wp-content/mu-plugins/hardening.php), para que sobreviva a un cambio de tema:

<?php
/**
 * Plugin Name: WPPoland hardening
 * Description: Elimina la versión del core del head, los feeds y los assets del core.
 */

// Etiqueta generator en <head>
remove_action('wp_head', 'wp_generator');

// Etiqueta generator en todos los tipos de feed: un solo filtro los cubre todos
add_filter('the_generator', '__return_empty_string');

// Enlace Really Simple Discovery (solo lo necesitan los clientes XML-RPC)
remove_action('wp_head', 'rsd_link');

/**
 * Elimina ?ver= solo cuando coincide con la versión del core.
 * Plugins y temas conservan sus propias versiones para el cache busting.
 */
function wppoland_strip_core_version(string $src): string {
    global $wp_version;

    $query = parse_url($src, PHP_URL_QUERY);
    if (!$query) {
        return $src;
    }

    parse_str($query, $args);
    if (isset($args['ver']) && $args['ver'] === $wp_version) {
        return remove_query_arg('ver', $src);
    }

    return $src;
}
add_filter('style_loader_src', 'wppoland_strip_core_version', 9999);
add_filter('script_loader_src', 'wppoland_strip_core_version', 9999);

Muchas copias de este fragmento también eliminan wlwmanifest_link. Esa función está obsoleta desde WordPress 6.3.0, porque el manifiesto de Windows Live Writer ya no forma parte del core, así que la línea no hace nada en una instalación actual.

Por qué importa el filtro selectivo: Muchos fragmentos eliminan el parámetro ver de todos los assets. Eso rompe el cache busting: tras actualizar un plugin, la URL no cambia y los navegadores y las CDN siguen sirviendo el archivo antiguo. Comparar el valor con $wp_version oculta la versión del core y deja intactas las versiones de plugins y temas.

Nota importante: esto es seguridad por oscuridad. Es la primera capa, no la única. Un atacante decidido todavía puede identificar WordPress por otros medios (endpoints de la REST API, estructura de archivos y las cabeceras que se describen en la siguiente sección).

#Bloquear readme.html y license.txt

readme.html y license.txt están en la raíz web de cada instalación. Bloquéalos en el servidor web, para que la petición nunca llegue a PHP.

Apache 2.4 (.htaccess):

<FilesMatch "^(readme\.html|license\.txt)$">
  Require all denied
</FilesMatch>

Nginx:

location ~* ^/(readme\.html|license\.txt)$ {
    deny all;
}

La sintaxis antigua Order deny,allow / Deny from all pertenece a Apache 2.2. En Apache 2.4 solo funciona a través de mod_access_compat, así que usa Require all denied.


#Qué cabeceras HTTP delatan WordPress además de la etiqueta generator

La etiqueta generator no es la única huella. El core y PHP añaden cabeceras de respuesta que le dicen a un escáner qué software mueve el sitio, aunque no se imprima ningún número de versión.

CabeceraLa envíaCuándo
Link: <https://example.com/wp-json/>; rel="https://api.w.org/"rest_output_link_header(), desde WordPress 4.4.0en cada petición al front end, en template_redirect (prioridad 11)
Link: <https://example.com/?p=123>; rel=shortlinkwp_shortlink_header()en entradas y páginas individuales, en template_redirect (prioridad 11)
X-Pingback: https://example.com/xmlrpc.phpWP::send_headers()en entradas individuales que aceptan pings (pings_open()), de forma condicional desde la 4.4.0
X-Powered-By: PHP/8.3.xel propio PHPmientras expose_php esté activado, que es el valor por defecto

Fuentes: la referencia de rest_output_link_header, la referencia de WP::send_headers y la documentación de las directivas del core de PHP.

Las cabeceras de WordPress se quitan con los mismos hooks que las añaden. La cabecera X-Pingback pasa por el filtro wp_headers, que según el core filtra las cabeceras HTTP antes de enviarlas al navegador:

// Descubrimiento de la REST API: cabecera y etiqueta <link>
remove_action('template_redirect', 'rest_output_link_header', 11);
remove_action('wp_head', 'rest_output_link_wp_head', 10);

// Shortlink: cabecera y etiqueta <link>
remove_action('template_redirect', 'wp_shortlink_header', 11);
remove_action('wp_head', 'wp_shortlink_wp_head', 10);

// X-Pingback en entradas individuales
add_filter('wp_headers', function (array $headers): array {
    unset($headers['X-Pingback']);
    return $headers;
});

Quitar el enlace de descubrimiento no desactiva la REST API: /wp-json/ sigue respondiendo y el editor de bloques depende de ella. El objetivo es dejar de anunciar el CMS en cada respuesta HTML, no romper la API.

X-Powered-By no se puede cambiar desde WordPress. El manual de PHP indica que expose_php solo se configura en php.ini, así que ini_set() no tiene efecto. Ajústalo en el pool de PHP-FPM o en el php.ini principal:

expose_php = Off

La OWASP HTTP Headers Cheat Sheet recomienda eliminar todas las cabeceras X-Powered-By y quitar Server o darle un valor que no informe de nada. También recuerda que un atacante puede identificar la pila por otros medios, y ese es el límite honesto de toda esta sección.


#Cabeceras de seguridad HTTP para WordPress

Ocultar la versión es como quitar el número de la puerta de tu casa. Ayuda, pero no echa la llave. Las cabeceras de seguridad HTTP son los cerrojos.

Los navegadores aplican estas políticas solo cuando el servidor las envía. Sin ellas dependes de los valores por defecto del navegador frente al cross-site scripting (XSS), el clickjacking y las fugas de datos.

Configúralas en el servidor web, no en PHP. Una caché de página completa (Nginx FastCGI cache, Varnish, una CDN) sirve respuestas almacenadas sin ejecutar PHP, así que una llamada a header() en un tema no llega a los visitantes anónimos.

#1. X-Frame-Options (protección contra clickjacking)

Esta cabecera le dice al navegador: “No permitas que otros sitios metan mi página dentro de un <iframe>.” Así se evita que un atacante cargue tu página de login o de pago en un marco invisible y capture los clics.

Implementación (.htaccess):

<IfModule mod_headers.c>
  Header always set X-Frame-Options "SAMEORIGIN"
</IfModule>

Nginx:

add_header X-Frame-Options "SAMEORIGIN" always;

Opciones:

  • DENY: la página no puede mostrarse en un marco
  • SAMEORIGIN: la página solo puede mostrarse en un marco del mismo origen
  • ALLOW-FROM uri: obsoleta. MDN indica que los navegadores modernos que encuentran esta directiva ignoran la cabecera entera; usa en su lugar frame-ancestors de CSP

MDN también advierte que X-Frame-Options definida mediante <meta http-equiv> no tiene ningún efecto. Solo funciona como cabecera HTTP.

#2. X-Content-Type-Options

Evita el MIME sniffing. Si un atacante sube un archivo .jpg que en realidad contiene JavaScript, esta cabecera le dice al navegador: “El servidor dijo que es una imagen, así que trátala como imagen y no la ejecutes.”

Implementación:

<IfModule mod_headers.c>
  Header always set X-Content-Type-Options "nosniff"
</IfModule>

Nginx:

add_header X-Content-Type-Options "nosniff" always;

#3. Strict-Transport-Security (HSTS)

Es crítica para TLS. Obliga al navegador a usar siempre HTTPS, aunque el usuario escriba http://. Evita ataques de degradación y man-in-the-middle en redes Wi-Fi públicas, a partir de la primera visita segura.

Implementación (solo con un certificado válido en todos los subdominios):

<IfModule mod_headers.c>
  Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
</IfModule>

Nginx:

add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;

Qué significan los parámetros:

  • max-age=31536000: el navegador recuerda la política durante 1 año
  • includeSubDomains: se aplica a todos los subdominios (por ejemplo, blog.tusitio.com)
  • preload: una solicitud para entrar en la lista de precarga de los navegadores, con las consecuencias que se describen más abajo

Advertencia: una vez que configures esta cabecera, los navegadores se negarán a conectar por HTTP durante el periodo indicado. Asegúrate de que HTTPS funciona en todos los hosts antes de activar HSTS.

#4. Content-Security-Policy (CSP)

Es la cabecera más potente. Le dice al navegador: “Ejecuta solo scripts de estas fuentes de confianza.”

Implementación básica:

<IfModule mod_headers.c>
  Header always set Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' https://www.google-analytics.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; frame-ancestors 'self';"
</IfModule>

Desglose:

  • default-src 'self': por defecto, cargar recursos solo de tu propio dominio
  • script-src 'self' 'unsafe-inline': permitir scripts de tu dominio y scripts inline (muchos temas y plugins aún los necesitan)
  • img-src 'self' data: https:: permitir imágenes de tu dominio, URI de datos y cualquier origen HTTPS
  • style-src 'self' 'unsafe-inline': permitir estilos de tu dominio y estilos inline
  • frame-ancestors 'self': el sustituto en CSP de X-Frame-Options

Advertencia: una CSP mal configurada puede romper tu sitio. Pruébala primero en modo report-only:

Header always set Content-Security-Policy-Report-Only "default-src 'self'; report-uri /csp-report.php"

#5. Referrer-Policy

Controla cuánta información se envía cuando un usuario hace clic en un enlace a otro sitio.

Implementación:

<IfModule mod_headers.c>
  Header always set Referrer-Policy "strict-origin-when-cross-origin"
</IfModule>

Nginx:

add_header Referrer-Policy "strict-origin-when-cross-origin" always;

Envía la URL completa en las peticiones al mismo origen, pero solo el origen (dominio) en las peticiones a otros orígenes.

Opciones:

  • no-referrer: no enviar nunca información de referencia
  • no-referrer-when-downgrade: no enviar el referrer al pasar de HTTPS a HTTP
  • origin: enviar solo el origen
  • strict-origin: enviar solo el origen, nunca al degradar a HTTP
  • same-origin: URL completa para el mismo origen, nada para otros orígenes
  • strict-origin-when-cross-origin: URL completa para el mismo origen, solo el origen para otros orígenes

#6. Permissions-Policy (antes Feature-Policy)

Desactiva funciones del navegador que no usas (cámara, micrófono, geolocalización).

Implementación:

<IfModule mod_headers.c>
  Header always set Permissions-Policy "camera=(), microphone=(), geolocation=()"
</IfModule>

Nginx:

add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;

Directivas habituales:

  • camera=(): desactiva el acceso a la cámara
  • microphone=(): desactiva el acceso al micrófono
  • geolocation=(): desactiva la geolocalización
  • payment=(): desactiva la Payment Request API
  • usb=(): desactiva el acceso WebUSB

#7. Hay que seguir enviando X-XSS-Protection en 2026

Las guías antiguas recomendaban X-XSS-Protection: 1; mode=block. Ese consejo está desfasado. La página de MDN sobre X-XSS-Protection marca la cabecera como obsoleta y no estándar, recomienda usar en su lugar una Content-Security-Policy y advierte que, en algunos casos, el filtro puede crear vulnerabilidades XSS en sitios que por lo demás serían seguros. MDN añade que mode=block puede quedar expuesto a ataques de canal lateral cuando la página se puede incrustar en un marco.

OWASP da la misma respuesta: no enviar la cabecera o fijarla explícitamente en 0. Si una configuración antigua o un plugin ya envía 1; mode=block, sobrescríbela:

Header always set X-XSS-Protection "0"
add_header X-XSS-Protection "0" always;

La protección que de verdad buscas viene de una CSP que no permita scripts 'unsafe-inline', un proyecto más largo en la mayoría de los sitios WordPress.


#WordPress envía X-Frame-Options por su cuenta

Sí, en dos pantallas. El core engancha send_frame_options_header() a login_init y, en wp-admin/includes/admin-filters.php, a admin_init. La función, disponible desde WordPress 3.1.3, envía dos cabeceras si todavía no se ha enviado ninguna:

X-Frame-Options: SAMEORIGIN
Content-Security-Policy: frame-ancestors 'self';

Fuente: la referencia de send_frame_options_header.

Por tanto, wp-login.php y el escritorio están protegidos frente al enmarcado incluso sin configurar el servidor. El front end público no: los temas, las páginas de pago y los formularios de contacto no reciben esa cabecera salvo que la añadas tú.

Esto tiene una consecuencia práctica en Apache. La documentación de mod_headers explica que set sustituye cualquier cabecera anterior con el mismo nombre, mientras que append añade el nuevo valor a la cabecera existente, separado por una coma. Con Header append, la página de login lleva el valor de WordPress más el tuyo. Con Header always set, lleva exactamente uno. La misma documentación explica que las cabeceras que genera PHP detrás de mod_proxy_fcgi van en la tabla always, y por eso los ejemplos de esta guía usan siempre Header always set.


#Por qué desaparecen las cabeceras de seguridad de Nginx en los bloques location

Una sorpresa habitual tras una configuración de Nginx que “funciona”: curl -I en la portada muestra todas las cabeceras de seguridad, pero /wp-login.php o un archivo estático no muestra ninguna. La documentación de ngx_http_headers_module explica el motivo: las directivas add_header se heredan del nivel de configuración anterior si, y solo si, el nivel actual no define ninguna directiva add_header.

Basta un add_header Cache-Control ... dentro de location ~* \.(css|js)$ para que ese location pierda todas las cabeceras de seguridad. Hay dos salidas:

  1. Guardar las cabeceras de seguridad en un snippet e incluirlo en el bloque server y en cada location que defina su propio add_header:
# /etc/nginx/snippets/security-headers.conf
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
location ~* \.(css|js|woff2)$ {
    include snippets/security-headers.conf;
    add_header Cache-Control "public, max-age=31536000, immutable";
}
  1. En Nginx 1.29.3 o posterior, add_header_inherit merge; añade los valores del nivel anterior a los del nivel actual en lugar de sustituirlos.

El parámetro always también importa. Sin él, Nginx solo añade la cabecera a las respuestas con estado 200, 201, 204, 206, 301, 302, 303, 304, 307 o 308. Un 403 en /xmlrpc.php o una página 404 salen sin cabeceras.


#Qué exige la lista de precarga HSTS

El token preload de la cabecera anterior no es un ajuste que funcione por sí solo. Es una solicitud para quedar grabado en los navegadores, y hstspreload.org enumera las condiciones para enviarla:

  • servir un certificado válido,
  • redirigir de HTTP a HTTPS en el mismo host, si escuchas en el puerto 80,
  • servir todos los subdominios por HTTPS, incluido www si existe un registro DNS para él,
  • enviar la cabecera HSTS en el dominio base por HTTPS con un max-age de al menos 31536000 segundos, includeSubDomains y preload,
  • si el sitio HTTPS hace otra redirección, esa redirección también debe llevar la cabecera HSTS.

La misma página advierte que la precarga se aplica a todos los subdominios, incluidos los internos que no son accesibles públicamente, que las nuevas entradas pueden tardar varios meses en llegar a la versión estable de Chrome y que la retirada de la lista suele ser lenta y penosa. Un entorno de staging en un subdominio sin certificado deja de cargar en cualquier navegador que tenga la lista.

El valor que recomienda OWASP es max-age=63072000; includeSubDomains; preload, dos años. El orden que evita bloqueos: empezar con un max-age corto y sin preload, revisar todos los subdominios, subir el valor y enviar la solicitud solo cuando todo el dominio vaya a ser exclusivamente HTTPS de forma definitiva. Si no estás listo para ese compromiso, deja preload fuera de la cabecera.


#Cómo desactivar XML-RPC en WordPress

xmlrpc.php es un archivo de API heredado. La REST API lo ha sustituido en la mayoría de las integraciones. Sigue siendo un objetivo frecuente de fuerza bruta porque el método system.multicall permite que una sola petición HTTP lleve muchos intentos de login, que los limitadores por petición no cuentan por separado.

#Bloquear xmlrpc.php en .htaccess, PHP o Nginx

Método 1: .htaccess (Apache 2.4)

<Files xmlrpc.php>
  Require all denied
</Files>

Método 2: Nginx

location = /xmlrpc.php {
    deny all;
    access_log off;
    log_not_found off;
}

Método 3: functions.php (parcial)

add_filter('xmlrpc_enabled', '__return_false');

No tomes el método 3 como un bloqueo completo. La referencia de xmlrpc_enabled aclara que, en contra de lo que sugiere su nombre, el filtro solo controla los métodos XML-RPC que requieren autenticación. Los pingbacks y otros endpoints que no piden login siguen abiertos. Solo la regla del servidor web detiene la petición antes de que se ejecute PHP.

Excepción: si usas Jetpack o la app móvil de WordPress con un sitio autoalojado, necesitas XML-RPC. En ese caso, usa un plugin como Disable XML-RPC Pingback para quitar solo los métodos de pingback.


#Cómo proteger el login de WordPress

Los endpoints /wp-admin/ y /wp-login.php están bajo ataque constante. Así se protegen.

#1. Limitar los intentos de login

Por defecto, WordPress permite intentos de login ilimitados.

Plugin: Limit Login Attempts Reloaded, o fail2ban en el servidor leyendo los logins fallidos del log de accesos. Una versión mínima como mu-plugin tiene que engancharse a authenticate; de lo contrario, el contador nunca bloquea nada:

add_filter('authenticate', function ($user) {
    $key = 'wppoland_login_fail_' . md5($_SERVER['REMOTE_ADDR'] ?? '');
    if ((int) get_transient($key) >= 5) {
        return new WP_Error('too_many_attempts', 'Demasiados intentos fallidos. Vuelve a intentarlo en 15 minutos.');
    }
    return $user;
}, 30);

add_action('wp_login_failed', function () {
    $key = 'wppoland_login_fail_' . md5($_SERVER['REMOTE_ADDR'] ?? '');
    set_transient($key, (int) get_transient($key) + 1, 15 * MINUTE_IN_SECONDS);
});

Detrás de un proxy inverso o una CDN, REMOTE_ADDR es la dirección del proxy, así que este código bloquearía a todo el mundo a la vez. Configura antes la IP real del cliente en el servidor web.

#2. Autenticación de dos factores (2FA)

Aunque un atacante consiga tu contraseña, no podrá entrar sin el segundo factor.

Plugins:

#3. Cambiar la URL de login

Por defecto, todo el mundo sabe que tu login está en /wp-login.php.

Plugin: WPS Hide Login

Cambia la URL por algo como /mi-acceso-secreto/. Los bots que buscan /wp-login.php reciben un 404. Vuelve a ser oscuridad: reduce el ruido en los logs, pero no sustituye al 2FA.

#4. Deshabilitar la enumeración de usuarios

Por defecto, puedes descubrir nombres de usuario visitando /?author=1 o /wp-json/wp/v2/users.

Solución:

// Redirigir los archivos de autor pedidos por ID
add_action('template_redirect', function (): void {
    if (is_author() && !is_admin()) {
        wp_safe_redirect(home_url(), 301);
        exit;
    }
});

// Ocultar los endpoints de usuarios a quien no tenga list_users
add_filter('rest_endpoints', function (array $endpoints): array {
    if (current_user_can('list_users')) {
        return $endpoints;
    }
    unset($endpoints['/wp/v2/users'], $endpoints['/wp/v2/users/(?P<id>[\d]+)']);
    return $endpoints;
});

La comprobación de list_users es importante: sin ella, los editores pierden el desplegable de autor del editor de bloques, que lee el mismo endpoint.


#Cómo proteger la base de datos de WordPress

#1. Cambiar el prefijo de las tablas

Por defecto, WordPress usa wp_ como prefijo de tablas. Un prefijo propio no evita la inyección SQL; solo rompe los scripts automatizados que dan por hecho los nombres de tabla con wp_.

Durante la instalación: establece un prefijo propio como xyz_ o prod_.

Después de la instalación: usa un plugin como Change Table Prefix o renombra a mano las tablas y las claves con prefijo de options y usermeta (haz antes una copia de seguridad).

#2. Deshabilitar la edición de archivos

WordPress permite a los administradores editar archivos de temas y plugins desde el escritorio. Si un atacante obtiene acceso de administrador, puede inyectar malware.

Desactívalo en wp-config.php:

define('DISALLOW_FILE_EDIT', true);

#3. Asegurar wp-config.php

Este archivo contiene las credenciales de tu base de datos. Protégelo.

Muévelo un directorio más arriba: WordPress también busca wp-config.php un nivel por encima de la raíz web.

O establece permisos estrictos:

chmod 400 wp-config.php

#Permisos de archivos en WordPress 755 y 644

Se pasan por alto a menudo. Si los permisos están mal, un atacante que comprometa un plugin puede reescribir todo el core.

#Qué permisos usar en archivos y carpetas

  • Directorios: 755 (lectura y ejecución para todos, escritura solo para el propietario)
  • Archivos: 644 (lectura para todos, escritura solo para el propietario)
  • wp-config.php: 400 o 440 (solo lectura para el propietario o el servidor)

#Cómo aplicar los permisos con chmod

find /ruta/a/wordpress -type d -exec chmod 755 {} \;
find /ruta/a/wordpress -type f -exec chmod 644 {} \;
chmod 400 wp-config.php

#Cómo verificar el endurecimiento con curl

Comprueba las respuestas reales, incluidas las que sirve una caché, después de cada cambio:

# Cabeceras de seguridad en la portada
curl -sI https://example.com/ | grep -iE "strict-transport|x-frame|x-content|referrer|permissions|content-security"

# Cabeceras en la página de login: se espera exactamente una línea X-Frame-Options
curl -sI https://example.com/wp-login.php | grep -i x-frame

# Huellas que deberían haber desaparecido
curl -sI https://example.com/ | grep -iE "x-powered-by|x-pingback|api.w.org"
curl -s https://example.com/ | grep -i generator

# Los archivos bloqueados deberían devolver 403
curl -sI https://example.com/xmlrpc.php | head -1
curl -sI https://example.com/readme.html | head -1

Repite la comprobación de cabeceras contra una URL que devuelva 404. En Nginx sin always, es ahí donde desaparecen las cabeceras.


#Monitoreo de seguridad en WordPress sin plugins

No recomendamos instalar plugins de seguridad para el monitoreo. Preferimos el monitoreo a nivel de servidor (análisis de logs, fail2ban, WAF del hosting), copias de seguridad regulares y endurecimiento (contraseñas fuertes, actualizaciones, intentos de login limitados). La seguridad se construye con configuración y medidas en el servidor.


#Checklist de seguridad para WordPress

#Acciones inmediatas (hoy)

  • Eliminar la versión de WordPress del HTML y los feeds
  • Eliminar la cadena de versión del core de CSS/JS
  • Configurar la cabecera X-Frame-Options
  • Configurar la cabecera X-Content-Type-Options
  • Configurar Strict-Transport-Security (HSTS) si usas HTTPS
  • Bloquear XML-RPC en el servidor web (si no lo necesitas)
  • Limitar los intentos de login
  • Cambiar el nombre de usuario “admin” por defecto

#Acciones a corto plazo (esta semana)

  • Implementar Content-Security-Policy (primero en report-only)
  • Configurar Referrer-Policy
  • Quitar X-XSS-Protection o fijarla en 0
  • Activar la autenticación de dos factores
  • Deshabilitar la enumeración de usuarios
  • Establecer los permisos de archivos correctos (755/644)

#Acciones a largo plazo (este mes)

  • Cambiar el prefijo de las tablas de la base de datos (si es posible)
  • Deshabilitar la edición de archivos en el escritorio
  • Asegurar wp-config.php (permisos 400)
  • Fijar expose_php = Off
  • Configurar monitoreo a nivel de servidor y copias de seguridad automáticas
  • Decidir sobre la precarga HSTS solo cuando todos los subdominios usen HTTPS

#Resumen de la seguridad por capas en WordPress

Conoce más sobre los servicios de seguridad WordPress en WPPoland. La seguridad no es un producto; es un proceso.

  1. Ocultar información (cadenas de versión, cabeceras que delatan el CMS): seguridad por oscuridad
  2. Endurecer la conexión (HSTS, cabeceras, CSP): protección a nivel de navegador
  3. Cerrar puertas (bloquear XML-RPC, limitar logins): reducir la superficie de ataque
  4. Bloquear archivos (permisos, desactivar la edición): contener brechas
  5. Monitorear (logs del servidor, alertas): detectar y responder

La seguridad va por capas. Ninguna medida aislada es perfecta. Empieza por lo básico (actualizaciones, contraseñas fuertes), añade oscuridad (ocultar la versión), después aplica el endurecimiento (cabeceras, permisos) y, por último, el monitoreo.

El objetivo no es ser imposible de comprometer. El objetivo es ser un blanco más difícil que el siguiente sitio, para que los ataques automatizados pasen de largo.

Siguiente paso

Transforma el artículo en una implementación real

Este bloque refuerza el enlazado interno y lleva al lector al siguiente paso más útil dentro de la arquitectura del sitio.

Explora otros servicios WordPress y base de conocimiento

Refuerza tu negocio con soporte técnico profesional en áreas clave del ecosistema WordPress.

Preguntas frecuentes

Respuestas prácticas para aplicar el tema en la ejecución real.

Es suficiente ocultar la versión de WordPress para la seguridad#
No. Ocultar la versión es seguridad por oscuridad, una primera capa de defensa que no basta por sí sola. Debes combinarla con un endurecimiento adecuado: cabeceras de seguridad, 2FA, actualizaciones regulares y contraseñas fuertes.
Qué cabeceras de seguridad HTTP importan más en WordPress#
X-Frame-Options (evita el clickjacking), Content-Security-Policy (frena XSS), Strict-Transport-Security/HSTS (fuerza HTTPS), X-Content-Type-Options (evita el MIME sniffing) y Referrer-Policy.
Conviene deshabilitar XML-RPC en WordPress#
Sí, salvo que uses Jetpack, apps móviles o herramientas de publicación externas. XML-RPC es un vector habitual de fuerza bruta. Bloquéalo en el servidor web; el filtro xmlrpc_enabled solo desactiva los métodos que requieren autenticación.
Las cabeceras de seguridad afectan al rendimiento del sitio#
No de forma medible. Son unos cientos de bytes de metadatos en la respuesta que el navegador usa para aplicar políticas de seguridad. Una CSP demasiado estricta puede bloquear recursos, pero eso es un problema funcional, no de velocidad.
Ocultar la versión de WordPress puede romper el sitio#
Quitar la etiqueta generator es seguro. Eliminar todos los parámetros ?ver= rompe el cache busting tras actualizar temas y plugins, así que elimina solo el valor que coincide con la versión del core.
Sigue teniendo sentido enviar X-XSS-Protection#
No. MDN marca la cabecera como obsoleta y OWASP aconseja no enviarla o fijarla en 0. La protección frente a XSS viene de una Content Security Policy.

¿Necesitas un FAQ adaptado a tu sector y mercado? Preparo una versión alineada con tus objetivos de negocio.

Hablemos

Artículos Relacionados

PHP2Shell: ¿está en riesgo mi sitio WordPress y qué hacer?

PHP 8.5.12, 8.4.27, 8.3.36 y 8.2.35, publicadas el 21 de octubre de 2026, corrigen dos vulnerabilidades críticas en el propio PHP (CVE-2026-109618, CVE-2026-109619). Cómo comprobar la versión de PHP en WordPress, qué hacer si el hosting aún no ha actualizado el servidor y qué contraseñas cambiar después de la actualización.