Безопасное встраивание любых ссылок
Что Blok проверяет, когда встраивает ссылку, и какие заголовки должно слать ваше приложение.
Что проверяет Blok
Blok встраивает только ссылки, которым доверяет, и даёт каждому фрейму как можно меньше прав.
- Провайдеры из реестра, например YouTube и Figma, получают sandbox провайдера и
referrerpolicy="strict-origin-when-cross-origin". - Общему эмбеду нужен
linkPaste.allowGenericEmbedили подходящая запись вallowedEmbedOrigins.- Его источник должен быть абсолютным URL
https://. Формы вродеhttps:/xилиhttps:x.comотклоняются, потому что браузер прочитал бы их относительно вашей страницы. - Blok сохраняет каноническую форму, поэтому
https://Example.comсохраняется какhttps://example.com/. - Он получает
sandbox="allow-scripts allow-forms allow-popups", без спискаallow, без referrer и сcredentialless(только Chrome). - По умолчанию он загружается только после клика читателя.
- Его источник должен быть абсолютным URL
- Общий эмбед никогда не встраивает ваш собственный сайт. Хост страницы,
hostAliases, их поддомены и родительские домены получают карточку ссылки. blockedEmbedOriginsсильнее обеих настроек разрешения.- Headless-рендер встраивает только эмбеды из реестра. После
transformUrlsrc фрейма должен остаться абсолютным https, иначе рендер выводит ссылку.
Инструмент файла встраивает только PDF по blob:, с новым типом application/pdf. Удалённый PDF по http(s) вместо этого получает ссылку «Открыть в новой вкладке».
Без загрузчика каждый blob: URL, который создаёт Blok, получает безопасный тип. Любой тип вне короткого списка безопасных становится text/plain, в том числе HTML, XML, zip, файлы Office и файлы без типа. Изображение SVG вообще не становится blob. Настоящий тип файла читайте из mimeType блока.
Без загрузчика изображение SVG сохраняется как URL data:image/svg+xml. Открытый в отдельной вкладке blob с SVG выполняет свой скрипт от имени вашего сайта, а data: URL нет.
- Blok показывает URL
data:image/svg+xmlтолько как источник изображения: в<img>в редакторе и в headless-рендере. - Ссылки, скачивание, фреймы и CSS по-прежнему его отклоняют.
- SVG больше 256 КБ вместо этого становится blob с типом
text/plain, поэтому изображение не показывается. Для больших SVG задайте загрузчик.
Страница внутри фрейма эмбеда не может сама забрать фокус клавиатуры.
- Если фокус уходит во фрейм без клика, касания или Tab от читателя, Blok возвращает фокус и курсор на место.
- После касания защита отключается, поэтому касание поля внутри эмбеда по-прежнему работает.
- В Firefox фрейм, который забирает фокус, пока страница ещё загружается, всё ещё может получить несколько нажатий клавиш до клика читателя.
Сохранённый размер фрейма остаётся в границах. Его высота лежит в пределах от 0,1 до 4 ширин. Ширина или высота, которая не является положительным числом, заменяется размером по умолчанию 580×320.
blocksToHtml и blocksToMarkdown доверяют сохранённому эмбеду только в меру его URL.
- Эмбед, экспортированный как ссылка, берёт сохранённое имя сервиса как текст, только если исходный URL подходит этому провайдеру из реестра. Иначе текстом становится URL.
- В
blocksToHtmlплеер сохраняет более свободный sandbox плеера, только еслиtransformUrlоставляет origin провайдера. Иначе он получает sandbox общего эмбеда.
Всё остальное в этом руководстве делает ваше приложение. Blok это библиотека, поэтому он не может слать заголовки для ваших страниц.
Ограничьте фреймы через CSP
Шлите Content-Security-Policy с директивой frame-src. Браузер проверяет её для каждого фрейма на странице, поэтому она работает, даже если в проверке Blok есть ошибка.
- Чтобы пускать любой https-сайт, как
allowGenericEmbed, укажитеframe-src https:. - Чтобы пускать только некоторые сайты, перечислите их. Сверьте список с
allowedEmbedOriginsи провайдерами реестра, которыми пользуетесь. - Встроенный просмотр PDF встраивает
blob:URL и читает его черезfetch. Строгой политике может понадобитьсяblob:вframe-srcиconnect-src. Мы это не измеряли.
Не пускайте свои страницы во фреймы
Шлите frame-ancestors 'self' или frame-ancestors 'none' на своих страницах, например настроек и доступа. Тогда чужой сайт не сможет встроить их и обманом заставить читателя кликнуть.
В общем эмбеде Blok отказывается встраивать ваш хост, его псевдонимы, их поддомены и родительские домены. Соседний поддомен он не ловит, например cdn.example.com рядом с app.example.com. Для этого нужен список публичных суффиксов. Перечислите такие хосты в hostAliases или blockedEmbedOrigins.
Списки хостов и проверка своего сайта ловят ещё две редкие формы адреса.
- Адрес IPv4, записанный как IPv6, например
[::ffff:10.0.0.1], совпадает с записями IPv4 вallowedEmbedOrigins,blockedEmbedOriginsиhostAliases. Проверка своего сайта тоже его узнаёт. - Если редактор работает в документе без имени хоста, например
blob:,about:srcdocилиabout:blank, Blok берёт хост из origin документа.
Разорвите связь с всплывающими окнами через COOP
Встроенная страница может открыть всплывающее окно. allow-popups-to-escape-sandbox сохраняют только видео- и аудиоплееры из реестра, например YouTube, Vimeo и Spotify. Кнопке YouTube «Watch on YouTube» нужна вкладка без sandbox.
Эмбеды кода, документов и соцсетей, например CodePen, Figma, Google Docs и посты, его не получают. Общий эмбед тоже. Их окна сохраняют sandbox фрейма, поэтому некоторые страницы провайдеров могут в них не загрузиться.
Окно, вышедшее из sandbox, это обычная страница встроенного сайта. В Firefox оно смогло увести вкладку редактора на другой адрес через window.opener.top. Chrome и WebKit это запретили.
Шлите Cross-Origin-Opener-Policy: same-origin на странице с редактором. Он по-прежнему нужен для плееров, чьи окна выходят из sandbox. Тогда у окна не было window.opener, и вкладка оставалась на месте во всех трёх браузерах. same-origin-allow-popups не помог: в Firefox окно всё равно увело вкладку.
same-origin может сломать и окна, которые ваше приложение открывает на других сайтах и с которыми говорит через window.opener, например окно входа. Проверьте такие сценарии перед включением.
Ограничьте опасные возможности
Общий эмбед не получает списка allow, поэтому у него нет камеры, микрофона, геолокации и платежей. Эмбед из реестра получает только encrypted-media, fullscreen и picture-in-picture.
Чтобы ограничить всю страницу, шлите заголовок Permissions-Policy. Каждой возможности, которой ваше приложение не пользуется, дайте пустой список:
camera=(), microphone=(), geolocation=(), payment=(), usb=()
По данным о совместимости браузеров этот заголовок соблюдает только Chrome. Firefox и Safari его не знают, поэтому главным рычагом остаётся атрибут allow. Мы это не измеряли.
Отдавайте файлы пользователей с другого сайта
Загруженный HTML- или SVG-файл выполняет скрипт от имени сайта, который его отдаёт. На хосте вашего приложения он ведёт себя как одна из ваших страниц. Отдавайте загрузки с отдельного хоста, например с собственного адреса бакета S3.
Отдельный сайт надёжнее поддомена вашего приложения, потому что для кук SameSite браузеры считают поддомены одним сайтом. Если берёте поддомен, перечислите его в hostAliases или blockedEmbedOrigins, чтобы он никогда не получил общий эмбед.
У blob: URL тоже origin вашей страницы. Blok меняет тип у blob, которые создаёт сам, но blob: URL из вашего загрузчика сохраняет тот тип, который вы дали. Оставляйте только безопасные типы: изображения, видео mp4 и webm, обычное аудио, PDF и простой текст. Всё остальное, включая zip, Office и octet-stream, переводите в text/plain до вызова URL.createObjectURL.
SVG в blob: URL тоже не оставляйте, даже для <img>. Читатель может открыть изображение в отдельной вкладке, и тогда SVG выполняется от имени вашего сайта. Отдавайте SVG с отдельного сайта или берите URL data:image/svg+xml, как делает Blok.
Проверьте ссылку до встраивания
Многие сайты запрещают встраивать себя, и читатель видит пустой фрейм. Ваш бэкенд может узнать это заранее. Передайте linkPaste.checkEmbed или задайте server, и Blok вызовет GET /frame-check сервера.
blockedилиinsecureвставляет вместо эмбеда закладку с коротким уведомлением.allowed,unknown, ошибка или отсутствие ответа за 3 секунды вставляет эмбед.
Вердикт это подсказка для интерфейса, а не проверка безопасности. Он не отменяет ни одного другого правила. Браузер всё равно соблюдает X-Frame-Options и frame-ancestors встроенного сайта.
Что мы измерили
Результаты про окна и PDF выше получены 3 ноября 2026 года на локальном тестовом сервере. Браузеры: Chrome 156 и собственные сборки Playwright для Firefox 146 и WebKit 26. Эти две сборки изменены, поэтому обычные Firefox и Safari могут вести себя иначе.
- Фрейм с sandbox без
allow-popups-to-escape-sandboxоткрывал окна, которые сохраняли sandbox. Они не смогли увести вкладку редактора ни в одном браузере. - С этим токеном окно в Firefox увело вкладку редактора.
same-origin-allow-popupsэто не остановил. Сsame-originу окна не былоwindow.openerво всех трёх браузерах. - HTML-страница во фрейме без sandbox выполнила свой скрипт и смогла увести вкладку редактора. Адрес на
.pdfничего не доказывает, решаетContent-Typeсервера. - Blob с типом
text/html, открытый в новой вкладке, работал от имени создавшей его страницы, даже сrel="noopener". - Blob с типом
image/svg+xml, открытый в новой вкладке, выполнил свой скрипт от имени создавшей его страницы и прочитал её хранилище. Тот же SVG как data: URL показался в<img>во всех трёх браузерах. Во вкладке он не открылся или работал с originnullи без хранилища. - Chrome блокирует свой просмотрщик PDF в любом фрейме с sandbox, включая
sandbox="". Поэтому удалённые PDF открываются в новой вкладке.