WordPress Mixed Content nach der Umstellung auf https
Mixed-Content-Warnungen in WordPress nach dem Wechsel auf https beseitigen: die verbliebenen http:// URLs in der Datenbank finden, sicher ersetzen und https in der .htaccess erzwingen.
Veröffentlicht
Dein Zertifikat ist installiert, die Website lädt über https — und das Schloss-Symbol erscheint trotzdem nicht. Das ist Mixed Content: Die Seite selbst kam über https, aber etwas darin — ein Bild, ein Stylesheet, ein Skript — wird weiterhin über einfaches http:// angefordert. Dieser Artikel zeigt, wie du diese URLs findest, sie ersetzt, ohne serialisierte Daten zu zerstören, siteurl und home korrigierst und https auf Serverebene erzwingst, damit das Problem nicht zurückkommt.
Was Mixed Content wirklich ist
Ein installiertes Zertifikat ändert, wie die Verbindung verschlüsselt wird. Es ändert nicht, was deine Seiten anfordern. Jedes http://yoursite.com/wp-content/uploads/logo.png, das vor dem Wechsel in einen Beitrag, ein Widget oder eine Theme-Einstellung geschrieben wurde, liegt weiterhin in der Datenbank — und der Browser fordert es brav über eine unsichere Verbindung an.
Browser behandeln zwei Kategorien unterschiedlich, und der Unterschied entscheidet darüber, wie dringend die Sache ist:
- Aktiver Mixed Content — Skripte, Stylesheets, iframes, XHR. Wird komplett blockiert. Deshalb kann eine Website nach dem Wechsel auf https kaputt aussehen, ohne dass irgendwo eine Fehlermeldung steht: Ein Stylesheet wurde stillschweigend verweigert.
- Passiver Mixed Content — Bilder, Audio, Video. Wird meist noch geladen, aber das Schloss-Symbol wird herabgestuft und manche Browser zeigen den Hinweis „nicht sicher“.
Beides gehört behoben. Nur das Erste zerstört etwas.
Schritt 1: herausfinden, was noch unsicher ist
Fang im Browser an. Öffne die Seite, öffne die Entwicklerwerkzeuge und lies die Konsole. Jede blockierte oder herabgestufte Anfrage steht dort mit vollständiger URL. Das sagt dir, was unsicher ist; es sagt dir nicht, wo es gespeichert ist.
Dafür schaust du direkt in die Datenbank. Exportiere sie und durchsuche den Dump:
wp db export dump.sql
grep -o "http://example\.com[^\"']*" dump.sql | sort -u | head -50
Steht WP-CLI nicht zur Verfügung, erzeugt mysqldump dieselbe Datei:
mysqldump -u USER -p DBNAME > dump.sql
Lies die eindeutigen URLs, die zurückkommen. Uploads-Pfade deuten auf Beitragsinhalte und Meta-Werte. Pfade zu Theme-Assets liegen meist in den Options. Alles mit einer Domain, die du nicht kennst, ist eine externe Einbindung — die braucht eine andere Lösung (weiter unten).
Schritt 2: zuerst siteurl und home korrigieren
siteurl und home sind die beiden Options, aus denen WordPress fast jede interne URL zusammenbaut. Steht bei einer davon noch http://, gibt WordPress weiterhin unsichere URLs aus, egal wie sauber der Rest der Datenbank ist.
Prüfe sie:
wp option get siteurl
wp option get home
Oder in sql:
SELECT option_name, option_value
FROM wp_options
WHERE option_name IN ('siteurl', 'home');
Diese beiden sind einfache Zeichenketten, keine serialisierten Arrays — ein direktes sql-Update ist hier also wirklich sicher:
UPDATE wp_options
SET option_value = REPLACE(option_value, 'http://example.com', 'https://example.com')
WHERE option_name IN ('siteurl', 'home');
Eine Falle, bevor du Zeit darin versenkst: Definiert die wp-config.php die Konstanten WP_HOME oder WP_SITEURL, überschreiben diese die Datenbank vollständig und dein Update scheint wirkungslos:
define( 'WP_HOME', 'https://example.com' );
define( 'WP_SITEURL', 'https://example.com' );
Passe sie dort an oder entferne sie und lass die Datenbank entscheiden. Prüfe das zuerst — es erklärt viele Fälle von „ich habe es geändert und nichts ist passiert“.
Schritt 3: das Ersetzen muss serialisierungssicher sein
Der Rest der Datenbank birgt das eigentliche Risiko, und zwar nicht das, mit dem die meisten rechnen. Die Gefahr ist nicht, dass das Ersetzen URLs übersieht. Sie besteht darin, dass es auf Textebene gelingt und dabei die umliegenden Daten zerstört.
WordPress speichert Arrays und Objekte als php-serialisierte Zeichenketten, und dieses Format hält die Bytelänge jeder enthaltenen Zeichenkette fest:
a:1:{s:4:"logo";s:38:"http://example.com/img/logo.png";}
Ersetzt du http:// per rohem sql REPLACE durch https://, wird der Text ein Byte länger, während die angegebene Länge auf dem alten Wert bleibt. php liest die Länge, springt so viele Bytes weiter, findet dort nicht das erwartete Abschlusszeichen und verweigert die Deserialisierung des gesamten Arrays. WordPress reicht dann einen Wert an Theme oder Plugin weiter, der sich verhält, als wäre die Einstellung nie gespeichert worden.
Das Symptom ist keine Fehlermeldung. Es sind verschwundene Widgets, zurückgesetzte Customizer-Einstellungen und leer gerenderte Page-Builder-Layouts — ohne dass im Backend irgendetwas auf einen Fehler hinweist. Ohne Backup ist die Wiederherstellung wirklich schmerzhaft, also mach eines:
wp db export backup-before-https-replace.sql
Nutze dann ein Werkzeug, das deserialisiert, innerhalb der dekodierten Struktur ersetzt und mit korrigierten Längen neu serialisiert. Erst ein Probelauf:
wp search-replace 'http://example.com' 'https://example.com' --dry-run --all-tables --skip-columns=guid
Lies den Bericht. Zeigt eine Tabelle, die du nicht kennst, tausende Treffer, halt an und schau nach, bevor du es festschreibst. Dann der echte Lauf:
wp search-replace 'http://example.com' 'https://example.com' --all-tables --skip-columns=guid --report-changed-only
--skip-columns=guid ist wichtig: Die guid ist eine dauerhafte Kennung für Feedreader, keine funktionierende URL. Schreibst du sie um, sehen Abonnenten unter Umständen dein gesamtes Archiv als neue Beiträge. Wenn du wissen willst, welche Spalten sich für einfaches sql eignen und welche eine serialisierungsbewusste Behandlung brauchen, bevor du irgendetwas ausführst, baue die Statements mit dem WordPress Suchen-und-Ersetzen sql Werkzeug.
Wenn du die Kommandozeile nicht nutzen kannst, erledigt ein Migrations-Plugin, das ausdrücklich angibt, serialisierte Daten zu unterstützen, dieselbe Dekodierarbeit über das Backend. Sagt ein Werkzeug das nicht, geh davon aus, dass es das nicht kann.
Schritt 4: https auf Serverebene erzwingen
Eine bereinigte Datenbank sorgt dafür, dass deine Seiten keine unsicheren Ressourcen mehr anfordern. Sie hindert einen Besucher nicht daran, überhaupt über http:// anzukommen. Dafür braucht es eine Weiterleitung auf Serverebene, und unter Apache gehört die in die .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
Zwei Dinge zur Platzierung. Setze das oberhalb der # BEGIN WordPress Marker — alles dazwischen wird verworfen, sobald jemand die Permalinks speichert. Und es funktioniert nur, wenn mod_rewrite aktiv ist und der Virtual Host Overrides erlaubt (AllowOverride All oder zumindest FileInfo). Unter nginx wird die .htaccess komplett ignoriert; dort gehört das in einen Server-Block.
Ein RedirectMatch erledigt diese Aufgabe nicht. Er vergleicht nur den Anfragepfad und kann nicht prüfen, ob die aktuelle Anfrage bereits sicher ist — er leitet https-Anfragen also auf sich selbst um. Die bedingte RewriteRule oben ist das richtige Werkzeug.
Die Endlosschleife, und warum sie entsteht
Leitet die Website in dem Moment endlos weiter, in dem du diese Regel hinzufügst, endet dein TLS irgendwo weiter oben — an einem Load Balancer, einem Reverse Proxy oder einem CDN — der danach einfaches http an Apache weiterreicht. Apache sieht eine unsichere Anfrage, leitet auf https um, der Proxy schickt wieder http, und so geht es rundherum.
Prüfe stattdessen den weitergereichten Header:
RewriteCond %{HTTP:X-Forwarded-Proto} !https
RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]
WordPress selbst hat in diesem Aufbau denselben blinden Fleck: is_ssl() liest $_SERVER['HTTPS'], was der Proxy nie setzt, also kommen Backend-URLs als http:// heraus. Ergänze das in der wp-config.php, oberhalb der Zeile „stop editing“:
if ( isset( $_SERVER['HTTP_X_FORWARDED_PROTO'] ) && 'https' === $_SERVER['HTTP_X_FORWARDED_PROTO'] ) {
$_SERVER['HTTPS'] = 'on';
}
Zum Risiko sollte man ehrlich sein: X-Forwarded-Proto ist ein vom Client gelieferter Header. Ihm zu vertrauen ist nur dann richtig, wenn ein Proxy unter deiner Kontrolle ihn immer überschreibt. Auf einem direkt aus dem Internet erreichbaren Server lässt er sich fälschen.
Was ein Datenbank-Ersetzen nicht behebt
- Fest eingetragene URLs in Dateien. Alles, was in
functions.php, ein Child-Theme-Template oder einen Inline-Asset-Pfad geschrieben wurde, steht nicht in der Datenbank. Durchsuche das Theme-Verzeichnis separat. - Escaptes JSON. Manche Builder speichern URLs als
https:\/\/example.com. Eine Suche nach der normalen Form findet sie gar nicht, ein zweiter Durchlauf auf die escapte Variante kann also nötig sein. - Externe Ressourcen. Ein Skript oder Embed von Dritten, das nur über http verfügbar ist, kannst du von deiner Seite aus nicht reparieren. Finde eine https-Variante oder wirf es raus.
- Caches. Seiten-Cache, Object-Cache und CDN-Kopien liefern das alte Markup auch nach einem korrekten Ersetzen weiter aus. Leere alle drei, bevor du das Ersetzen für gescheitert erklärst.
Eine Sache, die du getrost auslassen kannst: Der Content-Security-Policy-Header upgrade-insecure-requests bringt die Warnungen zum Schweigen, indem er unsichere Anfragen im Browser umschreibt. Er kuriert das Symptom und lässt die falschen URLs in deiner Datenbank, von wo der nächste Export, die nächste Migration oder der nächste Feed sie weiterträgt. Bring erst die Daten in Ordnung und nutz ihn danach als Sicherheitsnetz, wenn du magst.
Immer noch festgefahren?
Prüfe siteurl und home nach jedem Schritt erneut — Plugins und Migrationswerkzeuge schreiben sie manchmal hinter deinem Rücken um. Nennt die Konsole weiterhin eine unsichere URL, die du im Dump nicht findest, sieh dir den Quelltext der Seite an und suche sie dort: Taucht sie im erzeugten Markup auf, aber nicht in der Datenbank, wird sie in php gebaut — und dann ist das Theme oder Plugin, das sie erzeugt, die Stelle, die du reparieren musst.
FAQ
Fragen
Warum zeigt meine WordPress Website nach der Installation eines SSL-Zertifikats immer noch Mixed-Content-Warnungen?
Das Zertifikat ändert nur, wie die Verbindung verschlüsselt wird, nicht, was deine Seiten anfordern. Alte http:// URLs bleiben in der Datenbank gespeichert — im Beitragsinhalt, in Widget-Optionen und in Theme-Einstellungen. Der Browser lädt die Seite über https, sieht ein Bild oder Skript, das über einfaches http angefordert wird, und meldet Mixed Content auf einer ansonsten sicheren Seite.
Wie finde ich die http:// URLs, die noch in meiner WordPress Datenbank stecken?
Öffne eine Seite im Browser und lies die Entwicklerkonsole — sie benennt jede unsichere Anfrage mit vollständiger URL. Exportiere anschließend die Datenbank und durchsuche den Dump nach deiner Domain mit dem Präfix http. Die Trefferzahl pro Tabelle verrät dir, ob das Problem im Beitragsinhalt, in wp_options oder in Plugin-Tabellen liegt, mit denen du nicht gerechnet hast.
Kann ich Mixed Content mit einem einfachen sql Suchen-und-Ersetzen beheben?
Nur bei skalaren Werten wie siteurl und home. Alles, was WordPress als serialisiertes Array ablegt — und das betrifft die meisten Options- und Meta-Werte — speichert die Bytelänge jeder enthaltenen Zeichenkette mit. Ein reines Ersetzen ändert den Text, lässt aber die alte Länge stehen; der Wert lässt sich dann nicht mehr deserialisieren und die Einstellung fällt stillschweigend auf den Standard zurück.
Welche .htaccess Regel erzwingt https in WordPress?
Eine RewriteRule mit einer Bedingung auf die Variable HTTPS, platziert oberhalb der BEGIN WordPress Marker, damit das Speichern der Permalinks sie nicht löscht. Hinter einem Proxy oder CDN steht die Variable HTTPS auch bei sicheren Anfragen auf off — prüfe dort stattdessen den Header X-Forwarded-Proto, sonst leitet die Regel endlos weiter.
Warum ist meine Website nach dem Erzwingen von https in eine Endlosschleife geraten?
Mit ziemlicher Sicherheit endet dein TLS an einem Proxy oder CDN, der danach einfaches http an Apache weiterreicht. Die Rewrite-Regel sieht eine unsichere Anfrage, leitet auf https um, der Proxy schickt wieder http, und die Schleife wiederholt sich. Stell die Bedingung auf den Header X-Forwarded-Proto um und setze die Servervariable HTTPS in der wp-config.php.
Zerstört Mixed Content die ganze Seite oder nur das Schloss-Symbol?
Das hängt von der Ressource ab. Aktiven Mixed Content wie Skripte, Stylesheets und iframes blockieren Browser komplett, was Layout oder Funktionen ohne sichtbare Erklärung zerlegen kann. Passiver Mixed Content wie Bilder und Video wird meist noch geladen, aber das Schloss-Symbol wird herabgestuft und Besucher sehen womöglich einen Hinweis „nicht sicher“. Beides gehört behoben.