Saltar al contenido
Servidor y .htaccess

Contenido mixto en WordPress tras migrar a https

Elimina los avisos de contenido mixto de WordPress tras pasar a https: localiza las URL http:// que quedan en la base de datos, sustitúyelas sin romper nada y fuerza https en .htaccess.

Publicada

Tienes el certificado instalado, el sitio carga por https y el candado sigue negándose a aparecer. Eso es contenido mixto: la página en sí llegó por https, pero algo dentro de ella —una imagen, una hoja de estilo, un script— se sigue solicitando por http:// plano. Aquí se explica cómo localizar esas URL, sustituirlas sin corromper los datos serializados, corregir siteurl y home y forzar https en el servidor para que el problema no pueda volver.

Qué es en realidad el contenido mixto

Instalar un certificado cambia cómo se cifra la conexión. No cambia lo que tus páginas piden. Cada http://yoursite.com/wp-content/uploads/logo.png que se escribió en una entrada, un widget o un ajuste del tema antes de la migración sigue en la base de datos, y el navegador la solicita obedientemente por una conexión insegura.

Los navegadores tratan dos categorías de forma distinta, y la diferencia importa cuando decides cuán urgente es esto:

  • Contenido mixto activo — scripts, hojas de estilo, iframes, XHR. Se bloquea de plano. Por eso un sitio puede verse roto tras migrar a https sin ningún mensaje de error por ninguna parte: se rechazó una hoja de estilo en silencio.
  • Contenido mixto pasivo — imágenes, audio, vídeo. Normalmente sigue cargándose, pero el candado se degrada y algunos navegadores muestran un indicador de «no seguro».

Ambos merecen arreglarse. Solo el primero rompe cosas.

Paso 1: averigua qué sigue siendo inseguro

Empieza en el navegador. Abre la página, abre las herramientas de desarrollo y lee la consola. Cada petición bloqueada o degradada aparece ahí con su URL completa. Eso te dice qué es inseguro; no te dice dónde está guardado.

Para eso, mira la base de datos directamente. Expórtala y busca en el volcado:

wp db export dump.sql
grep -o "http://example\.com[^\"']*" dump.sql | sort -u | head -50

Si no tienes WP-CLI disponible, mysqldump produce el mismo archivo:

mysqldump -u USER -p DBNAME > dump.sql

Lee las URL únicas que devuelve. Las rutas de uploads apuntan al contenido y a los metadatos de las entradas. Las rutas de recursos del tema suelen apuntar a las opciones. Cualquier cosa con un dominio que no reconozcas es una inserción externa, que necesita otra solución (más abajo).

Paso 2: arregla primero siteurl y home

siteurl y home son las dos opciones que WordPress usa para construir casi todas las URL internas que genera. Si alguna sigue diciendo http://, WordPress seguirá emitiendo URL inseguras por muy limpio que esté el resto de la base de datos.

Compruébalas:

wp option get siteurl
wp option get home

O en sql:

SELECT option_name, option_value
FROM wp_options
WHERE option_name IN ('siteurl', 'home');

Estas dos son cadenas planas, no arrays serializados, así que aquí una actualización directa en sql sí es segura de verdad:

UPDATE wp_options
SET option_value = REPLACE(option_value, 'http://example.com', 'https://example.com')
WHERE option_name IN ('siteurl', 'home');

Una trampa antes de que dediques tiempo a esto. Si wp-config.php define WP_HOME o WP_SITEURL, esas constantes anulan por completo la base de datos y tu actualización parecerá no hacer nada:

define( 'WP_HOME', 'https://example.com' );
define( 'WP_SITEURL', 'https://example.com' );

Actualízalas ahí, o quítalas y deja que mande la base de datos. Comprueba esto primero: explica un montón de casos del tipo «lo cambié y no pasó nada».

Paso 3: la sustitución tiene que respetar la serialización

Todo lo demás en la base de datos es donde está el riesgo real, y no es el riesgo que la mayoría espera. El peligro no es que la sustitución se deje URL sin tocar. Es que tenga éxito a nivel de texto y destruya los datos que la rodean.

WordPress guarda arrays y objetos como cadenas serializadas de PHP, y ese formato registra la longitud en bytes de cada cadena que contiene:

a:1:{s:4:"logo";s:38:"http://example.com/img/logo.png";}

Cambia http:// por https:// con un REPLACE de sql en crudo y el texto pasa a ser un byte más largo mientras la longitud declarada se queda en el valor antiguo. PHP lee la longitud, avanza esos bytes, no encuentra el terminador donde lo espera y se niega a deserializar el array entero. WordPress le entrega entonces al tema o al plugin un valor que se comporta como si el ajuste nunca se hubiera guardado.

El síntoma no es un error. Son widgets que desaparecen, ajustes del personalizador que se reinician y maquetaciones de constructores de páginas que salen en blanco, sin nada en el administrador que indique que algo ha ido mal. Recuperarse de eso sin una copia de seguridad es realmente doloroso, así que hazla:

wp db export backup-before-https-replace.sql

Después usa una herramienta que deserialice, sustituya dentro de la estructura decodificada y vuelva a serializar con las longitudes corregidas. Haz primero una simulación:

wp search-replace 'http://example.com' 'https://example.com' --dry-run --all-tables --skip-columns=guid

Lee el informe. Si una tabla que no reconoces muestra miles de coincidencias, párate y míralo antes de confirmar. Luego ejecútalo de verdad:

wp search-replace 'http://example.com' 'https://example.com' --all-tables --skip-columns=guid --report-changed-only

--skip-columns=guid importa: el guid es un identificador permanente para los lectores de feeds, no una URL viva, y reescribirlo puede hacer que tus suscriptores vean todo tu archivo como entradas nuevas. Si necesitas ver qué columnas son seguras para sql plano y cuáles requieren un tratamiento consciente de la serialización antes de ejecutar nada, construye las sentencias con la herramienta de búsqueda y sustitución sql para WordPress.

Si no puedes usar la línea de comandos, un plugin de migración que declare explícitamente que maneja datos serializados hace el mismo trabajo de decodificación desde el administrador. Si una herramienta no lo dice, da por hecho que no lo hace.

Paso 4: fuerza https en el servidor

Limpiar la base de datos evita que tus páginas soliciten recursos inseguros. No evita que un visitante llegue por http:// en primer lugar. Eso es una redirección a nivel de servidor y, en Apache, corresponde al .htaccess:

# BEGIN Force HTTPS
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteCond %{HTTPS} !=on
RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]
</IfModule>
# END Force HTTPS

Dos cosas sobre la colocación. Pon esto por encima de los marcadores # BEGIN WordPress: todo lo que quede dentro se descarta cada vez que alguien guarda los enlaces permanentes. Y esto solo funciona si mod_rewrite está habilitado y el host virtual permite sobrescrituras (AllowOverride All, o al menos FileInfo). En nginx, .htaccess se ignora por completo y esto tiene que ir en un bloque de servidor.

Un RedirectMatch no sirve para esta tarea. Coincide únicamente con la ruta de la petición y no tiene forma de comprobar si la petición actual ya es segura, así que redirige las peticiones https hacia sí mismas. La RewriteRule condicional de arriba es la herramienta correcta.

El bucle de redirección, y por qué ocurre

Si el sitio empieza a redirigir eternamente en cuanto añades esa regla, tu TLS termina en algún punto anterior —un balanceador de carga, un proxy inverso o un CDN— que luego reenvía http plano a Apache. Apache ve una petición insegura, redirige a https, el proxy vuelve a reenviar http, y vuelta a empezar.

Evalúa en su lugar la cabecera reenviada:

RewriteCond %{HTTP:X-Forwarded-Proto} !https
RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]

El propio WordPress tiene el mismo punto ciego en esa configuración: is_ssl() lee $_SERVER['HTTPS'], que el proxy nunca define, así que las URL del administrador salen como http://. Añade esto a wp-config.php, por encima de la línea de «stop editing»:

if ( isset( $_SERVER['HTTP_X_FORWARDED_PROTO'] ) && 'https' === $_SERVER['HTTP_X_FORWARDED_PROTO'] ) {
	$_SERVER['HTTPS'] = 'on';
}

Conviene ser claro sobre el riesgo: X-Forwarded-Proto es una cabecera suministrada por el cliente. Confiar en ella solo es correcto cuando un proxy que controlas la sobrescribe siempre. En un servidor accesible directamente desde internet, puede falsificarse.

Lo que una sustitución en la base de datos no arreglará

  • URL escritas a mano en archivos. Todo lo que esté escrito en functions.php, en una plantilla de un tema hijo o en una ruta de recurso en línea no está en la base de datos. Haz un grep aparte sobre el directorio del tema.
  • JSON escapado. Algunos constructores guardan las URL como https:\/\/example.com. Una búsqueda de la forma normal se las salta por completo, así que puede hacer falta una segunda pasada sobre la cadena escapada.
  • Recursos externos. Un script o una inserción de terceros disponible solo por http no se puede arreglar desde tu lado. Busca una versión por https o elimínalo.
  • Cachés. La caché de página, la caché de objetos y las copias del CDN siguen sirviendo el marcado antiguo tras una sustitución correcta. Vacía las tres antes de concluir que la sustitución falló.

Una cosa que conviene saltarse: la cabecera de Content Security Policy upgrade-insecure-requests silenciará los avisos reescribiendo las peticiones inseguras en el navegador. Trata el síntoma y deja las URL equivocadas en tu base de datos, donde la próxima exportación, migración o feed las arrastrará. Arregla los datos y luego úsala como red de seguridad si quieres.

¿Sigues atascado?

Vuelve a comprobar siteurl y home después de cada paso: los plugins y las herramientas de migración a veces los reescriben a tus espaldas. Si la consola sigue nombrando una URL insegura que no encuentras en el volcado, mira el código fuente de la página y búscala ahí: si aparece en el marcado generado pero no en la base de datos, se está construyendo en PHP, y lo que hay que arreglar es el tema o el plugin que la produce.

FAQ

Preguntas

¿Por qué mi sitio WordPress sigue mostrando avisos de contenido mixto después de instalar un certificado SSL?

El certificado solo cambia cómo se cifra la conexión, no lo que piden tus páginas. Las URL http:// antiguas siguen guardadas en la base de datos, dentro del contenido de las entradas, las opciones de los widgets y los ajustes del tema. El navegador carga la página por https, ve que se solicita una imagen o un script por http plano e informa de contenido mixto en una página por lo demás segura.

¿Cómo encuentro las URL http:// que quedan en mi base de datos de WordPress?

Abre una página en el navegador y lee la consola de desarrollo, que nombra cada petición insegura con su URL. Después exporta la base de datos y busca tu dominio con el prefijo http en el volcado. Contar las coincidencias por tabla te dice si el problema vive en el contenido de las entradas, en wp_options o en tablas de plugins que no esperabas.

¿Puedo arreglar el contenido mixto con una simple búsqueda y sustitución en sql?

Solo para valores escalares como siteurl y home. Todo lo que WordPress guarda como array serializado, que es la mayoría de valores de opciones y metadatos, registra la longitud en bytes de cada cadena que contiene. Una sustitución en crudo cambia el texto pero deja la longitud antigua, el valor deja de poder deserializarse y el ajuste vuelve en silencio a su valor por defecto.

¿Qué regla de .htaccess fuerza https en WordPress?

Una RewriteRule protegida por una condición sobre la variable HTTPS, colocada por encima de los marcadores BEGIN WordPress para que guardar los enlaces permanentes no la borre. Detrás de un proxy o un CDN, la variable HTTPS se lee como off incluso en peticiones seguras, así que evalúa en su lugar la cabecera X-Forwarded-Proto o la regla redirigirá eternamente.

¿Por qué mi sitio entró en un bucle de redirección tras forzar https?

Casi con seguridad tu TLS termina en un proxy o un CDN que luego reenvía http plano a Apache. La reescritura ve una petición insegura, redirige a https, el proxy vuelve a reenviar http y el bucle se repite. Cambia la condición a la cabecera X-Forwarded-Proto y define la variable de servidor HTTPS en wp-config.php.

¿El contenido mixto rompe toda la página o solo el candado?

Depende del recurso. Los navegadores bloquean de plano el contenido mixto activo, como scripts, hojas de estilo e iframes, lo que puede romper la maquetación o la funcionalidad sin ninguna explicación visible. El contenido mixto pasivo, como imágenes y vídeo, suele seguir cargándose, pero el candado se degrada y los visitantes pueden ver un indicador de no seguro. Ambos merecen arreglarse.