WordPress mixed content na de overstap naar https
Mixed content waarschuwingen in WordPress oplossen na een overstap naar https: de achtergebleven http:// URLs in de database vinden, veilig vervangen en https afdwingen in .htaccess.
Gepubliceerd
Je certificaat staat, de site laadt over https, en het slotje weigert nog steeds te verschijnen. Dat is mixed content: de pagina zelf kwam over https binnen, maar iets erin — een afbeelding, een stylesheet, een script — wordt nog steeds over gewoon http:// opgevraagd. Dit artikel laat zien hoe je die URLs vindt, ze vervangt zonder geserialiseerde data te verwoesten, siteurl en home corrigeert en https op serverniveau afdwingt zodat het probleem niet terug kan komen.
Wat mixed content eigenlijk is
Een certificaat installeren verandert hoe de verbinding versleuteld wordt. Het verandert niet wat je pagina’s opvragen. Elke http://yoursite.com/wp-content/uploads/logo.png die vóór de overstap in een bericht, een widget of een thema-instelling is geschreven, staat nog steeds in de database, en de browser vraagt hem braaf op over een onveilige verbinding.
Browsers behandelen twee categorieën verschillend, en dat verschil bepaalt hoe dringend dit is:
- Actieve mixed content — scripts, stylesheets, iframes, XHR. Wordt volledig geblokkeerd. Daarom kan een site er na een overstap naar https kapot uitzien zonder dat er ergens een foutmelding staat: een stylesheet is stilletjes geweigerd.
- Passieve mixed content — afbeeldingen, audio, video. Laadt meestal nog wel, maar het slotje wordt gedegradeerd en sommige browsers tonen een melding dat de site niet veilig is.
Beide zijn het oplossen waard. Alleen de eerste maakt dingen stuk.
Stap 1: uitzoeken wat er nog onveilig is
Begin in de browser. Open de pagina, open de ontwikkelaarstools en lees de console. Elk geblokkeerd of gedegradeerd verzoek staat daar met de volledige URL. Dat vertelt je wat er onveilig is; het vertelt je niet waar het is opgeslagen.
Daarvoor kijk je rechtstreeks in de database. Exporteer hem en doorzoek de dump:
wp db export dump.sql
grep -o "http://example\.com[^\"']*" dump.sql | sort -u | head -50
Is WP-CLI niet beschikbaar, dan levert mysqldump hetzelfde bestand op:
mysqldump -u USER -p DBNAME > dump.sql
Lees de unieke URLs die terugkomen. Uploadpaden wijzen naar berichtinhoud en meta. Paden naar themabestanden wijzen meestal naar options. Alles met een domein dat je niet herkent is een externe embed, en die vraagt om een andere oplossing (verderop behandeld).
Stap 2: corrigeer eerst siteurl en home
siteurl en home zijn de twee options waarmee WordPress vrijwel elke interne URL opbouwt. Staat bij één van beide nog http://, dan blijft WordPress onveilige URLs uitspugen, hoe schoon de rest van de database ook is.
Controleer ze:
wp option get siteurl
wp option get home
Of in sql:
SELECT option_name, option_value
FROM wp_options
WHERE option_name IN ('siteurl', 'home');
Deze twee zijn gewone strings, geen geserialiseerde arrays, dus een directe sql-update is hier echt veilig:
UPDATE wp_options
SET option_value = REPLACE(option_value, 'http://example.com', 'https://example.com')
WHERE option_name IN ('siteurl', 'home');
Eén valkuil voordat je hier tijd in steekt. Definieert wp-config.php de constanten WP_HOME of WP_SITEURL, dan overschrijven die de database volledig en lijkt je update niets te doen:
define( 'WP_HOME', 'https://example.com' );
define( 'WP_SITEURL', 'https://example.com' );
Pas ze daar aan, of haal ze weg en laat de database leidend zijn. Controleer dit eerst — het verklaart heel wat gevallen van “ik heb het aangepast en er gebeurde niets”.
Stap 3: de vervanging moet serialisatieveilig zijn
De rest van de database is waar het echte risico zit, en het is niet het risico dat de meeste mensen verwachten. Het gevaar is niet dat de vervanging URLs mist. Het is dat hij op tekstniveau slaagt en de omliggende data vernielt.
WordPress slaat arrays en objecten op als php-geserialiseerde strings, en dat formaat legt de bytelengte van elke string erin vast:
a:1:{s:4:"logo";s:38:"http://example.com/img/logo.png";}
Verander http:// in https:// met een ruwe sql REPLACE en de tekst wordt één byte langer terwijl de opgegeven lengte op de oude waarde blijft staan. php leest de lengte, springt dat aantal bytes vooruit, vindt daar niet het verwachte eindteken en weigert de hele array te deserialiseren. WordPress geeft het thema of de plugin dan een waarde die zich gedraagt alsof de instelling nooit is opgeslagen.
Het symptoom is geen foutmelding. Het zijn verdwijnende widgets, customizer-instellingen die terugvallen, en pagebuilder-opmaak die leeg rendert — zonder dat er in de beheeromgeving iets op een probleem wijst. Herstellen zonder back-up is echt pijnlijk, dus maak er een:
wp db export backup-before-https-replace.sql
Gebruik daarna gereedschap dat deserialiseert, binnen de gedecodeerde structuur vervangt en met gecorrigeerde lengtes opnieuw serialiseert. Eerst een proefdraai:
wp search-replace 'http://example.com' 'https://example.com' --dry-run --all-tables --skip-columns=guid
Lees het rapport. Toont een tabel die je niet herkent duizenden treffers, stop dan en kijk eerst voordat je het vastlegt. Dan de echte run:
wp search-replace 'http://example.com' 'https://example.com' --all-tables --skip-columns=guid --report-changed-only
--skip-columns=guid doet ertoe: de guid is een permanente identificatie voor feedlezers, geen werkende URL, en hem herschrijven kan ervoor zorgen dat abonnees je hele archief als nieuwe berichten zien. Wil je zien welke kolommen veilig zijn voor gewone sql en welke een serialisatiebewuste aanpak vereisen voordat je iets uitvoert, bouw de statements dan met het WordPress zoek-en-vervang sql gereedschap.
Kun je de commandoregel niet gebruiken, dan doet een migratieplugin die uitdrukkelijk stelt geserialiseerde data aan te kunnen hetzelfde decodeerwerk via het beheerscherm. Zegt een tool dat niet, ga er dan van uit dat hij het niet kan.
Stap 4: https afdwingen op de server
Een opgeschoonde database zorgt ervoor dat je pagina’s geen onveilige bronnen meer opvragen. Het weerhoudt een bezoeker er niet van om überhaupt via http:// binnen te komen. Daarvoor is een omleiding op serverniveau nodig, en op Apache hoort die 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
Twee dingen over de plaatsing. Zet dit boven de # BEGIN WordPress markers — alles ertussen wordt weggegooid zodra iemand de permalinks opslaat. En het werkt alleen als mod_rewrite actief is en de virtual host overrides toestaat (AllowOverride All, of op zijn minst FileInfo). Op nginx wordt .htaccess volledig genegeerd en moet dit in een server-blok.
Een RedirectMatch kan deze klus niet klaren. Die matcht alleen op het aangevraagde pad en kan niet testen of het huidige verzoek al beveiligd is, dus leidt hij https-verzoeken naar zichzelf om. De voorwaardelijke RewriteRule hierboven is het juiste gereedschap.
De omleidingslus, en waarom die ontstaat
Blijft de site eindeloos omleiden zodra je die regel toevoegt, dan eindigt je TLS ergens stroomopwaarts — een load balancer, een reverse proxy of een CDN — die vervolgens gewoon http doorstuurt naar Apache. Apache ziet een onveilig verzoek, leidt om naar https, de proxy stuurt weer http door, en zo blijft het rondgaan.
Test in plaats daarvan de doorgestuurde header:
RewriteCond %{HTTP:X-Forwarded-Proto} !https
RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]
WordPress zelf heeft in die opzet dezelfde blinde vlek — is_ssl() leest $_SERVER['HTTPS'], wat de proxy nooit instelt, dus komen beheer-URLs eruit als http://. Voeg dit toe aan wp-config.php, boven de regel “stop editing”:
if ( isset( $_SERVER['HTTP_X_FORWARDED_PROTO'] ) && 'https' === $_SERVER['HTTP_X_FORWARDED_PROTO'] ) {
$_SERVER['HTTPS'] = 'on';
}
Even eerlijk over het risico: X-Forwarded-Proto is een header die de client aanlevert. Hem vertrouwen is alleen correct als een proxy die jij beheert hem altijd overschrijft. Op een server die rechtstreeks vanaf het internet bereikbaar is, valt hij te vervalsen.
Wat een databasevervanging niet oplost
- Hardgecodeerde URLs in bestanden. Alles wat in
functions.php, een child thema-template of een inline assetpad is geschreven, staat niet in de database. Doorzoek de themamap apart. - Ge-escapete JSON. Sommige builders slaan URLs op als
https:\/\/example.com. Een zoekopdracht op de normale vorm mist ze volledig, dus een tweede ronde op de ge-escapete variant kan nodig zijn. - Externe bronnen. Een script of embed van derden dat alleen over http beschikbaar is, kun je van jouw kant niet repareren. Zoek een https-versie of gooi het eruit.
- Caches. Paginacache, objectcache en CDN-kopieën blijven na een correcte vervanging de oude markup serveren. Leeg alle drie voordat je concludeert dat de vervanging is mislukt.
Eén ding dat je gerust kunt overslaan: de Content Security Policy header upgrade-insecure-requests legt de waarschuwingen het zwijgen op door onveilige verzoeken in de browser te herschrijven. Hij behandelt het symptoom en laat de verkeerde URLs in je database staan, waar de volgende export, migratie of feed ze weer meeneemt. Zet eerst de data recht en gebruik hem daarna als vangnet, als je wilt.
Nog steeds vastgelopen?
Controleer siteurl en home na elke stap opnieuw — plugins en migratietools herschrijven ze soms achter je rug om. Blijft de console een onveilige URL noemen die je niet in de dump kunt vinden, bekijk dan de paginabron en zoek hem daar: verschijnt hij in de gegenereerde markup maar niet in de database, dan wordt hij in php opgebouwd, en is het thema of de plugin die hem produceert het ding dat je moet repareren.
FAQ
Vragen
Waarom toont mijn WordPress site nog steeds mixed content waarschuwingen na het installeren van een SSL-certificaat?
Het certificaat verandert alleen hoe de verbinding versleuteld wordt, niet wat je pagina's opvragen. Oude http:// URLs blijven opgeslagen in de database, in berichtinhoud, widget-opties en thema-instellingen. De browser laadt de pagina over https, ziet een afbeelding of script dat over gewoon http wordt opgevraagd, en meldt mixed content op een verder beveiligde pagina.
Hoe vind ik de http:// URLs die nog in mijn WordPress database staan?
Open een pagina in een browser en lees de ontwikkelaarsconsole, die elk onveilig verzoek met volledige URL benoemt. Exporteer daarna de database en doorzoek de dump op je domein met het http-voorvoegsel. Het aantal treffers per tabel vertelt je of het probleem in de berichtinhoud zit, in wp_options, of in plugintabellen waar je geen rekening mee hield.
Kan ik mixed content oplossen met een gewone sql zoek-en-vervang actie?
Alleen voor scalaire waarden zoals siteurl en home. Alles wat WordPress als geserialiseerde array opslaat, en dat geldt voor de meeste option- en metawaarden, legt de bytelengte van elke string erin vast. Een ruwe vervanging wijzigt de tekst maar laat de oude lengte staan, waarna de waarde niet meer te deserialiseren is en de instelling stilletjes terugvalt op de standaardwaarde.
Welke .htaccess regel dwingt https af in WordPress?
Een RewriteRule met een voorwaarde op de HTTPS-variabele, geplaatst boven de BEGIN WordPress markers zodat het opslaan van permalinks hem niet wist. Achter een proxy of CDN staat de HTTPS-variabele op off, zelfs bij beveiligde verzoeken, dus test daar in plaats daarvan de header X-Forwarded-Proto of de regel blijft eindeloos omleiden.
Waarom belandde mijn site in een omleidingslus na het afdwingen van https?
Je TLS eindigt vrijwel zeker bij een proxy of CDN die vervolgens gewoon http doorstuurt naar Apache. De rewrite ziet een onveilig verzoek, leidt om naar https, de proxy stuurt weer http door, en de lus herhaalt zich. Zet de voorwaarde om naar de header X-Forwarded-Proto en stel de servervariabele HTTPS in de wp-config.php in.
Breekt mixed content de hele pagina of alleen het slotje?
Dat hangt van de bron af. Browsers blokkeren actieve mixed content zoals scripts, stylesheets en iframes volledig, wat de opmaak of functionaliteit kan slopen zonder zichtbare uitleg. Passieve mixed content zoals afbeeldingen en video laadt meestal nog wel, maar het slotje wordt gedegradeerd en bezoekers zien mogelijk een melding dat de site niet veilig is. Beide zijn het oplossen waard.