Mixed content w WordPress po przejściu na https
Usuń ostrzeżenia o mieszanej treści w WordPress po przejściu na https: znajdź adresy http:// pozostałe w bazie danych, bezpiecznie je podmień i wymuś https w pliku .htaccess.
Opublikowano
Certyfikat jest zainstalowany, witryna ładuje się przez https, a kłódka wciąż nie chce się pojawić. To właśnie mixed content, czyli mieszana treść: sama strona przyszła przez https, ale coś w środku — obrazek, arkusz stylów, skrypt — nadal jest pobierane zwykłym http://. Ten artykuł przeprowadzi cię przez znalezienie tych adresów, podmianę ich bez uszkodzenia danych serializowanych, poprawienie siteurl i home oraz wymuszenie https na poziomie serwera, żeby problem nie mógł wrócić.
Czym właściwie jest mieszana treść
Zainstalowanie certyfikatu zmienia sposób szyfrowania połączenia. Nie zmienia tego, o co proszą twoje strony. Każdy adres http://yoursite.com/wp-content/uploads/logo.png, który przed przenosinami trafił do wpisu, widgetu czy ustawienia motywu, nadal siedzi w bazie danych, a przeglądarka posłusznie pobiera go przez niezabezpieczone połączenie.
Przeglądarki traktują dwie kategorie inaczej, a różnica ma znaczenie, gdy oceniasz, jak pilna jest sprawa:
- Aktywna mieszana treść — skrypty, arkusze stylów, ramki iframe, XHR. Blokowana całkowicie. Dlatego witryna po przejściu na https potrafi wyglądać na zepsutą bez żadnego komunikatu o błędzie: arkusz stylów został po cichu odrzucony.
- Pasywna mieszana treść — obrazy, dźwięk, wideo. Zwykle nadal się ładuje, ale kłódka zostaje obniżona, a część przeglądarek pokazuje oznaczenie „niezabezpieczona”.
Warto naprawić jedno i drugie. Tylko pierwsze faktycznie psuje działanie.
Krok 1: ustal, co nadal jest niezabezpieczone
Zacznij w przeglądarce. Otwórz stronę, otwórz narzędzia deweloperskie i przeczytaj konsolę. Każde zablokowane lub obniżone żądanie jest tam wymienione z pełnym adresem. To mówi ci, co jest niezabezpieczone; nie mówi, gdzie to jest zapisane.
Po to trzeba zajrzeć wprost do bazy danych. Wyeksportuj ją i przeszukaj zrzut:
wp db export dump.sql
grep -o "http://example\.com[^\"']*" dump.sql | sort -u | head -50
Jeśli WP-CLI nie jest dostępne, ten sam plik utworzy mysqldump:
mysqldump -u USER -p DBNAME > dump.sql
Przeczytaj zwrócone unikalne adresy. Ścieżki do katalogu uploads wskazują na treść wpisów i metadane. Ścieżki do zasobów motywu zwykle wskazują na opcje. Wszystko z domeną, której nie rozpoznajesz, to osadzenie zewnętrzne, a to wymaga innego rozwiązania (opisanego niżej).
Krok 2: najpierw popraw siteurl i home
siteurl i home to dwie opcje, z których WordPress buduje niemal każdy generowany przez siebie adres wewnętrzny. Jeśli którakolwiek nadal ma http://, WordPress będzie dalej wypuszczał niezabezpieczone adresy, choćby reszta bazy była idealnie czysta.
Sprawdź je:
wp option get siteurl
wp option get home
Albo w sql:
SELECT option_name, option_value
FROM wp_options
WHERE option_name IN ('siteurl', 'home');
Te dwie wartości to zwykłe ciągi znaków, nie tablice serializowane, więc bezpośrednia aktualizacja sql jest tu naprawdę bezpieczna:
UPDATE wp_options
SET option_value = REPLACE(option_value, 'http://example.com', 'https://example.com')
WHERE option_name IN ('siteurl', 'home');
Jedna pułapka, zanim poświęcisz na to czas. Jeśli plik wp-config.php definiuje stałe WP_HOME lub WP_SITEURL, całkowicie nadpisują one bazę danych i twoja aktualizacja będzie wyglądać, jakby nic nie zrobiła:
define( 'WP_HOME', 'https://example.com' );
define( 'WP_SITEURL', 'https://example.com' );
Zaktualizuj je tam albo usuń i pozwól decydować bazie danych. Sprawdź to na początku — wyjaśnia to sporo przypadków w stylu „zmieniłem i nic się nie stało”.
Krok 3: zamiana musi być bezpieczna dla serializacji
Reszta bazy danych to miejsce, gdzie leży prawdziwe ryzyko, i nie jest to ryzyko, którego większość ludzi się spodziewa. Niebezpieczeństwo nie polega na tym, że zamiana pominie jakieś adresy. Polega na tym, że uda się na poziomie tekstu i zniszczy otaczające dane.
WordPress przechowuje tablice i obiekty jako ciągi serializowane przez php, a ten format zapisuje długość w bajtach każdego ciągu, który zawiera:
a:1:{s:4:"logo";s:38:"http://example.com/img/logo.png";}
Zamień http:// na https:// surowym poleceniem REPLACE w sql, a tekst wydłuży się o jeden bajt, podczas gdy zadeklarowana długość zostanie na starej wartości. php odczytuje długość, przesuwa się o tyle bajtów, nie znajduje tam oczekiwanego znaku zamykającego i odmawia deserializacji całej tablicy. WordPress podaje wtedy motywowi lub wtyczce wartość, która zachowuje się tak, jakby ustawienie nigdy nie zostało zapisane.
Objawem nie jest komunikat o błędzie. Objawem są znikające widgety, resetujące się ustawienia personalizacji i puste układy z kreatorów stron — bez żadnego śladu w panelu, że cokolwiek poszło nie tak. Odzyskanie tego bez kopii zapasowej jest naprawdę bolesne, więc ją zrób:
wp db export backup-before-https-replace.sql
Następnie użyj narzędzia, które deserializuje dane, podmienia je wewnątrz odkodowanej struktury i serializuje ponownie z poprawionymi długościami. Najpierw przebieg próbny:
wp search-replace 'http://example.com' 'https://example.com' --dry-run --all-tables --skip-columns=guid
Przeczytaj raport. Jeśli tabela, której nie rozpoznajesz, pokazuje tysiące trafień, zatrzymaj się i sprawdź, zanim to zatwierdzisz. Potem uruchom na serio:
wp search-replace 'http://example.com' 'https://example.com' --all-tables --skip-columns=guid --report-changed-only
--skip-columns=guid ma znaczenie: guid to trwały identyfikator dla czytników kanałów, a nie działający adres, i jego przepisanie może sprawić, że subskrybenci zobaczą całe twoje archiwum jako nowe wpisy. Jeśli przed uruchomieniem czegokolwiek chcesz sprawdzić, które kolumny są bezpieczne dla zwykłego sql, a które wymagają obsługi świadomej serializacji, zbuduj zapytania narzędziem do wyszukiwania i zamiany sql w WordPress.
Jeśli nie możesz korzystać z wiersza poleceń, wtyczka migracyjna, która wprost deklaruje obsługę danych serializowanych, wykona tę samą pracę dekodowania z poziomu panelu. Jeśli narzędzie tego nie deklaruje, zakładaj, że tego nie potrafi.
Krok 4: wymuś https na serwerze
Wyczyszczenie bazy sprawia, że twoje strony przestają prosić o niezabezpieczone zasoby. Nie powstrzymuje odwiedzającego przed wejściem przez http:// w pierwszej kolejności. Do tego służy przekierowanie na poziomie serwera, a w Apache jego miejsce jest w pliku .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
Dwie uwagi o miejscu. Umieść to powyżej znaczników # BEGIN WordPress — wszystko w środku jest kasowane, gdy tylko ktoś zapisze bezpośrednie odnośniki. I działa to tylko wtedy, gdy mod_rewrite jest włączony, a host wirtualny pozwala na nadpisania (AllowOverride All albo przynajmniej FileInfo). W nginx plik .htaccess jest całkowicie ignorowany i to musi trafić do bloku serwera.
RedirectMatch nie wykona tego zadania. Dopasowuje wyłącznie ścieżkę żądania i nie ma jak sprawdzić, czy bieżące żądanie jest już bezpieczne, więc przekierowuje żądania https na nie same. Warunkowa reguła RewriteRule powyżej to właściwe narzędzie.
Pętla przekierowań i skąd się bierze
Jeśli witryna zaczyna przekierowywać w nieskończoność w chwili dodania tej reguły, twoje TLS kończy się gdzieś wyżej — na load balancerze, serwerze reverse proxy albo w sieci CDN — a ten przekazuje do Apache zwykłe http. Apache widzi niezabezpieczone żądanie, przekierowuje na https, proxy znowu podaje http i tak w kółko.
Sprawdzaj zamiast tego nagłówek przekazany dalej:
RewriteCond %{HTTP:X-Forwarded-Proto} !https
RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]
Sam WordPress ma w takiej konfiguracji tę samą ślepą plamkę — is_ssl() czyta $_SERVER['HTTPS'], czego proxy nigdy nie ustawia, więc adresy panelu wychodzą jako http://. Dodaj to do wp-config.php, powyżej linii „stop editing”:
if ( isset( $_SERVER['HTTP_X_FORWARDED_PROTO'] ) && 'https' === $_SERVER['HTTP_X_FORWARDED_PROTO'] ) {
$_SERVER['HTTPS'] = 'on';
}
Warto uczciwie powiedzieć o ryzyku: X-Forwarded-Proto to nagłówek dostarczany przez klienta. Ufanie mu jest poprawne tylko wtedy, gdy kontrolowane przez ciebie proxy zawsze go nadpisuje. Na serwerze osiągalnym bezpośrednio z internetu można go sfałszować.
Czego zamiana w bazie danych nie naprawi
- Adresy zapisane na sztywno w plikach. Wszystko, co trafiło do
functions.php, szablonu motywu potomnego albo osadzonej ścieżki do zasobu, nie jest w bazie danych. Przeszukaj katalog motywu osobno. - JSON ze znakami ucieczki. Część kreatorów zapisuje adresy jako
https:\/\/example.com. Wyszukiwanie zwykłej postaci całkowicie je pomija, więc może być potrzebny drugi przebieg na wersji ze znakami ucieczki. - Zasoby zewnętrzne. Skryptu ani osadzenia od zewnętrznego dostawcy, dostępnego wyłącznie przez http, nie naprawisz po swojej stronie. Znajdź wersję na https albo z tego zrezygnuj.
- Pamięci podręczne. Cache stron, cache obiektów i kopie w sieci CDN nadal serwują stary kod po poprawnie wykonanej zamianie. Wyczyść wszystkie trzy, zanim uznasz, że zamiana się nie udała.
Jedna rzecz, którą warto pominąć: nagłówek Content Security Policy upgrade-insecure-requests uciszy ostrzeżenia, przepisując niezabezpieczone żądania w przeglądarce. Leczy objaw i zostawia błędne adresy w bazie danych, skąd kolejny eksport, migracja albo kanał poniesie je dalej. Napraw najpierw dane, a potem — jeśli chcesz — użyj go jako siatki bezpieczeństwa.
Nadal utknąłeś?
Sprawdzaj siteurl i home po każdym kroku — wtyczki i narzędzia migracyjne czasem przepisują je za twoimi plecami. Jeśli konsola nadal wskazuje niezabezpieczony adres, którego nie możesz znaleźć w zrzucie, obejrzyj źródło strony i poszukaj go tam: jeśli pojawia się w wygenerowanym kodzie, ale nie w bazie danych, jest budowany w php, a naprawić trzeba motyw lub wtyczkę, która go produkuje.
FAQ
Pytania
Dlaczego moja witryna WordPress nadal pokazuje ostrzeżenia o mieszanej treści po zainstalowaniu certyfikatu SSL?
Certyfikat zmienia tylko sposób szyfrowania połączenia, a nie to, o co proszą twoje strony. Stare adresy http:// nadal siedzą w bazie danych: w treści wpisów, w opcjach widgetów i w ustawieniach motywu. Przeglądarka ładuje stronę przez https, widzi obrazek lub skrypt pobierany zwykłym http i zgłasza mieszaną treść na skądinąd bezpiecznej stronie.
Jak znaleźć adresy http:// nadal obecne w bazie danych WordPress?
Otwórz stronę w przeglądarce i przeczytaj konsolę narzędzi deweloperskich, która wskazuje każde niezabezpieczone żądanie wraz z pełnym adresem. Następnie wyeksportuj bazę danych i przeszukaj zrzut pod kątem swojej domeny z przedrostkiem http. Liczba trafień w poszczególnych tabelach powie ci, czy problem tkwi w treści wpisów, w wp_options, czy w tabelach wtyczek, których się nie spodziewałeś.
Czy mogę naprawić mieszaną treść zwykłym wyszukaniem i zamianą w sql?
Tylko dla wartości skalarnych, takich jak siteurl i home. Wszystko, co WordPress przechowuje jako tablicę serializowaną, a to obejmuje większość wartości opcji i metadanych, zapisuje długość w bajtach każdego ciągu znaków w środku. Surowa zamiana zmienia tekst, ale zostawia starą długość, wartość przestaje się deserializować, a ustawienie po cichu wraca do wartości domyślnej.
Jaka reguła w .htaccess wymusza https w WordPress?
Reguła RewriteRule zabezpieczona warunkiem na zmiennej HTTPS, umieszczona powyżej znaczników BEGIN WordPress, żeby zapis bezpośrednich odnośników jej nie skasował. Za serwerem proxy lub siecią CDN zmienna HTTPS ma wartość off nawet przy bezpiecznych żądaniach, więc sprawdzaj tam nagłówek X-Forwarded-Proto, bo inaczej reguła będzie przekierowywać w nieskończoność.
Dlaczego moja witryna wpadła w pętlę przekierowań po wymuszeniu https?
Twoje TLS niemal na pewno kończy się na serwerze proxy lub w sieci CDN, która potem przekazuje do Apache zwykłe http. Reguła widzi niezabezpieczone żądanie, przekierowuje na https, proxy znowu podaje http i pętla się powtarza. Przestaw warunek na nagłówek X-Forwarded-Proto i ustaw zmienną serwerową HTTPS w pliku wp-config.php.
Czy mieszana treść psuje całą stronę, czy tylko kłódkę?
To zależy od zasobu. Aktywną mieszaną treść, czyli skrypty, arkusze stylów i ramki iframe, przeglądarki blokują całkowicie, co potrafi rozwalić układ lub funkcje bez żadnego widocznego wyjaśnienia. Pasywna mieszana treść, czyli obrazy i wideo, zwykle nadal się ładuje, ale kłódka zostaje obniżona, a odwiedzający mogą zobaczyć oznaczenie o braku bezpieczeństwa. Warto naprawić jedno i drugie.