Mixed content po włączeniu SSL: dlaczego kłódka nie jest zielona mimo certyfikatu
Certyfikat SSL jest zainstalowany, adres strony zaczyna się od https://, hosting potwierdza, że wszystko działa. A przeglądarka dalej informuje, że połączenie nie jest w pełni bezpieczne. Czasem dochodzą do tego znikające zdjęcia, rozjechany układ strony albo slider, menu czy przycisk koszyka, które nagle przestały reagować.
W zdecydowanej większości przypadków winna jest treść mieszana, czyli mixed content. Trafiam na nią regularnie po migracjach, przenosinach na nowy hosting i samodzielnym włączaniu SSL. Dobra wiadomość jest taka, że źródło da się namierzyć w kilka minut. Gorsza: „naprawa jedną wtyczką” często tylko przykrywa problem, zamiast go usunąć.
Jeśli na Twojej stronie w ogóle nie ma certyfikatu, zacznij od wpisu o braku certyfikatu SSL i jego wpływie na zaufanie klientów. Ten tekst dotyczy sytuacji, w której certyfikat jest, a przeglądarka i tak ma zastrzeżenia.
Czym jest mixed content
Sama strona ładuje się przez HTTPS, ale część jej elementów (zdjęcia, skrypty, arkusze stylów, czcionki, osadzone ramki) jest pobierana przez zwykłe, nieszyfrowane HTTP. Przeglądarka rozumuje prosto: skoro część danych idzie otwartym kanałem, całej strony nie można nazwać bezpieczną.
W praktyce spotkasz dwa rodzaje treści mieszanej:
- Pasywna: zdjęcia, pliki audio i wideo. Nowoczesne przeglądarki próbują same pobrać je przez HTTPS. Jeśli pod adresem https:// zasobu nie ma, po prostu się nie wczyta. Efekt to puste miejsca zamiast zdjęć i ostrzeżenie przy pasku adresu.
- Aktywna: skrypty JavaScript, pliki CSS, ramki iframe, zapytania w tle. Te przeglądarka blokuje bez dyskusji. Efekt to rozsypany wygląd, niedziałające menu, formularze, koszyk, a w sklepie nawet bramka płatności.
A gdzie ta zielona kłódka?
Mała uwaga, bo często słyszę to pytanie. Zielona kłódka to już trochę historia. Chrome od wersji 117 w ogóle nie pokazuje kłódki przy adresie, tylko ikonę ustawień strony. Informację o bezpieczeństwie połączenia zobaczysz dopiero po jej kliknięciu. Firefox nadal wyświetla kłódkę, a przy treści mieszanej dokłada do niej ostrzeżenie. Dlatego zamiast szukać koloru, sprawdzaj komunikat po kliknięciu ikony przy adresie: albo połączenie jest bezpieczne, albo nie.
Dlaczego nie warto tego odkładać, zwłaszcza w sklepie
- Zaufanie. Klient, który przy podawaniu danych i płatności widzi ostrzeżenie o niepełnym bezpieczeństwie, ma gotowy powód, żeby zamknąć kartę. Więcej o tym, jak drobiazgi budują lub niszczą zaufanie, pisałem we wpisie jak budować zaufanie w sklepie internetowym.
- Działanie strony. Zablokowany skrypt to nie tylko kosmetyka. Jeśli dotyczy koszyka, wyboru dostawy albo płatności, zamówienia zwyczajnie przestają przechodzić, a Ty widzisz tylko rosnącą liczbę porzuconych koszyków.
- Formularze. Jeśli formularz na stronie HTTPS wysyła dane pod adres http://, przeglądarka ostrzega użytkownika jeszcze przed wysłaniem.
- Wizerunek w wynikach wyszukiwania. Sam mixed content nie wyrzuci strony z Google, ale brakujące zdjęcia, rozjechany układ i gorsze wrażenia użytkownika już pracują na Twoją niekorzyść.
Ścieżka diagnostyczna: jak namierzyć źródło
Nie zaczynam od zgadywania ani od instalowania wtyczek. Najpierw ustalam, które dokładnie pliki ładują się przez HTTP i skąd biorą się ich adresy.
Krok 1. Konsola przeglądarki
Otwórz stronę, naciśnij F12 i przejdź do zakładki Console (Konsola). Każdy problematyczny zasób pojawi się tam jako ostrzeżenie zaczynające się od słów Mixed Content, razem z pełnym adresem pliku, który ładuje się przez HTTP. To najcenniejsza informacja w całej diagnozie, więc spisz te adresy.
Krok 2. Podgląd źródła strony
Naciśnij Ctrl+U, a potem Ctrl+F i wyszukaj http://. Zwróć uwagę na atrybuty src, arkusze stylów oraz zapisy url(...) w CSS. Zwykłe linki do innych stron (<a href="http://...">) nie powodują mixed content, więc nimi się teraz nie przejmuj.
Krok 3. Sprawdź różne typy podstron
Treść mieszana rzadko jest wszędzie. Często siedzi tylko w starszych wpisach, na kartach produktów, w stopce albo w koszyku. Przejdź przez stronę główną, przykładowy wpis, stronę produktu, koszyk i zamówienie. Pomocniczo możesz użyć darmowego skanera Why No Padlock, który sprawdza pojedynczy adres i wypisuje niezabezpieczone zasoby.
Krok 4. Przypisz adres do źródła
Sam adres pliku zwykle podpowiada, gdzie szukać:
/wp-content/uploads/: zdjęcia wstawione do treści, czyli stare adresy w bazie danych./wp-content/themes/: motyw, jego ustawienia albo ręcznie dopisany kod./wp-content/plugins/: konkretna wtyczka lub jej zapisana konfiguracja.- Obca domena: zewnętrzny skrypt, widżet, czcionka, mapa lub osadzona ramka.
Jeśli w konsoli widzisz skrypty z domen, których zupełnie nie kojarzysz, a nikt ich świadomie nie dodawał, potraktuj to poważnie. To bywa ślad infekcji, a wtedy zamiast poprawiać adresy zajrzyj do wpisu jak odwirusować stronę na WordPressie.
Najczęstsze przyczyny i jak je naprawiam
Zanim cokolwiek zmienisz: zrób pełną kopię zapasową plików i bazy danych. Część poniższych kroków działa bezpośrednio na danych strony.
1. Adres witryny w ustawieniach nadal zaczyna się od http
Wejdź w Ustawienia, a potem Ogólne i sprawdź pola Adres WordPressa oraz Adres witryny. Oba powinny zaczynać się od https://. Jeśli pola są wyszarzone i nie da się ich edytować, adres jest ustawiony na sztywno w pliku wp-config.php stałymi WP_HOME i WP_SITEURL. Wtedy zmianę robisz właśnie tam.
Uwaga: błąd w tych polach potrafi odciąć dostęp do panelu. Jeśli po zmianie nie możesz się zalogować, przydatny będzie wpis o pętli logowania do wp-admin.
2. Stare adresy zapisane w bazie danych
To najczęstsza przyczyna. WordPress zapisuje w treści wpisów i stron pełne adresy obrazków, razem z protokołem. Każde zdjęcie wstawione przed włączeniem SSL ma w bazie adres http://. Zmiana ustawień witryny tego nie poprawia, trzeba przejść przez całą bazę i zamienić stary adres na nowy.
Czego nie robić: zwykłej zamiany SQL w phpMyAdmin. Ustawienia widżetów, motywu i kreatorów stron są zapisane jako dane serializowane, w których zapamiętana jest długość każdego tekstu. Zamiana http na https wydłuża tekst o jeden znak, długość przestaje się zgadzać i WordPress odrzuca całe ustawienie. Kończy się to zniknięciem widżetów, menu albo konfiguracji motywu.
Dlatego używam narzędzi, które obsługują serializację. Najprościej zrobić to wtyczką Better Search Replace: wpisujesz http://twojadomena.pl jako szukany tekst, https://twojadomena.pl jako zamiennik, zaznaczasz wszystkie tabele i najpierw uruchamiasz próbę bez zapisu, żeby zobaczyć, ile zmian zostanie wprowadzonych.
Jeśli masz dostęp do SSH, to samo zrobisz w WP-CLI:
wp search-replace 'http://twojadomena.pl' 'https://twojadomena.pl' --all-tables --skip-columns=guid --dry-run
Parametr --dry-run tylko pokazuje, co zostałoby zmienione. Gdy wynik wygląda rozsądnie, uruchamiasz polecenie ponownie bez niego. Kolumnę guid celowo pomijam, bo to wewnętrzny identyfikator wpisu, a nie adres, którego używa przeglądarka.
Dwie rzeczy, o których łatwo zapomnieć. Po pierwsze, sprawdź też wariant z www i bez www, bo w starszej bazie często występują oba. Po drugie, jeśli strona jest zbudowana w Elementorze, skorzystaj dodatkowo z jego narzędzia do zamiany adresów URL i wygeneruj ponownie pliki CSS. Elementor trzyma własne kopie stylów, w których stare adresy potrafią przetrwać zamianę w bazie.
3. Adresy wpisane na sztywno w motywie lub CSS
Jeśli konsola wskazuje pliki z katalogu motywu, szukam adresów http:// w plikach header.php, footer.php i functions.php, w polu Dodatkowy CSS w Personalizacji (zwłaszcza w regułach background-image) oraz w ustawieniach motywu, gdzie wgrywa się logo i ikonę strony.
Poprawki w plikach gotowego motywu rób w motywie potomnym. Zmiany wprowadzone bezpośrednio w motywie znikną przy najbliższej aktualizacji, a przy okazji łatwo o sytuację opisaną we wpisie strona rozsypała się po aktualizacji wtyczki lub motywu.
4. Zewnętrzne skrypty i osadzenia
Stare kody liczników, widżetów opinii, map, czatów czy czcionek bywają wklejone z adresem http://. Zwykle wystarczy zmienić protokół na https://. Jeśli dostawca nie obsługuje HTTPS, to jasny sygnał, że usługa jest porzucona i lepiej ją usunąć albo zastąpić czymś aktualnym.
5. Pamięć podręczna, która trzyma starą wersję
Klasyczna sytuacja: wszystko poprawione, a ostrzeżenie dalej jest. Winny bywa cache. Wtyczki optymalizujące tworzą własne, połączone i zminifikowane pliki CSS i JS, które powstały jeszcze ze starymi adresami. Do tego dochodzi cache serwera i CDN, na przykład Cloudflare. Po naprawie wyczyść wszystkie warstwy po kolei, a na końcu sprawdź stronę w oknie prywatnym. Jak te warstwy ze sobą współpracują, opisywałem przy okazji tematu wolno działającego sklepu WooCommerce.
6. Serwer za proxy lub Cloudflare: WordPress nie wie, że działa na HTTPS
To przyczyna mniej oczywista, ale częsta. Jeśli szyfrowane połączenie kończy się na serwerze pośredniczącym (proxy, load balancer, Cloudflare), sam WordPress widzi ruch jako zwykłe HTTP. Wtedy część adresów, które generuje sam, dostaje protokół http://, mimo że w bazie wszystko jest już poprawne.
Rozwiązanie to dopisanie do pliku wp-config.php, powyżej linii z komentarzem o końcu edycji, takiego fragmentu:
if ( isset( $_SERVER['HTTP_X_FORWARDED_PROTO'] ) && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https' ) {
$_SERVER['HTTPS'] = 'on';
}
Jeśli korzystasz z Cloudflare, sprawdź też tryb SSL. Tryb Flexible oznacza, że połączenie między Cloudflare a Twoim serwerem w ogóle nie jest szyfrowane, i to on najczęściej stoi za tego typu problemami, łącznie z pętlami przekierowań. Docelowo ustawiam tryb Full (strict), a na serwerze ważny certyfikat.
A co z wtyczkami typu Really Simple SSL?
Takie wtyczki potrafią podmieniać adresy http na https „w locie”, zanim strona trafi do przeglądarki. To działa i jako szybka łatka jest w porządku. Sam jednak wolę naprawić problem u źródła, z trzech powodów. Po pierwsze, to kolejna warstwa, która przy każdym wyświetleniu przepisuje kod strony. Po drugie, po wyłączeniu wtyczki problem wraca w całości. Po trzecie, żadna wtyczka nie naprawi zasobu, który pod adresem https:// po prostu nie istnieje.
Siatka bezpieczeństwa: nagłówek upgrade-insecure-requests
Gdy źródła są już poprawione, dodaję zabezpieczenie na przyszłość. Na serwerach Apache wystarczy dopisać do pliku .htaccess:
<IfModule mod_headers.c>
Header always set Content-Security-Policy "upgrade-insecure-requests"
</IfModule>
Ten nagłówek każe przeglądarce automatycznie pobierać przez HTTPS wszystkie zasoby, które strona wskazuje przez HTTP. Dzięki temu pojedynczy stary adres w nowym wpisie nie zepsuje od razu wrażenia bezpieczeństwa. To jednak uzupełnienie, a nie zamiennik porządków: jeśli zasobu nie ma pod adresem https://, nagłówek go nie wyczaruje.
Na koniec: przekierowanie i porządki
- Przekierowanie 301 z HTTP na HTTPS. Każdy, kto wpisze stary adres lub trafi na stary link, powinien automatycznie trafić na wersję szyfrowaną. Wiele paneli hostingowych ma do tego gotową opcję.
- Google Search Console. Jeśli masz tam dodaną usługę dla adresu z http://, dodaj wersję z https:// albo usługę dla całej domeny.
- HSTS dopiero na końcu. Ten nagłówek każe przeglądarkom przez długi czas łączyć się z Twoją stroną wyłącznie przez HTTPS. Włączaj go dopiero wtedy, gdy wszystko działa bez zarzutu, bo jego skutków nie da się szybko cofnąć.
Szybka checklista
- Kopia zapasowa plików i bazy danych.
- Konsola przeglądarki (F12): lista adresów ładowanych przez HTTP.
- Adres WordPressa i adres witryny z https://.
- Zamiana adresów w bazie narzędziem obsługującym serializację, z wariantem www i bez www.
- Przegląd motywu, dodatkowego CSS i zewnętrznych skryptów.
- Konfiguracja proxy lub Cloudflare (tryb Full strict).
- Wyczyszczenie wszystkich warstw cache i test w oknie prywatnym.
- Przekierowanie 301, nagłówek upgrade-insecure-requests, na końcu ewentualnie HSTS.
Potrzebujesz pomocy?
Jeśli po tych krokach przeglądarka nadal ostrzega albo po prostu nie chcesz ruszać bazy danych na działającym sklepie, napisz do mnie. Znajdę źródło treści mieszanej i usunę je u podstaw, zamiast maskować problem kolejną wtyczką.
Nie chcesz robić tego samodzielnie?
Zajmę się tym za Ciebie. Napisz krótko, co się dzieje - odpowiem tego samego dnia roboczego.