Conteúdo misto no WordPress depois da mudança para https
Elimina os avisos de conteúdo misto do WordPress depois da mudança para https: encontra os URL http:// que ficaram na base de dados, substitui-os em segurança e força https no .htaccess.
Publicado
O certificado está instalado, o site carrega por https e o cadeado continua a recusar-se a aparecer. Isso é conteúdo misto: a própria página veio por https, mas algo lá dentro — uma imagem, uma folha de estilo, um script — continua a ser pedido por http:// simples. Aqui vemos como encontrar esses URL, substituí-los sem corromper dados serializados, corrigir o siteurl e o home e forçar https no servidor para que o problema não possa voltar.
O que é de facto o conteúdo misto
Instalar um certificado muda a forma como a ligação é cifrada. Não muda o que as tuas páginas pedem. Cada http://yoursite.com/wp-content/uploads/logo.png que foi escrito num artigo, num widget ou numa definição do tema antes da mudança continua na base de dados, e o navegador pede-o obedientemente por uma ligação insegura.
Os navegadores tratam duas categorias de forma diferente, e a distinção conta quando estás a decidir a urgência disto:
- Conteúdo misto ativo — scripts, folhas de estilo, iframes, XHR. Bloqueado por completo. É por isto que um site pode parecer partido depois da mudança para https sem qualquer mensagem de erro em lado nenhum: uma folha de estilo foi recusada em silêncio.
- Conteúdo misto passivo — imagens, áudio, vídeo. Normalmente ainda carrega, mas o cadeado é desclassificado e alguns navegadores mostram um indicador de «não seguro».
Ambos merecem ser corrigidos. Só o primeiro parte coisas.
Passo 1: descobre o que ainda está inseguro
Começa no navegador. Abre a página, abre as ferramentas de programador e lê a consola. Cada pedido bloqueado ou desclassificado é identificado ali com o URL completo. Isso diz-te o que está inseguro; não te diz onde está guardado.
Para isso, olha diretamente para a base de dados. Exporta-a e procura no ficheiro:
wp db export dump.sql
grep -o "http://example\.com[^\"']*" dump.sql | sort -u | head -50
Se o WP-CLI não estiver disponível, o mysqldump produz o mesmo ficheiro:
mysqldump -u USER -p DBNAME > dump.sql
Lê os URL únicos que aparecem. Os caminhos de uploads apontam para o conteúdo e os metadados dos artigos. Os caminhos de recursos do tema costumam apontar para as opções. Qualquer coisa com um domínio que não reconheças é uma incorporação externa, que precisa de outra solução (mais abaixo).
Passo 2: corrige primeiro o siteurl e o home
O siteurl e o home são as duas opções que o WordPress usa para construir quase todos os URL internos que gera. Se algum deles ainda disser http://, o WordPress vai continuar a emitir URL inseguros por muito limpo que esteja o resto da base de dados.
Verifica-os:
wp option get siteurl
wp option get home
Ou em sql:
SELECT option_name, option_value
FROM wp_options
WHERE option_name IN ('siteurl', 'home');
Estes dois são cadeias simples, não matrizes serializadas, por isso aqui uma atualização direta em sql é genuinamente segura:
UPDATE wp_options
SET option_value = REPLACE(option_value, 'http://example.com', 'https://example.com')
WHERE option_name IN ('siteurl', 'home');
Uma armadilha antes de perderes tempo com isto. Se o wp-config.php definir WP_HOME ou WP_SITEURL, essas constantes sobrepõem-se por completo à base de dados e a tua atualização vai parecer não fazer nada:
define( 'WP_HOME', 'https://example.com' );
define( 'WP_SITEURL', 'https://example.com' );
Atualiza-as aí, ou remove-as e deixa a base de dados mandar. Verifica isto primeiro — explica muitos casos do género «mudei e não aconteceu nada».
Passo 3: a substituição tem de respeitar a serialização
Todo o resto da base de dados é onde está o risco verdadeiro, e não é o risco que a maioria das pessoas espera. O perigo não é a substituição falhar URL. É ter sucesso ao nível do texto e destruir os dados à volta.
O WordPress guarda matrizes e objetos como cadeias serializadas de PHP, e esse formato regista o comprimento em bytes de cada cadeia que contém:
a:1:{s:4:"logo";s:38:"http://example.com/img/logo.png";}
Muda http:// para https:// com um REPLACE de sql em bruto e o texto fica um byte mais comprido enquanto o comprimento declarado permanece no valor antigo. O PHP lê o comprimento, avança esses bytes, não encontra o terminador onde o espera e recusa-se a desserializar a matriz inteira. O WordPress entrega então ao tema ou ao plugin um valor que se comporta como se a definição nunca tivesse sido guardada.
O sintoma não é um erro. São widgets a desaparecer, definições do personalizador a reiniciar e disposições de construtores de páginas a aparecer em branco — sem nada na administração a indicar que algo correu mal. Recuperar disso sem uma cópia de segurança é genuinamente doloroso, por isso faz uma:
wp db export backup-before-https-replace.sql
Depois usa uma ferramenta que desserialize, substitua dentro da estrutura descodificada e volte a serializar com os comprimentos corrigidos. Simulação primeiro:
wp search-replace 'http://example.com' 'https://example.com' --dry-run --all-tables --skip-columns=guid
Lê o relatório. Se uma tabela que não reconheces mostrar milhares de ocorrências, para e olha antes de confirmares. Depois corre a sério:
wp search-replace 'http://example.com' 'https://example.com' --all-tables --skip-columns=guid --report-changed-only
O --skip-columns=guid é importante: o guid é um identificador permanente para os leitores de feeds, não um URL vivo, e reescrevê-lo pode fazer os teus subscritores verem todo o teu arquivo como artigos novos. Se precisares de ver que colunas são seguras para sql simples e quais exigem tratamento consciente da serialização antes de correres o que quer que seja, constrói as instruções com a ferramenta de procura e substituição sql para WordPress.
Se não puderes usar a linha de comandos, um plugin de migração que declare explicitamente que lida com dados serializados faz o mesmo trabalho de descodificação a partir do ecrã de administração. Se uma ferramenta não o disser, assume que não o faz.
Passo 4: força https no servidor
Limpar a base de dados impede as tuas páginas de pedirem recursos inseguros. Não impede um visitante de chegar por http:// logo à partida. Isso é um redirecionamento ao nível do servidor e, no Apache, pertence ao .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
Duas notas sobre a colocação. Põe isto acima dos marcadores # BEGIN WordPress — tudo o que fique lá dentro é descartado sempre que alguém guarda as ligações permanentes. E isto só funciona se o mod_rewrite estiver ativo e o anfitrião virtual permitir sobreposições (AllowOverride All, ou pelo menos FileInfo). No nginx, o .htaccess é ignorado por completo e isto tem de ir para um bloco de servidor.
Um RedirectMatch não serve para esta tarefa. Só faz correspondência sobre o caminho do pedido e não tem forma de testar se o pedido atual já é seguro, por isso redireciona pedidos https para eles próprios. A RewriteRule condicional acima é a ferramenta certa.
O ciclo de redirecionamento, e porque acontece
Se o site começa a redirecionar para sempre no momento em que adicionas essa regra, o teu TLS termina algures a montante — num balanceador de carga, num proxy inverso ou numa CDN — que depois encaminha http simples para o Apache. O Apache vê um pedido inseguro, redireciona para https, o proxy volta a encaminhar http, e lá vai outra vez.
Avalia antes o cabeçalho encaminhado:
RewriteCond %{HTTP:X-Forwarded-Proto} !https
RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]
O próprio WordPress tem o mesmo ponto cego nessa configuração — o is_ssl() lê o $_SERVER['HTTPS'], que o proxy nunca define, por isso os URL da administração saem como http://. Acrescenta isto ao wp-config.php, acima da linha «stop editing»:
if ( isset( $_SERVER['HTTP_X_FORWARDED_PROTO'] ) && 'https' === $_SERVER['HTTP_X_FORWARDED_PROTO'] ) {
$_SERVER['HTTPS'] = 'on';
}
Vale a pena ser direto quanto ao risco: o X-Forwarded-Proto é um cabeçalho fornecido pelo cliente. Confiar nele só é correto quando um proxy que controlas o reescreve sempre. Num servidor alcançável diretamente a partir da internet, pode ser falsificado.
O que uma substituição na base de dados não vai resolver
- URL escritos à mão em ficheiros. Tudo o que esteja escrito no
functions.php, num template de um tema filho ou num caminho de recurso em linha não está na base de dados. Faz um grep à parte na pasta do tema. - JSON com escape. Alguns construtores guardam os URL como
https:\/\/example.com. Uma procura pela forma normal falha-os por completo, por isso pode ser preciso um segundo passe sobre a cadeia com escape. - Recursos externos. Um script ou uma incorporação de terceiros disponível apenas por http não pode ser corrigido do teu lado. Encontra uma versão por https ou deita-o fora.
- Caches. A cache de página, a cache de objetos e as cópias na CDN continuam a servir a marcação antiga depois de uma substituição correta. Limpa as três antes de concluíres que a substituição falhou.
Uma coisa que vale a pena saltar: o cabeçalho de Content Security Policy upgrade-insecure-requests vai silenciar os avisos ao reescrever os pedidos inseguros no navegador. Trata o sintoma e deixa os URL errados na tua base de dados, onde a próxima exportação, migração ou feed os vai levar consigo. Corrige os dados e depois usa-o como rede de segurança se quiseres.
Ainda encravado?
Volta a verificar o siteurl e o home depois de cada passo — os plugins e as ferramentas de migração às vezes reescrevem-nos nas tuas costas. Se a consola continuar a identificar um URL inseguro que não consegues encontrar no ficheiro exportado, vê o código-fonte da página e procura-o aí: se aparecer na marcação gerada mas não na base de dados, está a ser construído em PHP, e o tema ou plugin que o produz é a coisa a corrigir.
FAQ
Perguntas
Porque é que o meu site WordPress continua a mostrar avisos de conteúdo misto depois de instalar um certificado SSL?
O certificado só muda a forma como a ligação é cifrada, não o que as tuas páginas pedem. Os URL http:// antigos continuam guardados na base de dados, dentro do conteúdo dos artigos, das opções dos widgets e das definições do tema. O navegador carrega a página por https, vê uma imagem ou um script a ser pedido por http simples e reporta conteúdo misto numa página que de resto é segura.
Como encontro os URL http:// que ainda estão na minha base de dados do WordPress?
Abre uma página no navegador e lê a consola de programador, que identifica cada pedido inseguro pelo seu URL. Depois exporta a base de dados e procura o teu domínio com o prefixo http no ficheiro exportado. Contar as ocorrências por tabela diz-te se o problema está no conteúdo dos artigos, na wp_options, ou em tabelas de plugins que não esperavas.
Posso resolver o conteúdo misto com uma simples procura e substituição em sql?
Só para valores escalares como siteurl e home. Tudo o que o WordPress guarda como matriz serializada, o que abrange a maioria dos valores de opções e metadados, regista o comprimento em bytes de cada cadeia que contém. Uma substituição em bruto muda o texto mas deixa o comprimento antigo, o valor deixa de poder ser desserializado e a definição volta em silêncio ao valor predefinido.
Que regra de .htaccess força https no WordPress?
Uma RewriteRule protegida por uma condição sobre a variável HTTPS, colocada acima dos marcadores BEGIN WordPress para que guardar as ligações permanentes não a apague. Por trás de um proxy ou de uma CDN, a variável HTTPS aparece como off mesmo em pedidos seguros, por isso avalia antes o cabeçalho X-Forwarded-Proto ou a regra vai redirecionar para sempre.
Porque é que o meu site entrou num ciclo de redirecionamento depois de forçar https?
O teu TLS quase de certeza termina num proxy ou numa CDN que depois encaminha http simples para o Apache. A reescrita vê um pedido inseguro, redireciona para https, o proxy volta a encaminhar http, e o ciclo repete-se. Muda a condição para o cabeçalho X-Forwarded-Proto e define a variável de servidor HTTPS no wp-config.php.
O conteúdo misto parte a página inteira ou apenas o cadeado?
Depende do recurso. Os navegadores bloqueiam por completo o conteúdo misto ativo, como scripts, folhas de estilo e iframes, o que pode partir a disposição ou a funcionalidade sem qualquer explicação visível. O conteúdo misto passivo, como imagens e vídeo, normalmente continua a carregar, mas o cadeado é desclassificado e os visitantes podem ver um indicador de não seguro. Ambos merecem ser corrigidos.