Перейти к содержимому
ФреймворкJavaScript

Безопасное встраивание любых ссылок

Что Blok проверяет, когда встраивает ссылку, и какие заголовки должно слать ваше приложение.

Обновлено 5 нояб. 2026 г.Редактировать на GitHub

Что проверяет 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).
    • По умолчанию он загружается только после клика читателя.
  • Общий эмбед никогда не встраивает ваш собственный сайт. Хост страницы, hostAliases, их поддомены и родительские домены получают карточку ссылки.
  • blockedEmbedOrigins сильнее обеих настроек разрешения.
  • Headless-рендер встраивает только эмбеды из реестра. После transformUrl src фрейма должен остаться абсолютным 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> во всех трёх браузерах. Во вкладке он не открылся или работал с origin null и без хранилища.
  • Chrome блокирует свой просмотрщик PDF в любом фрейме с sandbox, включая sandbox="". Поэтому удалённые PDF открываются в новой вкладке.