---
title: "Blok Embed Safety — Embedding Any URL Safely"
description: "What Blok enforces when it frames a link, and the CSP, COOP and Permissions-Policy headers your app must send."
source: https://blokeditor.com/docs/embed-safety/
lastmod: 2026-11-05
---

Framework JavaScript

Extending & system Embedding any URL safely

# Embedding any URL safely

What Blok enforces when it frames a link, and the headers your app must send.

Last updated Nov 5, 2026 [Edit this page on GitHub](https://github.com/JackUait/blok/blob/main/docs/src/components/api/EmbedSafetyContent.tsx)

## What Blok enforces

Blok frames only links it trusts, and gives each frame as little power as it can.

- Registry providers, such as YouTube and Figma, get the provider sandbox and `referrerpolicy="strict-origin-when-cross-origin"`.
- A generic embed needs `linkPaste.allowGenericEmbed` or a matching `allowedEmbedOrigins` entry.
  - Its source must be an absolute `https://` URL. Forms like `https:/x` or `https:x.com` are refused, because the browser would read them against your own page.
  - Blok saves the canonical form, so `https://Example.com` is saved as `https://example.com/`.
  - It gets `sandbox="allow-scripts allow-forms allow-popups"`, no `allow` list, no referrer, and `credentialless` (Chrome only).
  - By default it loads only after the reader clicks.
- A generic embed never frames your own site. The page host, `hostAliases`, and their subdomains and parent domains get a link card.
- `blockedEmbedOrigins` beats both allow settings.
- The headless view frames registry embeds only. After `transformUrl`, the frame src must still be absolute https, or the view prints a link.

The file tool frames only `blob:` PDFs, re-typed as `application/pdf`. A remote http(s) PDF gets an "Open in new tab" link instead.

With no uploader, every `blob:` URL Blok makes gets a safe type. Any type outside a short safe list becomes `text/plain`, including HTML, XML, zip, Office files and untyped files. An SVG image is not a blob at all. Read a file's real type from the block's `mimeType`.

With no uploader, an SVG image is stored as a `data:image/svg+xml` URL. Opened in its own tab, an SVG blob runs its script as your site, and a data: URL does not.

- Blok shows a `data:image/svg+xml` URL only as an image source: the `<img>` in the editor and in the headless view.
- Links, downloads, frames and CSS still refuse it.
- An SVG over 256 KB becomes a `text/plain` blob instead, so the image does not show. Set an uploader for larger SVG files.

A page inside an embed frame cannot take keyboard focus on its own.

- If focus moves into a frame without a click, tap or Tab from the reader, Blok puts focus and the caret back.
- After a touch press the guard stands down, so a tap into a field inside an embed still works.
- In Firefox, a frame that grabs focus while the page is still loading can still get some keys before the reader clicks.

A frame's saved size stays in bounds. Its height stays between 0.1 and 4 times its width. A width or height that is not a positive number falls back to the default 580×320.

`blocksToHtml` and `blocksToMarkdown` trust a saved embed only as far as its URL.

- An embed exported as a link uses the stored service name as its text only when the source URL matches that registry provider. Otherwise the URL is the text.
- In `blocksToHtml`, a player keeps the looser player sandbox only if `transformUrl` keeps the provider's origin. Otherwise it gets the generic sandbox.

The rest of this guide is up to your app. Blok is a library, so it cannot send headers for your pages.

## Limit frames with CSP

Send a `Content-Security-Policy` with a `frame-src` directive. The browser checks it for every frame on your page, so it still holds if a check in Blok has a bug.

- To allow any https site, as `allowGenericEmbed` does, use `frame-src https:`.
- To allow only some sites, list them. Match the list to `allowedEmbedOrigins` and the registry providers you use.
- The inline PDF preview frames a `blob:` URL and reads it with `fetch`. A strict policy may need `blob:` in `frame-src` and `connect-src`. We have not measured this.

## Keep your own pages out of frames

Send `frame-ancestors 'self'` or `frame-ancestors 'none'` on your own pages, such as settings and sharing. Then another site cannot frame them to trick a signed-in reader into a click.

In a generic embed, Blok refuses to frame your own host, its aliases, and their subdomains and parent domains. It does not catch a sibling subdomain, such as `cdn.example.com` next to `app.example.com`. That would need the public suffix list. List such hosts in `hostAliases` or `blockedEmbedOrigins`.

The host lists and the own-site check also catch two rarer address forms.

- An IPv4 address written as IPv6, such as `[::ffff:10.0.0.1]`, matches IPv4 entries in `allowedEmbedOrigins`, `blockedEmbedOrigins` and `hostAliases`. The own-site check matches it too.
- When the editor runs in a document with no hostname, such as `blob:`, `about:srcdoc` or `about:blank`, Blok reads the host from the document's origin.

## Cut the link to popups with COOP

A framed page can open a popup. Only video and audio players from the registry, such as YouTube, Vimeo and Spotify, keep `allow-popups-to-escape-sandbox`. YouTube's "Watch on YouTube" needs an unsandboxed tab.

Code, document and social embeds, such as CodePen, Figma, Google Docs and posts, do not get it. Neither does a generic embed. Their popups keep the frame's sandbox, so some provider pages may refuse to load in them.

An escaped popup is a normal page of the framed site. In Firefox it could send your editor's tab to another address through `window.opener.top`. Chrome and WebKit refused.

Send `Cross-Origin-Opener-Policy: same-origin` on the page that hosts the editor. You still need it for the players, whose popups escape. The popup then had no `window.opener`, and the tab stayed put in all three browsers. `same-origin-allow-popups` did not help: in Firefox the popup still moved the tab.

`same-origin` may also break popups your app opens on other sites and talks to through `window.opener`, such as a sign-in window. Test those flows before you turn it on.

## Cap powerful features

A generic embed gets no `allow` list, so it gets no camera, microphone, geolocation or payment. A registry embed gets only `encrypted-media`, `fullscreen` and `picture-in-picture`.

To cap the whole page, send a `Permissions-Policy` header. Give each feature your app does not use an empty list:

- `camera=(), microphone=(), geolocation=(), payment=(), usb=()`

Browser compatibility data says only Chrome enforces this header. Firefox and Safari ignore it, so the `allow` attribute stays the main lever. We have not measured this.

## Serve user files from another site

An uploaded HTML or SVG file runs script as the site that serves it. On your app's own host it acts as one of your pages. Serve uploads from a separate host, such as an S3 bucket's own address.

A separate site is safer than a subdomain of your app, because browsers treat subdomains as the same site for `SameSite` cookies. If you use a subdomain, list it in `hostAliases` or `blockedEmbedOrigins`, so it never gets a generic embed.

A `blob:` URL has your page's origin too. Blok re-types the blobs it makes, but a `blob:` URL from your own uploader keeps the type you give it. Keep only safe types, such as images, mp4 and webm video, common audio, PDF and plain text. Re-type everything else to `text/plain` before you call `URL.createObjectURL`, including zip, Office and octet-stream.

Do not keep SVG in a `blob:` URL either, even for `<img>`. The reader can open the image in its own tab, and the SVG then runs as your site. Serve SVG from a separate site, or use a `data:image/svg+xml` URL, as Blok does.

## Check a link before it is framed

Many sites refuse to be framed, and the reader then sees an empty frame. Your backend can tell first. Pass `linkPaste.checkEmbed`, or set `server` and Blok calls the server's `GET /frame-check`.

- `blocked` or `insecure` inserts a bookmark instead, with a short notice.
- `allowed`, `unknown`, an error, or no answer within 3 seconds inserts the embed.

The verdict is a hint for the interface, not a security check. It never lifts another rule. The browser still enforces the framed site's own `X-Frame-Options` and `frame-ancestors`.

## What we measured

The popup and PDF results above come from runs on 3 November 2026, on a local test server. The browsers were Chrome 156 and Playwright's own builds of Firefox 146 and WebKit 26. Those two builds are patched, so stock Firefox and Safari may differ.

- A frame sandboxed without `allow-popups-to-escape-sandbox` opened popups that kept the sandbox. They could not move the editor's tab in any browser.
- With that token, the Firefox popup moved the editor's tab. `same-origin-allow-popups` did not stop it. With `same-origin`, the popup had no `window.opener` in all three browsers.
- An HTML page framed with no sandbox ran its script and could move the editor's tab. A `.pdf` address proves nothing, because the server's `Content-Type` decides.
- A `text/html` blob opened in a new tab ran as the page that made it, even with `rel="noopener"`.
- An `image/svg+xml` blob opened in a new tab ran its script as the page that made it, and read its storage. The same SVG as a data: URL showed in `<img>` in all three browsers. Opened in a tab, it did not open, or it ran with origin `null` and no storage.
- Chrome blocks its PDF viewer in every sandboxed frame, `sandbox=""` included. That is why remote PDFs open in a new tab.
