Смешанный контент в WordPress после перехода на HTTPS
Убираем предупреждения о смешанном контенте в WordPress после переезда на HTTPS: находим оставшиеся в базе http://-адреса, безопасно их заменяем и включаем принудительный HTTPS в .htaccess.
Опубликовано
Сертификат установлен, сайт открывается по HTTPS, а замочек всё равно не появляется. Это и есть смешанный контент: сама страница пришла по HTTPS, но что-то внутри неё — картинка, стиль, скрипт — по-прежнему запрашивается по обычному http://. Разбираем, как найти эти адреса, заменить их, не испортив сериализованные данные, поправить siteurl и home и включить принудительный HTTPS на сервере, чтобы проблема не вернулась.
Что такое смешанный контент на самом деле
Установка сертификата меняет то, как шифруется соединение. Она не меняет то, что ваши страницы запрашивают. Каждый http://yoursite.com/wp-content/uploads/logo.png, записанный в запись, виджет или настройку темы до переезда, так и лежит в базе, а браузер послушно запрашивает его по незащищённому соединению.
Браузеры относятся к двум категориям по-разному, и это различие важно, когда вы решаете, насколько всё срочно:
- Активный смешанный контент — скрипты, стили, iframe, XHR. Блокируется полностью. Именно поэтому сайт после переезда на HTTPS может выглядеть сломанным вообще без сообщений об ошибках: стиль просто молча отклонили.
- Пассивный смешанный контент — изображения, аудио, видео. Обычно всё-таки грузится, но замочек понижается, и некоторые браузеры показывают отметку «не защищено».
Чинить стоит и то, и другое. Но ломает вещи только первое.
Шаг 1: выясните, что осталось незащищённым
Начните с браузера. Откройте страницу, откройте инструменты разработчика и прочитайте консоль. Каждый заблокированный или пониженный запрос назван там вместе с полным URL. Это говорит вам, что небезопасно, но не говорит, где оно хранится.
Для этого смотрите прямо в базу. Выгрузите её и поищите в дампе:
wp db export dump.sql
grep -o "http://example\.com[^\"']*" dump.sql | sort -u | head -50
Если WP-CLI недоступен, mysqldump даёт тот же файл:
mysqldump -u USER -p DBNAME > dump.sql
Прочитайте полученные уникальные адреса. Пути к загрузкам указывают на содержимое записей и мета-поля. Пути к ассетам темы обычно ведут в опции. Всё, что с незнакомым доменом, — это внешние встраивания, и им нужно другое лечение (о нём ниже).
Шаг 2: сначала исправьте siteurl и home
siteurl и home — это две опции, из которых WordPress строит почти каждый внутренний адрес, который генерирует. Если хоть в одной осталось http://, WordPress продолжит выдавать небезопасные URL, каким бы чистым ни было всё остальное в базе.
Проверьте их:
wp option get siteurl
wp option get home
Или в SQL:
SELECT option_name, option_value
FROM wp_options
WHERE option_name IN ('siteurl', 'home');
Это две обычные строки, а не сериализованные массивы, поэтому прямое обновление через SQL здесь действительно безопасно:
UPDATE wp_options
SET option_value = REPLACE(option_value, 'http://example.com', 'https://example.com')
WHERE option_name IN ('siteurl', 'home');
Одна ловушка, прежде чем вы потратите на это время. Если в wp-config.php определены WP_HOME или WP_SITEURL, эти константы полностью перекрывают базу, и ваше обновление будто бы ничего не сделает:
define( 'WP_HOME', 'https://example.com' );
define( 'WP_SITEURL', 'https://example.com' );
Поправьте их там или удалите и отдайте управление базе. Проверьте это первым делом — так объясняется изрядная часть случаев «я поменял, а ничего не изменилось».
Шаг 3: замена должна быть безопасной для сериализации
Всё остальное в базе — это как раз то место, где сидит настоящий риск, и он не тот, которого ждут. Опасность не в том, что замена пропустит какие-то адреса. Опасность в том, что она успешно отработает на уровне текста и уничтожит окружающие данные.
WordPress хранит массивы и объекты как PHP-сериализованные строки, а этот формат записывает длину каждой строки в байтах:
a:1:{s:4:"logo";s:38:"http://example.com/img/logo.png";}
Поменяйте http:// на https:// сырым SQL-выражением REPLACE — и текст станет на один байт длиннее, а объявленная длина останется прежней. PHP читает длину, отсчитывает столько байтов вперёд, не находит завершающий символ там, где ожидал, и отказывается десериализовать весь массив. После этого WordPress отдаёт теме или плагину значение, которое ведёт себя так, будто настройку никогда не сохраняли.
Симптом — не ошибка. Симптом — это исчезающие виджеты, сбрасывающиеся настройки кастомайзера и пустые макеты конструктора страниц, причём в админке ничто не намекает, что что-то пошло не так. Восстанавливаться после такого без резервной копии по-настоящему больно, так что сделайте её:
wp db export backup-before-https-replace.sql
Затем используйте инструмент, который десериализует данные, заменяет внутри разобранной структуры и сериализует обратно с исправленными длинами. Сначала холостой прогон:
wp search-replace 'http://example.com' 'https://example.com' --dry-run --all-tables --skip-columns=guid
Прочитайте отчёт. Если незнакомая таблица показывает тысячи совпадений, остановитесь и разберитесь, прежде чем применять. Затем запускайте по-настоящему:
wp search-replace 'http://example.com' 'https://example.com' --all-tables --skip-columns=guid --report-changed-only
--skip-columns=guid важен: guid — это постоянный идентификатор для читалок фидов, а не рабочий URL, и его переписывание может показать подписчикам весь ваш архив как новые записи. Если вам нужно понять, какие колонки безопасны для обычного SQL, а какие требуют обработки с учётом сериализации, соберите запросы в инструменте поиска и замены SQL для WordPress до того, как что-то запускать.
Если командная строка недоступна, плагин миграции, который прямо заявляет, что умеет работать с сериализованными данными, делает ту же работу по разбору через админку. Если инструмент об этом не заявляет — считайте, что не умеет.
Шаг 4: включите принудительный HTTPS на сервере
Чистка базы избавляет ваши страницы от запросов к небезопасным ресурсам. Но она не мешает посетителю прийти на http:// изначально. Это уровень сервера, и на Apache это место — .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
Два момента про размещение. Ставьте это выше маркеров # BEGIN WordPress — всё, что внутри них, выбрасывается каждый раз, когда кто-то сохраняет постоянные ссылки. И это работает только если включён mod_rewrite, а виртуальный хост разрешает переопределения (AllowOverride All или хотя бы FileInfo). На nginx .htaccess игнорируется полностью, и это правило нужно переносить в блок server.
RedirectMatch для такой задачи не годится. Он сопоставляет только путь запроса и никак не может проверить, защищён ли текущий запрос, поэтому будет редиректить HTTPS-запросы сами на себя. Условное RewriteRule выше — правильный инструмент.
Цикл редиректов и почему он возникает
Если сайт начинает редиректить бесконечно ровно в момент добавления этого правила, значит ваш TLS завершается где-то выше — на балансировщике, обратном прокси или CDN, — который дальше передаёт в Apache обычный HTTP. Apache видит небезопасный запрос, редиректит на HTTPS, прокси снова передаёт HTTP — и понеслось.
Проверяйте вместо этого пробрасываемый заголовок:
RewriteCond %{HTTP:X-Forwarded-Proto} !https
RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]
У самого WordPress в такой схеме то же слепое пятно — is_ssl() читает $_SERVER['HTTPS'], которую прокси никогда не выставляет, поэтому адреса админки получаются с http://. Добавьте это в wp-config.php выше строки «stop editing»:
if ( isset( $_SERVER['HTTP_X_FORWARDED_PROTO'] ) && 'https' === $_SERVER['HTTP_X_FORWARDED_PROTO'] ) {
$_SERVER['HTTPS'] = 'on';
}
Стоит честно сказать о риске: X-Forwarded-Proto — заголовок, приходящий от клиента. Доверять ему правильно только тогда, когда подконтрольный вам прокси всегда его перезаписывает. На сервере, доступном из интернета напрямую, его можно подделать.
Что замена в базе не исправит
- Жёстко прописанные адреса в файлах. Всё, что записано в
functions.php, шаблон дочерней темы или встроенный путь к ассету, в базе не лежит. Каталог темы придётся отгрепать отдельно. - Экранированный JSON. Некоторые конструкторы хранят адреса как
https:\/\/example.com. Поиск обычной формы их вообще не заметит, так что может понадобиться второй проход по экранированной строке. - Внешние ресурсы. Сторонний скрипт или встраивание, доступные только по HTTP, с вашей стороны не чинятся. Найдите HTTPS-версию или откажитесь от них.
- Кэши. Кэш страниц, объектный кэш и копии в CDN продолжают отдавать старую разметку и после корректной замены. Сбросьте все три, прежде чем решать, что замена не сработала.
Одну вещь стоит пропустить: заголовок Content Security Policy upgrade-insecure-requests заглушит предупреждения, переписывая небезопасные запросы прямо в браузере. Он лечит симптом и оставляет неправильные адреса в вашей базе, откуда их унесут следующий экспорт, миграция или фид. Сначала почините данные, а потом, если хотите, используйте его как страховку.
Всё ещё не получается?
Перепроверяйте siteurl и home после каждого шага — плагины и инструменты миграции иногда переписывают их за вашей спиной. Если консоль по-прежнему называет небезопасный адрес, которого вы не находите в дампе, откройте исходный код страницы и поищите его там: если он есть в сгенерированной разметке, но не в базе, значит его собирает PHP, и чинить надо тему или плагин, который его выдаёт.
FAQ
Вопросы
Почему сайт на WordPress продолжает показывать предупреждения о смешанном контенте после установки SSL-сертификата?
Сертификат меняет только то, как шифруется соединение, а не то, что запрашивают ваши страницы. Старые http://-адреса остаются в базе данных: в содержимом записей, в настройках виджетов и в опциях темы. Браузер загружает страницу по HTTPS, видит запрос картинки или скрипта по обычному HTTP и сообщает о смешанном контенте на в остальном безопасной странице.
Как найти http://-адреса, оставшиеся в базе данных WordPress?
Откройте страницу в браузере и посмотрите консоль разработчика — она называет каждый небезопасный запрос по URL. Затем выгрузите базу и поищите в дампе свой домен с префиксом http. Подсчёт совпадений по таблицам покажет, где именно живёт проблема: в содержимом записей, в wp_options или в таблицах плагинов, о которых вы не думали.
Можно ли исправить смешанный контент обычным поиском и заменой через SQL?
Только для скалярных значений вроде siteurl и home. Всё, что WordPress хранит как сериализованный массив, — а это большинство опций и мета-значений, — записывает длину каждой строки в байтах. Прямая замена меняет текст, но оставляет старую длину, значение перестаёт десериализоваться, и настройка молча откатывается к значению по умолчанию.
Какое правило .htaccess включает принудительный HTTPS в WordPress?
RewriteRule с условием на переменную HTTPS, размещённое выше маркеров BEGIN WordPress, чтобы сохранение постоянных ссылок его не стирало. За прокси или CDN переменная HTTPS читается как off даже на защищённых запросах, поэтому проверяйте вместо неё заголовок X-Forwarded-Proto, иначе правило будет редиректить бесконечно.
Почему после включения принудительного HTTPS сайт ушёл в цикл редиректов?
Почти наверняка ваш TLS завершается на прокси или CDN, который дальше передаёт в Apache обычный HTTP. Правило видит небезопасный запрос, редиректит на HTTPS, прокси снова передаёт HTTP — и цикл повторяется. Переключите условие на заголовок X-Forwarded-Proto и выставьте серверную переменную HTTPS в wp-config.php.
Смешанный контент ломает всю страницу или только замочек?
Зависит от ресурса. Активный смешанный контент — скрипты, стили и iframe — браузеры блокируют полностью, из-за чего вёрстка или функциональность могут сломаться без видимых объяснений. Пассивный смешанный контент вроде картинок и видео обычно всё же грузится, но замочек понижается и посетители могут увидеть отметку «не защищено». Чинить стоит и то, и другое.