Змішаний контент у 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 — це постійний ідентифікатор для читачок стрічок, а не робоча адреса, і його переписування може показати передплатникам увесь ваш архів як нові записи. Якщо вам треба зрозуміти, які колонки безпечні для звичайного 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 — браузери блокують повністю, через що верстка чи функціональність можуть зламатися без жодних видимих пояснень. Пасивний змішаний контент на кшталт зображень і відео зазвичай усе ж завантажується, але замочок понижується і відвідувачі можуть побачити позначку «не захищено». Виправляти варто і те, і те.