Skip to content
EN
← Все статьи

Mixed content: как найти и исправить HTTP на HTTPS-сайте

Коротко. Mixed content — это загрузка подресурсов по http:// на странице, открытой по https://. Активный контент (скрипты, стили, iframe, fetch, WebSocket) браузер блокирует жёстко: подменённый скрипт получает полный контроль над страницей. Пассивный (картинки, видео, аудио) браузер молча апгрейдит до HTTPS и выбрасывает, если апгрейд не удался. Чинится не одним заголовком, а по слоям: шаблоны, контент в базе, сериализованные настройки, сторонние виджеты.

Что такое mixed content и почему браузер его режет

HTTPS даёт странице три гарантии: конфиденциальность (трафик не читается), целостность (ответ не подменён) и аутентификацию (вы говорите с тем сервером, чьё имя в сертификате). Подробнее о том, как это работает, — в руководстве по SSL/TLS.

Один подресурс, загруженный по http://, ломает вторую и третью гарантии для всей страницы. Атакующему на пути трафика (публичный Wi-Fi, скомпрометированный домашний роутер, прозрачный прокси провайдера, транзитная сеть) не нужно ломать TLS. Достаточно дождаться единственного незашифрованного запроса и ответить на него своим содержимым.

Дальше всё зависит от того, что именно вы загрузили по HTTP:

  • Картинка — подменяется изображение. Логотип банка, кнопка «Оплатить», скриншот инструкции. Плюс сам факт запроса утекает третьей стороне: адрес страницы в Referer, IP, User-Agent.
  • Скрипт — атакующий выполняет свой код в вашем origin. Это чтение и запись DOM, доступ к document.cookie (всё, что не HttpOnly), перехват отправки форм, подмена реквизитов, кража токенов из localStorage. Разницы с полноценной XSS практически нет.
  • Стиль — подменённый CSS перерисовывает страницу, прячет реальную форму и показывает свою, подтягивает произвольные шрифты и фоновые изображения. Через @import он может дотянуть ещё код.

Именно поэтому браузер не ограничивается предупреждением. Для активного контента блокировка — единственное безопасное поведение по умолчанию, и отключить её со стороны сайта нельзя.

Mixed content — это не косметика и не «замочек стал серым». Это дыра ровно того же класса, что XSS, только эксплуатируется с сетевой позиции, а не через ввод пользователя.

Что НЕ считается mixed content

  • Обычная ссылка <a href='http://example.com'>. Это навигация верхнего уровня, а не подресурс текущей страницы. Браузер по ней перейдёт и покажет «Не защищено» уже на новой странице. Такие ссылки чинит HSTS и редиректы, а не CSP.
  • Запросы, которые делает ваш сервер. Если PHP-бэкенд ходит к внешнему API по HTTP, браузер об этом не знает. Проблема реальная, но искать её надо в логах приложения, а не в консоли.
  • Схемы data: и blob: — считаются потенциально доверенными и не блокируются.
  • http://localhost, http://127.0.0.1, *.localhost — по спецификации это потенциально доверенные origin. На локальной разработке mixed content не воспроизводится, и это классическая причина «у меня всё работает».

Passive и active mixed content: различие, которое решает всё

Спецификация W3C Mixed Content делит небезопасные подресурсы на два класса, и от класса зависит и поведение браузера, и стратегия починки.

Passive (display) mixed content

Это изображения, видео и аудио — контент, который вставляется в страницу, но не может выполнять код и не имеет доступа к DOM. В спецификации он называется optionally-blockable.

Раньше браузеры такой ресурс просто загружали, снимая замок в адресной строке. Сейчас поведение другое: браузер сначала пытается запросить тот же URL по HTTPS (автоматический апгрейд), и только если HTTPS-запрос не удался — ресурс не отображается вовсе. Chrome, Firefox и Safari перешли на эту модель в разных версиях, но направление одно.

Практический вывод, который меняет диагностику: «пассивный» больше не значит «загрузится с предупреждением». Если у стороннего хоста нет HTTPS, или сертификат просрочен, или неполная цепочка сертификатов — картинка просто исчезнет. Причём в консоли будет уже не «Mixed Content», а сетевая ошибка вроде net::ERR_CERT_AUTHORITY_INVALID, и связь с исходной http-ссылкой в разметке неочевидна.

Active (blockable) mixed content

Это всё, что может исполняться или влиять на исполнение: <script>, <link rel='stylesheet'>, <iframe>, <object>, <embed>, веб-шрифты, XHR и fetch(), EventSource, Web Workers, WebSocket по ws://, navigator.sendBeacon().

Такой запрос блокируется без попытки апгрейда и без возможности разрешить его со стороны сайта. В консоли появляется:

Mixed Content: The page at 'https://example.com/' was loaded over HTTPS,
but requested an insecure script 'http://cdn.example.net/widget.js'.
This request has been blocked; the content must be served over HTTPS.

Формулировка после «insecure» — самое полезное слово в сообщении: script, stylesheet, frame, font, XMLHttpRequest endpoint. Оно сразу указывает, какой слой чинить.

РесурсКлассЧто делает браузерЧем чинить
<img>, <picture>, srcsetПассивныйАпгрейд до HTTPS; при неудаче не отображаетсяЗамена URL в контенте и медиатеке
CSS background-image: url(http://…)ПассивныйАпгрейд; при неудаче фона нетПравка исходного SCSS/CSS и пересборка
<video>, <audio>, posterПассивныйАпгрейд; при неудаче плеер пустойПеренос медиа на HTTPS-хост
<link rel='icon'> (favicon)ПассивныйАпгрейд; при неудаче иконка по умолчаниюОтносительный путь вместо абсолютного
<script src>АктивныйБлокируется полностьюHTTPS-URL, самохостинг или замена
<link rel='stylesheet'>, @importАктивныйБлокируется полностьюHTTPS-URL или локальная копия
Веб-шрифты в @font-faceАктивныйБлокируется; текст рисуется фолбэкомСамохостинг шрифтов
<iframe src>АктивныйБлокируется; пустая рамкаHTTPS-эмбед, прокси или замена сервиса
<object>, <embed>АктивныйБлокируетсяОтказ от плагина, HTML5-аналог
XHR, fetch(), EventSourceАктивныйБлокируется, промис отклоняетсяHTTPS на API-эндпоинте
WebSocket ws://АктивныйБлокируется при открытииПеревод на wss://
navigator.sendBeacon(), WorkerАктивныйБлокируется молчаHTTPS-эндпоинт сбора
<form action='http://…'>Особый случайНе mixed content, но браузер предупреждает об отправке в открытом видеHTTPS в action
<a href='http://…'>Не считаетсяПереход разрешёнРедирект и HSTS на целевом домене
Схема: HTTPS-страница загружает часть подресурсов по HTTP, активные блокируются, пассивные апгрейдятся
Активный контент блокируется без вариантов, пассивный сначала пробуют апгрейдить до HTTPS.

Что ломается на практике: симптом, причина, проверка

Владелец сайта редко приходит со словами «у меня mixed content». Он приходит с симптомом. Таблица ниже — карта соответствий.

СимптомЧто произошлоКак проверитьФикс
Страница «разъехалась», голый HTML без оформленияЗаблокирован основной CSS-бандлConsole: insecure stylesheetHTTPS-URL стиля в шаблоне
Не работают меню, слайдеры, модалкиЗаблокирован jQuery или бандл скриптовConsole: insecure script + $ is not definedHTTPS-CDN или локальная копия
Форма отправляется «в никуда», нет ответаЗаблокирован fetch к http-APINetwork: запрос в статусе (blocked:mixed-content)HTTPS на эндпоинте
Пропали счётчики, метрика не пишет визитыЗаблокирован тег аналитикиОтсутствие запроса в NetworkОбновить сниппет счётчика
Пустой прямоугольник вместо карты или видеоЗаблокирован iframeConsole: insecure frameHTTPS-эмбед у провайдера
Текст «прыгает» и рисуется системным шрифтомЗаблокирован веб-шрифтConsole: insecure fontСамохостинг шрифтов
Часть картинок в старых статьях исчезлаПассивный апгрейд не удался: у хоста нет HTTPSNetwork: ошибка сертификата на домене-источникеПерезалить изображения к себе
Замок серый, но визуально всё целоЕдинственная http-картинка или faviconПроверка mixed contentТочечная замена ссылки
Ломается только у части пользователейРесурс подгружается на редком сценарии — корзина, ЛК, поискCSP Report-Only на живом трафикеПо адресам из отчётов

Отдельная ловушка: у авторизованного пользователя набор подключаемых скриптов часто отличается от гостевого. Проверять только главную в анонимном режиме — гарантированный способ пропустить половину проблем.

Как найти все вхождения mixed content

Ни один метод не находит всё. Рабочая схема — четыре независимых прохода: браузер, отчёты CSP с живого трафика, краулер и grep по коду и дампу базы.

DevTools: Console, Issues и Network

Быстрая проверка одной страницы:

  1. Console — фильтр по строке Mixed Content. Показывает и заблокированное, и апгрейженное.
  2. Issues (отдельная вкладка) — группирует нарушения по типу и даёт ссылку на строку в исходнике.
  3. Network — снимите галку кэша, перезагрузите с Ctrl+Shift+R. Заблокированные запросы видны в статусе как (blocked:mixed-content). В колонке Initiator видно, кто именно инициировал запрос — это единственный надёжный способ поймать ресурс, вставленный сторонним скриптом в рантайме.
  4. Security — сводка по origin страницы и списку небезопасных источников.

Ограничение метода очевидно: вы видите только те страницы, которые открыли, и только тот сценарий, который прошли.

CSP Report-Only: сбор нарушений с живого трафика

Самый мощный способ. Вы вешаете отчётную политику, которая ничего не блокирует, но присылает JSON на каждое нарушение — с реальных страниц, которые открывают реальные пользователи. Трюк в том, чтобы разрешить любой источник по схеме https: и запретить всё остальное: тогда каждый http-ресурс станет нарушением.

# /etc/nginx/conf.d/csp-report-only.conf
# Ничего не блокирует. Задача одна: собрать список http-подресурсов
# со всех страниц и всех сценариев, включая личный кабинет и корзину.

add_header Reporting-Endpoints 'csp-endpoint="https://example.com/_csp"' always;
add_header Content-Security-Policy-Report-Only "default-src https: data: blob: 'unsafe-inline' 'unsafe-eval'; report-uri /_csp; report-to csp-endpoint" always;

# 'unsafe-inline' и 'unsafe-eval' здесь намеренно: они убирают шум от
# инлайновых скриптов, чтобы в отчётах остались только нарушения схемы.
# report-uri оставлен ради старых браузеров, report-to - актуальный механизм.

Три подводных камня, на которых теряют время:

  • Наследование add_header в nginx. Если во вложенном location есть хотя бы один свой add_header, все родительские заголовки в этом блоке исчезают. Проверяйте фактический вывод, а не конфиг.
  • upgrade-insecure-requests в Report-Only игнорируется. Это прямо оговорено в спецификации. Если вы одновременно включите апгрейд и отчёты в одной Report-Only-политике, апгрейда не будет.
  • Через <meta> отчёты не работают. Директивы report-uri, report-to, frame-ancestors и sandbox в meta-теге игнорируются. Только HTTP-заголовок.

Разбор формата отчётов и типовых ошибок политики — в статьях о настройке Content-Security-Policy и разборе ошибок CSP.

Краулер по сайту

Отчёты CSP покрывают только посещаемые страницы. Старые статьи, которые никто не открывает годами, найдёт только обход. Минимальный вариант — пройти по sitemap и посчитать http-подресурсы на каждой странице:

# Сколько http-ссылок на подресурсы в HTML каждой страницы из sitemap
curl -sL https://example.com/sitemap.xml \
  | grep -oE '<loc>[^<]+' | cut -c6- \
  | while read -r u; do
      n=$(curl -sL --max-time 20 "$u" | grep -coiE '="http://[^"]+"')
      [ "$n" -gt 0 ] && printf '%5s  %s\n' "$n" "$u"
    done | sort -rn | head -50

# Все http-подресурсы одной страницы, с указанием атрибута
curl -sL https://example.com/blog/old-post/ \
  | grep -oiE '(src|href|action|poster|data-src|data-lazy-src)="http://[^"]+"' \
  | sort -u

# http внутри собранного CSS: url() и @import
curl -sL https://example.com/assets/app.css \
  | grep -oE '(url\(|@import[[:space:]]+)["'"'"']?http://[^)"'"'"']+' | sort -u

Метод не увидит ресурсы, которые подставляет JavaScript после загрузки, — для них нужен headless-браузер или отчёты CSP. Заодно краулер полезен для поиска битых ссылок: см. проверку битых ссылок.

grep по исходникам и по дампу базы

# 1. Исходники: шаблоны, вёрстка, собранные бандлы
grep -rnE 'http://[a-z0-9.-]+' \
  --include='*.html' --include='*.php' --include='*.twig' --include='*.tpl' \
  --include='*.js' --include='*.jsx' --include='*.ts' --include='*.tsx' \
  --include='*.css' --include='*.scss' --include='*.vue' --include='*.xml' \
  . | grep -vE '(xmlns|schema\.org|w3\.org|purl\.org|DOCTYPE|localhost|127\.0\.0\.1)'

# xmlns и schema.org отфильтрованы намеренно: это идентификаторы
# пространств имён, а не адреса загрузки. Браузер их не запрашивает.

# 2. Протокол-относительные ссылки - отдельный список
grep -rnE '(src|href)="//[a-z0-9.-]+' --include='*.php' --include='*.html' .

# 3. Дамп базы: какие хосты вообще встречаются и сколько раз
mysqldump --no-tablespaces --single-transaction -u user -p sitedb \
  | grep -oE 'http://[a-zA-Z0-9._-]+' | sort | uniq -c | sort -rn | head -30

# 4. То же для JSON-полей, где слэши экранированы (конструкторы страниц)
mysqldump --no-tablespaces --single-transaction -u user -p sitedb \
  | grep -oE 'http:\\/\\/[a-zA-Z0-9._-]+' | sort | uniq -c | sort -rn | head -30

Четвёртая команда важнее, чем кажется. Конструкторы страниц хранят настройки блоков в JSON, где / экранирован как \/. Обычный поиск по http:// такие адреса не находит, и после «полной» замены сайт продолжает тянуть http-картинки.

Четыре канала поиска mixed content: DevTools, отчёты CSP, краулер и grep по коду и базе
Ни один канал не покрывает всё: браузер видит открытые страницы, CSP — живой трафик, краулер — забытые разделы, grep — исходники.

Почему поиск только по шаблонам темы не находит всё

Самая частая ошибка — прогнать grep по каталогу темы, получить ноль совпадений и объявить проблему решённой. Ссылки живут далеко за пределами репозитория:

  • Контент в базе. Статьи и страницы, написанные до перехода на HTTPS, содержат абсолютные http://-адреса прямо в HTML тела записи.
  • Сериализованные настройки. Опции темы, конфигурация виджетов, настройки плагинов лежат в базе как PHP-сериализованные строки. Внутри — те же абсолютные адреса.
  • Конструкторы страниц. Данные блоков хранятся в JSON с экранированными слэшами, а иногда ещё и в base64.
  • Рекламные вставки и баннеры. Код вставляется через админку в поле «произвольный HTML» и в файлах проекта не существует.
  • Теги в менеджере тегов. Скрипт, добавленный в контейнер GTM или аналогичный сервис, вообще не хранится у вас. Он приходит в рантайме и в git его нет.
  • Сторонние виджеты второго уровня. Вы подключили HTTPS-скрипт чата, а он изнутри дотягивает http-ресурс. В вашем коде — чисто, в консоли — ошибка.
  • Загруженные пользователями материалы. Аватары, отзывы с картинками, вложения, импортированные когда-то с внешнего хостинга.
Где живут ссылкиЧем искатьЧем чинить
Шаблоны, вёрстка, компонентыgrep -rnE по исходникамПравка кода, релиз
Собранные CSS/JS-бандлыgrep по dist/ + curl по бандлуПравка исходников и пересборка
Тело записей и страниц в БДgrep по дампуИнструмент замены с поддержкой сериализации
Сериализованные опции и мета-поляgrep по дампу, поиск s:NN:"http://Только через unserialize/serialize
JSON конструкторов страницПоиск по http:\\/\\/Замена с учётом экранирования
Баннеры и произвольный HTML в админкеОбход разделов админки вручнуюРучная правка
Теги в менеджере теговNetwork → Initiator в браузереПравка тега в интерфейсе сервиса
Сторонние виджеты и их вложенные запросыОтчёты CSP, NetworkОбновление версии, обращение к вендору
Шаблоны писем и рассылокgrep по шаблонам + тестовое письмоПравка шаблона
RSS/XML-фиды, sitemapcurl + grep по <loc> и <url>Регенерация фида после фикса базы
JSON-LD, canonical, og:image, hreflangПросмотр заголовков и HTMLПравка шаблона мета-тегов
Настройки CMS: адрес сайта, домен CDNАдминка, конфиг-файлы, .envСмена значения и сброс кэша

Лечение по слоям

Порядок важен: сначала код, потом база, потом настройки. Если начать с базы, следующий деплой вернёт http-ссылки из шаблонов обратно.

Слой 1. Шаблоны и собранные ассеты

Правило простое: внутренние ресурсы — корневые относительные пути, внешние — явный https://.

<!-- Плохо: жёсткий http -->
<script src='http://cdn.example.net/lib.js'></script>
<link rel='stylesheet' href='http://example.com/assets/app.css'>

<!-- Плохо: протокол-относительный URL, антипаттерн 2026 года -->
<script src='//cdn.example.net/lib.js'></script>

<!-- Хорошо: свой ресурс - корневой относительный путь -->
<link rel='stylesheet' href='/assets/app.css'>

<!-- Хорошо: внешний ресурс - явный https -->
<script src='https://cdn.example.net/lib.js'
        integrity='sha384-...' crossorigin='anonymous'></script>

Корневой относительный путь для своих файлов лучше абсолютного: он переживает смену домена, работает на staging-копии и физически не может стать mixed content.

Протокол-относительные // — почему это антипаттерн сегодня

Синтаксис //cdn.example.net/lib.js придумали в эпоху, когда сайт жил одновременно на HTTP и HTTPS, и было важно не смешивать протоколы. Этой эпохи больше нет: HTTPS — единственный рабочий вариант. Что остаётся — только минусы:

  • Файл, открытый локально по file://, превращает такой URL в file://cdn.example.net/… — ресурс не грузится. Ломается любой офлайн-просмотр вёрстки и превью писем.
  • Адрес нельзя скопировать из кода и открыть, нельзя проверить curl-ом без ручной доработки.
  • Скрывает намерение: по коду не видно, поддерживает ли источник HTTPS вообще.
  • Если страницу когда-нибудь отдадут по HTTP (staging, локальный прокси, скачанная копия) — ресурс тихо уедет на HTTP и станет mixed content уже на другом сайте.

Пишите https:// явно. Единственный случай, где // ещё встречается оправданно, — сторонние сниппеты, которые вы не редактируете; их всё равно стоит заменить на актуальную версию.

Слой 2. Массовая замена в базе и ловушка сериализации

Здесь ломают сайты чаще всего. Наивная команда выглядит безобидно:

-- ТАК ДЕЛАТЬ НЕЛЬЗЯ для таблиц с сериализованными данными
UPDATE wp_posts
   SET post_content = REPLACE(post_content, 'http://example.com', 'https://example.com');

-- Почему: PHP-сериализация хранит длину строки в байтах.
--   было:  s:27:"http://example.com/logo.png"
--   стало: s:27:"https://example.com/logo.png"   ← длина уже 28
-- unserialize() возвращает false, настройки виджета/темы молча обнуляются.
-- В JSON-полях та же беда с другой стороны: там адрес записан
-- как http:\/\/example.com и под условие REPLACE вообще не попадает.

Правильный путь — инструмент, который понимает формат хранения: разбирает значение, заменяет строку, пересчитывает длину и упаковывает обратно.

# WordPress: WP-CLI. Сначала ВСЕГДА сухой прогон.
wp search-replace 'http://example.com' 'https://example.com' \
  --all-tables-with-prefix --precise --recount --skip-columns=guid \
  --report-changed-only --dry-run

# Убедились в списке таблиц и количестве замен - повторяем без --dry-run.
wp search-replace 'http://example.com' 'https://example.com' \
  --all-tables-with-prefix --precise --recount --skip-columns=guid

# Отдельным проходом - экранированный вариант для конструкторов страниц.
wp search-replace 'http:\/\/example.com' 'https:\/\/example.com' \
  --all-tables-with-prefix --precise --dry-run

# Перед любым проходом - дамп. Откат должен занимать минуты, а не часы.
wp db export backup-$(date +%F-%H%M).sql

Пояснения к флагам: --precise заставляет выполнять замену на стороне PHP, а не SQL, — только так корректно обрабатывается сериализация. --skip-columns=guid обязателен: guid — это постоянный идентификатор записи для лент, менять его нельзя, иначе подписчики получат все старые посты заново. --all-tables-with-prefix захватывает таблицы плагинов, которые не входят в стандартный набор.

Для других систем принцип тот же, меняются места:

  • 1С-Битрикс: тексты в b_iblock_element (DETAIL_TEXT, PREVIEW_TEXT), значения свойств в b_iblock_element_property, сериализованные настройки модулей в b_option. Последнюю таблицу обычным REPLACE трогать нельзя.
  • Drupal: тело материалов в node__body, конфигурация в таблице config — сериализованная.
  • Самописные проекты: ищите поля с типом TEXT/LONGTEXT, куда сохраняется HTML редактора, и любые колонки с настройками в JSON.

Правило перед массовой заменой: снять дамп, выполнить сухой прогон, зафиксировать ожидаемое число замен, применить, проверить пять случайных страниц из разных типов контента. Пропуск любого шага рано или поздно стоит вечера восстановления.

Слой 3. Настройки, кэш и CDN

  • Адрес сайта в настройках CMS и в конфигурации приложения (.env, config/*.php) — заменить на https://.
  • Базовый URL медиа и домен CDN — проверить отдельно: он часто хранится в своей настройке и не попадает под общую замену.
  • Полный сброс кэша страниц, объектного кэша и кэша CDN. Иначе исправленный HTML не увидят ни пользователи, ни ваш собственный краулер.
  • Регенерация sitemap и RSS-фидов после фикса базы.
Слои исправления mixed content: шаблоны, база данных с сериализованными полями, настройки и кэш
Порядок обязателен: начнёте с базы — следующий деплой вернёт http-ссылки из шаблонов.

Директивы CSP: upgrade-insecure-requests и устаревшая block-all-mixed-content

upgrade-insecure-requests — временная мера

Директива заставляет браузер переписать все http://-запросы страницы в https:// до отправки. Ставится заголовком:

# nginx: временная мера на период чистки кода.
# Держать не дольше одного релизного цикла.
add_header Content-Security-Policy "upgrade-insecure-requests" always;

# Проверка, что заголовок реально отдаётся:
curl -sI https://example.com/ | grep -i 'content-security-policy'

# Альтернатива через meta - работает, но хуже:
# заголовок применяется ко всем ответам, meta - только к этому документу
# и только к запросам, начатым ПОСЛЕ разбора тега. Ставить первым в head.
# <meta http-equiv="Content-Security-Policy" content="upgrade-insecure-requests">

Что директива делает: апгрейдит подресурсы, вложенные навигации (iframe), отправку форм и, в современных браузерах, ws:// до wss://.

Чего она НЕ делает — и это причина не считать её решением:

  • Не создаёт HTTPS там, где его нет. Если у стороннего домена нет валидного сертификата, запрос после апгрейда просто провалится с ошибкой TLS. Ресурс так же отсутствует, но теперь диагностировать сложнее: в консоли нет слова «Mixed Content», и связь с исходной http-ссылкой не видна.
  • Не чинит навигацию. Переход пользователя по внешней http://-ссылке остаётся незащищённым.
  • Скрывает проблему. Консоль чистая, отчётов нет, http-ссылки продолжают копиться в контенте. Через год у вас тысячи битых адресов и ни одного сигнала.
  • Не работает в Report-Only. В отчётной политике директива игнорируется по спецификации.
  • Не защищает от ошибки редактора. Автор вставит http-картинку — она молча апгрейдится, а если источник без HTTPS, читатель увидит пустоту.

upgrade-insecure-requests — это гипс, а не лечение. Он держит сайт работоспособным, пока вы чините реальные ссылки. Включили — сразу ставьте задачу на чистку и Report-Only-политику, иначе гипс останется навсегда.

block-all-mixed-content устарела

Эту директиву до сих пор советуют в старых подборках заголовков. Не используйте её:

  • Она объявлена устаревшей в спецификации Mixed Content и не входит в актуальный набор директив CSP.
  • Современные браузеры её игнорируют или трактуют как no-op — поведение, которое она задавала, уже включено по умолчанию: активный контент блокируется всегда, пассивный апгрейдится.
  • В связке с upgrade-insecure-requests она бессмысленна: апгрейд отрабатывает раньше, блокировать становится нечего.
  • Её присутствие в заголовке создаёт ложное ощущение защиты при аудите: строка есть, эффекта нет.

Если директива уже прописана — просто удалите её и проверьте итоговый набор заголовков через анализатор HTTP-заголовков. Общий разбор правильного набора — в статье о заголовках безопасности.

Если сторонний ресурс не отдаёт HTTPS

Ситуация редкая, но тупиковая: виджет, счётчик или витрина партнёра доступны только по HTTP. Со стороны сайта разрешить загрузку нельзя — исключений в браузере для этого не предусмотрено. Вариантов ровно четыре.

  1. Потребовать HTTPS у поставщика. Самый правильный путь. Бесплатный сертификат выпускается за минуты, и отсутствие HTTPS в 2026 году — это не техническое ограничение, а недосмотр. Проверить, что у них появилось, можно через проверку SSL-сертификата.
  2. Проксировать через свой домен. Для статики (изображения, JSON-фиды, простые скрипты) вы поднимаете у себя эндпоинт вроде /proxy/partner/…, который ходит к источнику по HTTP на серверной стороне и отдаёт результат по HTTPS. Работает, но платите вы: трафиком, задержкой, кэшированием и, главное, ответственностью — с точки зрения браузера этот код теперь ваш, со всеми правами в вашем origin. Обязательно ограничьте список разрешённых путей и хостов, иначе получите SSRF-прокси. Для интерактивных iframe-приложений способ обычно не работает: ломаются относительные адреса, cookies и правила фреймирования.
  3. Заменить сервис. У любого счётчика, чата, карты и калькулятора есть современный аналог с HTTPS. Часто это дешевле прокси.
  4. Убрать. Если ресурс не приносит измеримой пользы — удалить и не тратить время. Незаменимых виджетов не бывает.

Чего делать нельзя: советовать посетителям включить «небезопасный контент» в настройках браузера. Это работает только у того, кто включил, отключается при обновлении, и вы своими руками снимаете защиту с чужой машины.

Частные случаи: iframe, шрифты, favicon, sitemap и canonical

iframe

Самый заметный отказ: вместо карты, видео или формы оплаты — пустой прямоугольник, иногда даже без границы. Фолбэка нет: браузер не покажет ни alt, ни заглушку. Проверьте embed-код — многие сервисы годами раздавали сниппеты с http://, и он до сих пор лежит в старых статьях. Обновление обычно сводится к замене одной буквы в адресе.

Шрифты

Веб-шрифт относится к активному контенту и блокируется. Симптом смазанный: текст читается, но нарисован системным шрифтом, метрики другие, вёрстка едет, а в консоли ошибка теряется среди прочих. Правильный ответ почти всегда — самохостинг: шрифты кладутся рядом с ассетами, подключаются относительным путём, попутно исчезает сторонний домен из критического пути загрузки.

favicon

Иконка — пассивный контент, апгрейдится молча. Влияние на пользователя минимальное, но абсолютный http://-адрес в <link rel='icon'> регулярно всплывает в отчётах и в результатах сканеров, создавая шум. Лечится за секунду: относительный путь /favicon.ico.

sitemap, canonical, og:image

Формально это не mixed content: canonical и адреса в sitemap не загружаются как подресурсы страницы. Но проблема реальная и относится к поисковой видимости:

  • <link rel='canonical' href='http://…'> указывает поисковику, что канонической является http-версия. Дальше — лишний редирект в цепочке и размывание сигналов между двумя адресами.
  • sitemap со списком http-адресов заставляет робота обходить редиректы вместо страниц и тратить краулинговый бюджет.
  • og:image с http-адресом при живом HTTPS-сайте — типичная причина, по которой превью в мессенджерах и соцсетях не показывается.
  • hreflang с http-адресами разрывает связку языковых версий.

Проверяются эти поля отдельно от mixed content — вместе с остальной технической частью. Влияние протокола на выдачу разобрано в статье о влиянии HTTPS на SEO, а полный порядок перевода сайта — в руководстве по миграции с HTTP на HTTPS.

Связь с HSTS: после preload ошибки становятся жёстче

Заголовок HSTS (RFC 6797) говорит браузеру: к этому хосту всегда ходи по HTTPS. Апгрейд происходит внутри браузера, до отправки запроса и до проверки на mixed content, поэтому HSTS фактически устраняет mixed content для собственных доменов.

Границы этого эффекта надо понимать точно:

  • Работает только для хостов, про которые браузер уже знает — то есть после первого визита по HTTPS или после включения в preload-список.
  • includeSubDomains распространяет правило на поддомены, включая те, о которых вы забыли: старый img.example.com без сертификата станет недоступен целиком.
  • На сторонние домены не действует вообще. Http-ссылка на чужой CDN останется mixed content.

И главное последствие: после включения preload ошибки перестают быть мягкими. Браузер не позволит перейти по HTTP даже вручную, а страницу с невалидным сертификатом покажет без кнопки «всё равно перейти». Просроченный сертификат на поддомене превращается из неприятности в полную недоступность. Поэтому порядок такой: сначала чистый mixed content и проверенные сертификаты на всех поддоменах, потом HSTS с коротким max-age, потом увеличение срока и только затем preload. Детали и порядок выката — в руководстве по HSTS.

Preload-список практически необратим: удаление занимает месяцы и доезжает до пользователей только со сменой версии браузера. Не включайте preload, пока не уверены, что каждый поддомен, включая технические, умеет HTTPS.

Последовательность: чистка mixed content, затем HSTS с коротким сроком, затем preload
HSTS убирает mixed content для своих доменов, но делает любую ошибку сертификата жёсткой. Порядок выката имеет значение.

Проверка после исправления и мониторинг регресса

Разовая чистка не держится. Новая статья с http-картинкой, обновление плагина, рекламный тег, добавленный маркетологом в менеджер тегов, — и через месяц всё возвращается. Нужны два контура: приёмка и постоянный контроль.

Приёмка после фикса

  1. Сбросить кэш приложения и CDN. Без этого вы проверяете старый HTML.
  2. Пройти не только главную: карточку товара, старую статью с картинками, корзину, форму заявки, личный кабинет под авторизацией.
  3. DevTools → Console и Network: ноль записей Mixed Content и ноль статусов (blocked:mixed-content).
  4. Прогнать краулер по sitemap командой из раздела выше — ожидаемый результат — пустой вывод.
  5. Убедиться, что Content-Security-Policy-Report-Only отдаётся на всех страницах, а не только на главной.
  6. Прожить неделю без новых нарушений в отчётах. Только после этого снимать upgrade-insecure-requests, если он включался.

Защита от регресса

  • Report-Only остаётся навсегда. Отчётная политика ничего не ломает и служит постоянным датчиком. Всплеск нарушений после релиза — сигнал.
  • Проверка в CI. Простой шаг конвейера: grep по сборке на "http:// и ="//, красный билд при совпадении вне белого списка.
  • Фильтр на стороне редактора. В большинстве CMS можно повесить обработчик сохранения контента, который переписывает http:// на https:// для известных доменов и предупреждает автора о внешнем http-адресе.
  • Регламент для сторонних тегов. Любой скрипт, добавляемый в менеджер тегов, проверяется на HTTPS до публикации. Это единственный слой, который не ловится ни grep-ом, ни ревью кода.
  • Периодический обход. Раз в месяц — краулер по sitemap плюс контроль набора заголовков, чтобы вместе с mixed content не уехал и остальной набор безопасности. Автоматизировать удобно через мониторинг сайта.

Как проверить

  • Проверка mixed content — находит http-подресурсы на странице и разделяет их на активные и пассивные.
  • Проверка SSL/TLS — сертификат, цепочка и срок действия; сюда же приходят после того, как апгрейд пассивного контента не удался.
  • Анализ Content-Security-Policy — разбор политики, поиск устаревших директив и проверка того, что отчётный эндпоинт настроен.
  • HTTP-заголовки — фактический набор заголовков ответа, включая CSP и HSTS.
  • Сканер безопасности — сводная оценка страницы: заголовки, mixed content, конфигурация TLS.
  • Проверка битых ссылок — обход сайта, попутно показывает ссылки на http-адреса и редиректы.

Частые вопросы

Картинка по HTTP действительно опасна?

Напрямую она не выполняет код, поэтому её и относят к пассивному контенту. Косвенно риск есть: атакующий на пути трафика подменяет изображение (логотип платёжной системы, скриншот инструкции, QR-код) и использует запрос для трекинга. Плюс современный браузер всё равно попытается апгрейдить её до HTTPS, и при неудаче картинка исчезнет — то есть это ещё и вопрос работоспособности, а не только безопасности.

Можно ли отключить проверку mixed content?

Со стороны сайта — нет, такой возможности не предусмотрено. В браузере пользователь может разрешить небезопасный контент для конкретного сайта в настройках, но это работает только на его машине, часто сбрасывается при обновлении и не решает задачу. Просить об этом посетителей нельзя: вы предлагаете им снять защиту в обмен на ваше удобство.

upgrade-insecure-requests работает для fetch и WebSocket?

Да, директива применяется ко всем запросам, инициированным страницей, — включая fetch, XHR, отправку форм и вложенные iframe; современные браузеры апгрейдят и ws:// до wss://. Но апгрейд бесполезен, если на целевом хосте нет HTTPS: запрос провалится на этапе TLS. И на переход пользователя по внешней ссылке директива не влияет — это зона HSTS.

Я сделал UPDATE с REPLACE, и часть настроек пропала. Что случилось?

Вы попали в сериализацию. PHP хранит строки как s:ДЛИНА:"значение", длина указана в байтах. Замена http:// на https:// удлиняет строку на один байт, а префикс остаётся прежним — при чтении unserialize() возвращает false, и приложение подставляет значения по умолчанию. Восстанавливается из дампа; повторять замену нужно инструментом, который пересчитывает длины (для WordPress — wp search-replace --precise).

Консоль чистая, а сканер всё равно находит mixed content. Почему?

Три типовые причины. Первая: у вас включён upgrade-insecure-requests — браузер апгрейдит ссылки молча, а сканер смотрит исходный HTML и видит http://. Вторая: проблемный ресурс подгружается на другой странице или в другом сценарии — например, только авторизованному пользователю. Третья: вы смотрите закэшированную версию, а сканер запросил свежую.

Хватит ли одного HSTS вместо чистки ссылок?

Только для ссылок на ваши собственные домены и только после того, как браузер узнал политику. На сторонние хосты HSTS не действует вообще, а первый визит нового пользователя до попадания хоста в preload-список остаётся незащищённым. Это полезный дополнительный слой, но не замена исправлению адресов.

Сколько времени занимает полная чистка?

Зависит не от количества ссылок, а от числа слоёв. Лендинг на статике — час. Магазин с десятилетней историей контента, конструктором страниц, десятком плагинов и тегами в менеджере тегов — от нескольких дней, причём основное время уходит не на замену, а на поиск оставшихся источников и на неделю наблюдения за отчётами.

Чеклист

  • Разделили нарушения на активные и пассивные — приоритет активным, они ломают функциональность.
  • Прошли все четыре канала поиска: DevTools, отчёты CSP с живого трафика, краулер по sitemap, grep по коду и дампу базы.
  • Искали в дампе оба варианта записи: http:// и экранированный http:\/\/.
  • Проверили сценарии под авторизацией, а не только главную в анонимном режиме.
  • Починили в порядке: шаблоны и ассеты → база → настройки и кэш.
  • Массовую замену в базе делали инструментом с поддержкой сериализации, после дампа и сухого прогона.
  • Убрали протокол-относительные //, свои ресурсы перевели на корневые относительные пути.
  • Удалили block-all-mixed-content, если она была в заголовках.
  • upgrade-insecure-requests включён как временная мера и снят после чистки.
  • Для сторонних ресурсов без HTTPS выбран осознанный вариант: требование к вендору, прокси с белым списком, замена или удаление.
  • Проверили canonical, sitemap, og:image и hreflang на http-адреса.
  • Сбросили кэш приложения и CDN, регенерировали sitemap и фиды.
  • Content-Security-Policy-Report-Only оставлен постоянно как датчик регресса.
  • Добавлена проверка в CI и регламент на сторонние теги.
  • HSTS и preload включаются только после того, как mixed content вычищен, а сертификаты на всех поддоменах валидны.

Первоисточники: W3C Mixed Content, W3C Content Security Policy Level 3, W3C Upgrade Insecure Requests, RFC 6797 (HSTS).

Проверьте ваш сайт прямо сейчас

Проверить SSL своего сайта →
Другие статьи: SSL/TLS
SSL/TLS
Сертификат Минцифры: как установить и вернуть доступ к банкам
31.08.2026 · 3 394 просм.
SSL/TLS
SSL Handshake Failed: причины ошибки и пошаговая диагностика
15.04.2026 · 2 134 просм.
SSL/TLS
ERR_CERT_AUTHORITY_INVALID: причины и как исправить
13.07.2026 · 1 100 просм.
SSL/TLS
Как проверить SSL-сертификат сайта — 3 способа за 2 минуты
12.04.2026 · 798 просм.