Pular para o conteúdo
Servidor e .htaccess

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.