Коротко. 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 на целевом домене |

Что ломается на практике: симптом, причина, проверка
Владелец сайта редко приходит со словами «у меня mixed content». Он приходит с симптомом. Таблица ниже — карта соответствий.
| Симптом | Что произошло | Как проверить | Фикс |
|---|---|---|---|
| Страница «разъехалась», голый HTML без оформления | Заблокирован основной CSS-бандл | Console: insecure stylesheet | HTTPS-URL стиля в шаблоне |
| Не работают меню, слайдеры, модалки | Заблокирован jQuery или бандл скриптов | Console: insecure script + $ is not defined | HTTPS-CDN или локальная копия |
| Форма отправляется «в никуда», нет ответа | Заблокирован fetch к http-API | Network: запрос в статусе (blocked:mixed-content) | HTTPS на эндпоинте |
| Пропали счётчики, метрика не пишет визиты | Заблокирован тег аналитики | Отсутствие запроса в Network | Обновить сниппет счётчика |
| Пустой прямоугольник вместо карты или видео | Заблокирован iframe | Console: insecure frame | HTTPS-эмбед у провайдера |
| Текст «прыгает» и рисуется системным шрифтом | Заблокирован веб-шрифт | Console: insecure font | Самохостинг шрифтов |
| Часть картинок в старых статьях исчезла | Пассивный апгрейд не удался: у хоста нет HTTPS | Network: ошибка сертификата на домене-источнике | Перезалить изображения к себе |
| Замок серый, но визуально всё цело | Единственная http-картинка или favicon | Проверка mixed content | Точечная замена ссылки |
| Ломается только у части пользователей | Ресурс подгружается на редком сценарии — корзина, ЛК, поиск | CSP Report-Only на живом трафике | По адресам из отчётов |
Отдельная ловушка: у авторизованного пользователя набор подключаемых скриптов часто отличается от гостевого. Проверять только главную в анонимном режиме — гарантированный способ пропустить половину проблем.
Как найти все вхождения mixed content
Ни один метод не находит всё. Рабочая схема — четыре независимых прохода: браузер, отчёты CSP с живого трафика, краулер и grep по коду и дампу базы.
DevTools: Console, Issues и Network
Быстрая проверка одной страницы:
- Console — фильтр по строке
Mixed Content. Показывает и заблокированное, и апгрейженное. - Issues (отдельная вкладка) — группирует нарушения по типу и даёт ссылку на строку в исходнике.
- Network — снимите галку кэша, перезагрузите с
Ctrl+Shift+R. Заблокированные запросы видны в статусе как(blocked:mixed-content). В колонке Initiator видно, кто именно инициировал запрос — это единственный надёжный способ поймать ресурс, вставленный сторонним скриптом в рантайме. - 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-картинки.

Почему поиск только по шаблонам темы не находит всё
Самая частая ошибка — прогнать 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-фиды, sitemap | curl + 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-фидов после фикса базы.

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

Проверка после исправления и мониторинг регресса
Разовая чистка не держится. Новая статья с http-картинкой, обновление плагина, рекламный тег, добавленный маркетологом в менеджер тегов, — и через месяц всё возвращается. Нужны два контура: приёмка и постоянный контроль.
Приёмка после фикса
- Сбросить кэш приложения и CDN. Без этого вы проверяете старый HTML.
- Пройти не только главную: карточку товара, старую статью с картинками, корзину, форму заявки, личный кабинет под авторизацией.
- DevTools → Console и Network: ноль записей
Mixed Contentи ноль статусов(blocked:mixed-content). - Прогнать краулер по sitemap командой из раздела выше — ожидаемый результат — пустой вывод.
- Убедиться, что
Content-Security-Policy-Report-Onlyотдаётся на всех страницах, а не только на главной. - Прожить неделю без новых нарушений в отчётах. Только после этого снимать
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).