Vai al contenuto
Server e .htaccess

Contenuto misto in WordPress dopo il passaggio a https

Elimina gli avvisi di contenuto misto in WordPress dopo il passaggio a https: trova gli URL http:// rimasti nel database, sostituiscili in sicurezza e forza https in .htaccess.

Pubblicato

Il certificato è installato, il sito si carica via https e il lucchetto continua a non comparire. Questo è il contenuto misto: la pagina è arrivata via https, ma qualcosa al suo interno — un’immagine, un foglio di stile, uno script — viene ancora richiesto via http:// in chiaro. Qui vediamo come trovare quegli URL, sostituirli senza corrompere i dati serializzati, correggere siteurl e home e forzare https a livello di server perché il problema non possa tornare.

Cos’è davvero il contenuto misto

Installare un certificato cambia il modo in cui la connessione viene cifrata. Non cambia ciò che le tue pagine richiedono. Ogni http://yoursite.com/wp-content/uploads/logo.png scritto in un articolo, in un widget o in un’impostazione del tema prima della migrazione è ancora nel database, e il browser lo richiede diligentemente su una connessione non sicura.

I browser trattano due categorie in modo diverso, e la distinzione conta quando devi decidere quanto è urgente:

  • Contenuto misto attivo — script, fogli di stile, iframe, XHR. Bloccato del tutto. Ecco perché un sito può sembrare rotto dopo il passaggio a https senza alcun messaggio di errore da nessuna parte: un foglio di stile è stato rifiutato in silenzio.
  • Contenuto misto passivo — immagini, audio, video. Di solito viene comunque caricato, ma il lucchetto viene declassato e alcuni browser mostrano un indicatore «non sicuro».

Entrambi vanno risolti. Solo il primo rompe le cose.

Passo 1: scopri cosa è ancora non sicuro

Parti dal browser. Apri la pagina, apri gli strumenti per sviluppatori e leggi la console. Ogni richiesta bloccata o declassata è elencata lì con il suo URL completo. Questo ti dice cosa è non sicuro; non ti dice dove è salvato.

Per quello, guarda direttamente il database. Esportalo e cerca nel dump:

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

Se WP-CLI non è disponibile, mysqldump produce lo stesso file:

mysqldump -u USER -p DBNAME > dump.sql

Leggi gli URL unici che tornano. I percorsi degli uploads puntano al contenuto e ai metadati degli articoli. I percorsi delle risorse del tema di solito puntano alle opzioni. Qualsiasi cosa con un dominio che non riconosci è un embed esterno, che richiede una soluzione diversa (più avanti).

Passo 2: correggi prima siteurl e home

siteurl e home sono le due opzioni che WordPress usa per costruire quasi tutti gli URL interni che genera. Se una delle due dice ancora http://, WordPress continuerà a produrre URL non sicuri per quanto pulito sia il resto del database.

Controllale:

wp option get siteurl
wp option get home

Oppure in sql:

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

Queste due sono stringhe semplici, non array serializzati, quindi qui un update sql diretto è davvero sicuro:

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

Una trappola prima di dedicarci tempo. Se wp-config.php definisce WP_HOME o WP_SITEURL, quelle costanti scavalcano completamente il database e il tuo update sembrerà non fare nulla:

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

Aggiornale lì, oppure rimuovile e lascia decidere al database. Controlla questo per primo: spiega parecchi casi del tipo «l’ho cambiato e non è successo niente».

Passo 3: la sostituzione deve rispettare la serializzazione

Tutto il resto del database è dove sta il rischio vero, e non è il rischio che la maggior parte delle persone si aspetta. Il pericolo non è che la sostituzione si lasci sfuggire degli URL. È che riesca a livello di testo e distrugga i dati intorno.

WordPress salva array e oggetti come stringhe serializzate PHP, e quel formato registra la lunghezza in byte di ogni stringa che contiene:

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

Cambia http:// in https:// con un REPLACE sql grezzo e il testo diventa più lungo di un byte mentre la lunghezza dichiarata resta al valore vecchio. PHP legge la lunghezza, avanza di quei byte, non trova il terminatore dove se lo aspetta e rifiuta di deserializzare l’intero array. WordPress a quel punto consegna al tema o al plugin un valore che si comporta come se l’impostazione non fosse mai stata salvata.

Il sintomo non è un errore. Sono widget che spariscono, impostazioni del personalizzatore che si azzerano e layout dei page builder che si mostrano vuoti, senza nulla nell’amministrazione che segnali il problema. Recuperare da una situazione simile senza backup è davvero doloroso, quindi fallo:

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

Poi usa uno strumento che deserializzi, sostituisca dentro la struttura decodificata e riserializzi con le lunghezze corrette. Prima una simulazione:

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

Leggi il report. Se una tabella che non riconosci mostra migliaia di occorrenze, fermati e guarda prima di confermare. Poi esegui sul serio:

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

--skip-columns=guid è importante: il guid è un identificatore permanente per i lettori di feed, non un URL vivo, e riscriverlo può far vedere ai tuoi iscritti l’intero archivio come articoli nuovi. Se vuoi capire quali colonne sono sicure per sql semplice e quali richiedono una gestione consapevole della serializzazione prima di eseguire qualsiasi cosa, costruisci le istruzioni con lo strumento di cerca e sostituisci sql per WordPress.

Se non puoi usare la riga di comando, un plugin di migrazione che dichiari esplicitamente di gestire i dati serializzati fa lo stesso lavoro di decodifica dalla schermata di amministrazione. Se uno strumento non lo dichiara, dai per scontato che non lo faccia.

Passo 4: forza https a livello di server

Pulire il database impedisce alle tue pagine di richiedere risorse non sicure. Non impedisce a un visitatore di arrivare su http:// in primo luogo. Quello è un redirect a livello di server e su Apache va in .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

Due cose sulla posizione. Metti questo sopra i marcatori # BEGIN WordPress: tutto ciò che sta al loro interno viene scartato ogni volta che qualcuno salva i permalink. E funziona solo se mod_rewrite è abilitato e il virtual host consente gli override (AllowOverride All, o almeno FileInfo). Su nginx .htaccess viene ignorato del tutto e questo va invece in un blocco server.

Un RedirectMatch non è adatto a questo compito. Fa corrispondenza solo sul percorso della richiesta e non ha modo di verificare se la richiesta corrente è già sicura, quindi redirige le richieste https su se stesse. La RewriteRule condizionale qui sopra è lo strumento giusto.

Il loop di redirect, e perché si verifica

Se il sito inizia a redirigere all’infinito nel momento in cui aggiungi quella regola, il tuo TLS termina da qualche parte a monte — un load balancer, un reverse proxy o una CDN — che poi inoltra http in chiaro ad Apache. Apache vede una richiesta non sicura, redirige a https, il proxy inoltra di nuovo http, e si ricomincia.

Valuta invece l’header inoltrato:

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

WordPress stesso ha lo stesso punto cieco in quella configurazione: is_ssl() legge $_SERVER['HTTPS'], che il proxy non imposta mai, quindi gli URL dell’amministrazione escono come http://. Aggiungi questo a wp-config.php, sopra la riga «stop editing»:

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

Vale la pena essere chiari sul rischio: X-Forwarded-Proto è un header fornito dal client. Fidarsene è corretto solo quando un proxy che controlli lo sovrascrive sempre. Su un server raggiungibile direttamente da internet può essere falsificato.

Cosa una sostituzione nel database non risolverà

  • URL scritti a mano nei file. Tutto ciò che è scritto in functions.php, in un template di un tema child o in un percorso di risorsa inline non sta nel database. Fai un grep separato sulla directory del tema.
  • JSON con escape. Alcuni builder salvano gli URL come https:\/\/example.com. Una ricerca sulla forma normale li manca del tutto, quindi può servire un secondo passaggio sulla stringa con escape.
  • Risorse esterne. Uno script o un embed di terze parti disponibile solo via http non è risolvibile dalla tua parte. Trova una versione https o eliminalo.
  • Cache. La cache di pagina, la cache degli oggetti e le copie sulla CDN continuano a servire il markup vecchio dopo una sostituzione corretta. Svuota tutte e tre prima di concludere che la sostituzione è fallita.

Una cosa che conviene evitare: l’header di Content Security Policy upgrade-insecure-requests zittirà gli avvisi riscrivendo le richieste non sicure nel browser. Cura il sintomo e lascia gli URL sbagliati nel database, dove la prossima esportazione, migrazione o feed se li porterà dietro. Sistema i dati, poi usalo come rete di sicurezza se vuoi.

Ancora bloccato?

Ricontrolla siteurl e home dopo ogni passo: plugin e strumenti di migrazione a volte li riscrivono alle tue spalle. Se la console segnala ancora un URL non sicuro che non riesci a trovare nel dump, guarda il codice sorgente della pagina e cercalo lì: se compare nel markup generato ma non nel database, viene costruito in PHP, ed è il tema o il plugin che lo produce la cosa da sistemare.

FAQ

Domande

Perché il mio sito WordPress mostra ancora avvisi di contenuto misto dopo aver installato un certificato SSL?

Il certificato cambia solo il modo in cui la connessione viene cifrata, non ciò che le tue pagine richiedono. I vecchi URL http:// restano salvati nel database, dentro il contenuto degli articoli, le opzioni dei widget e le impostazioni del tema. Il browser carica la pagina via https, vede un'immagine o uno script richiesti via http in chiaro e segnala contenuto misto su una pagina per il resto sicura.

Come trovo gli URL http:// ancora presenti nel database di WordPress?

Apri una pagina nel browser e leggi la console per sviluppatori, che elenca ogni richiesta non sicura con il suo URL. Poi esporta il database e cerca il tuo dominio con il prefisso http nel dump. Contare le occorrenze per tabella ti dice se il problema sta nel contenuto degli articoli, in wp_options o in tabelle di plugin che non ti aspettavi.

Posso risolvere il contenuto misto con un semplice cerca e sostituisci in sql?

Solo per valori scalari come siteurl e home. Tutto ciò che WordPress salva come array serializzato, cioè la maggior parte dei valori di opzioni e metadati, registra la lunghezza in byte di ogni stringa che contiene. Una sostituzione grezza cambia il testo ma lascia la vecchia lunghezza, il valore non riesce più a essere deserializzato e l'impostazione torna silenziosamente al suo valore predefinito.

Quale regola .htaccess forza https in WordPress?

Una RewriteRule protetta da una condizione sulla variabile HTTPS, collocata sopra i marcatori BEGIN WordPress così che salvare i permalink non la cancelli. Dietro un proxy o una CDN la variabile HTTPS risulta off anche su richieste sicure, quindi valuta invece l'header X-Forwarded-Proto o la regola redirigerà all'infinito.

Perché il sito è finito in un loop di redirect dopo aver forzato https?

Quasi certamente il tuo TLS termina su un proxy o una CDN che poi inoltra http in chiaro ad Apache. Il rewrite vede una richiesta non sicura, redirige a https, il proxy inoltra di nuovo http e il loop si ripete. Sposta la condizione sull'header X-Forwarded-Proto e imposta la variabile di server HTTPS in wp-config.php.

Il contenuto misto rompe l'intera pagina o solo il lucchetto?

Dipende dalla risorsa. I browser bloccano del tutto il contenuto misto attivo come script, fogli di stile e iframe, il che può rompere il layout o le funzionalità senza alcuna spiegazione visibile. Il contenuto misto passivo come immagini e video di solito viene comunque caricato, ma il lucchetto viene declassato e i visitatori possono vedere un indicatore non sicuro. Entrambi vanno risolti.