GenericPopover re-parses innerHTML while closing, which violates a strict CSP (style-src without 'unsafe-inline')
メンテナーはふだん 1 日以内に返信
まだ誰も着手していません。
評価
- 難易度
- 3/5
- 見積もり時間
- 半日
- 初心者へのやさしさ
- 62/100
- issue の種類
- バグ
- 明瞭さ
- 明確に書かれている
- 活発さ
- 活発
- 技術スタック
- react, typescript
- 領域
- frontend
調査の方向性
Start in packages/react/src/components/Popovers/GenericPopover.tsx, where the snapshot is taken from ref.current.innerHTML and rendered back through dangerouslySetInnerHTML while status === "close". Reproduce by serving the editor under a style-src CSP without 'unsafe-inline' and closing the / menu, then check that closing produces no CSP console errors. Done when the snapshot is kept as cloned DOM nodes, appended in a layout effect during close and removed in its cleanup, and the fade-out looks unchanged.
索引モデルが issue の本文から書いたものです。
説明
What’s broken?
I serve a BlockNote editor (@blocknote/react 0.56.0) under a strict CSP: style-src 'self' 'nonce-<per-response>', no 'unsafe-inline'. The nonce is forwarded to Mantine (see #997 / #792), so everything renders correctly while popovers are open.
But every time a popover closes (the / suggestion menu, the formatting toolbar), the console fills with ~100 errors: Applying inline style violates the following Content Security Policy directive 'style-src …', and the popover fades out with broken styling.
Cause
GenericPopover stores ref.current.innerHTML while the popover is open. When floating-ui reports status === "close", it renders that string again with dangerouslySetInnerHTML so the exit animation shows a frozen copy. The children carry inline style attributes (Mantine CSS variables, positioning). Parsing HTML that contains style="…" attributes is checked against style-src, so every one is blocked.
I checked in Chrome what a CSP without 'unsafe-inline' allows:
innerHTML,DOMParserand<template>are all blocked.Node.cloneNode(true)keeps the inline styles and is not blocked.- CSSOM writes such as
el.style.cssText = …are allowed.
What did you expect to happen?
Closing a popover should not trigger CSP violations under a style-src policy without 'unsafe-inline', and the exit animation should show the popover exactly as it looked while open.
Proposal
Keep the snapshot, but store it as cloned DOM nodes instead of an HTML string:
snapshot = useRef<Node[]>([]), filled withArray.from(ref.current.childNodes, n => n.cloneNode(true))in the same effect that currently readsinnerHTML.- While
status === "close", render the wrapper with no React children. A layout effect (deps[status]) appends the cloned nodes before paint, and its cleanup removes them. Reopening during the fade-out therefore doesn't leave the snapshot next to the real children. dangerouslySetInnerHTMLgoes away. No API change.
Behaviour is the same as before: the closing popover shows the last open DOM and fades out.
Diff (packages/react/src/components/Popovers/GenericPopover.tsx)
+import { useIsomorphicLayoutEffect } from "../../util/useIsomorphicLayoutEffect.js";
...
- const innerHTML = useRef<string>("");
+ // Cloned DOM nodes, not an HTML string: re-parsing HTML (`innerHTML`) is
+ // blocked by a Content Security Policy without `'unsafe-inline'` for the
+ // inline `style` attributes the children carry, while `cloneNode` keeps
+ // the inline styles without going through the CSP check.
+ const snapshot = useRef<Node[]>([]);
...
if (ref.current && hasChildrenBesidesPortalElementAnchor(ref.current)) {
- innerHTML.current = ref.current.innerHTML;
+ snapshot.current = Array.from(ref.current.childNodes, (node) =>
+ node.cloneNode(true),
+ );
}
...
+ // While closing, the wrapper is rendered without React children (see below)
+ // and the snapshot is appended to it here, before paint. The nodes are
+ // removed again when the status changes, so a popover that reopens while
+ // closing doesn't keep them next to its real children.
+ useIsomorphicLayoutEffect(() => {
+ if (status !== "close" || !ref.current) {
+ return;
+ }
+
+ const nodes = snapshot.current;
+ ref.current.append(...nodes);
+
+ return () => {
+ for (const node of nodes) {
+ node.parentNode?.removeChild(node);
+ }
+ };
+ }, [status]);
...
return (
<FloatingPortal root={portalElement}>
- <div
- ref={mergedRefs}
- {...mergedProps}
- dangerouslySetInnerHTML={{ __html: innerHTML.current }}
- />
+ <div ref={mergedRefs} {...mergedProps} />
</FloatingPortal>
);
I've run this as a pnpm patch against 0.56.0 in a strict-CSP build. Opening and closing / and the formatting toolbar gives zero new CSP errors. During the fade-out the wrapper has opacity 0 and a 250 ms transition, and the full clone (23 items / 101 styled descendants) is present, then removed.
Happy to open a PR with this change if you're open to it.
Steps to reproduce
- Serve the editor with
Content-Security-Policy: default-src 'none'; script-src 'self'; style-src 'self' 'nonce-…'(plus the usual connect/img directives), forwarding the nonce to Mantine. - Type
/to open the suggestion menu, then close it (Escape or click away). The formatting toolbar behaves the same way. - Check the console: ~100 errors per close,
Applying inline style violates the following Content Security Policy directive 'style-src …'.
BlockNote version
0.56.0 (main still has the same innerHTML snapshot)
Environment
Chrome (macOS), React 19, Vite build served with a per-response CSP nonce
Additional context
No response
Contribution
- I'd be interested in contributing a fix for this issue
Sponsor
- I'm a sponsor and would appreciate if you could look into this sooner than later 💖
- 主要言語
- TypeScript
- スター
- 10.3k
- フォーク
- 780
- 平均マージ
- 2日 20時間
- マージ済み PR(30日)
- 44
環境構築
- Dockerfile・Docker Compose ファイルなし
- プルリクエストのテンプレートあり
- コントリビューションガイドを読む
はじめの一歩
- issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
- 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
- リポジトリをフォークし、ブランチを切って変更します。
- issue 番号を参照したプルリクエストを送ります。
TypeCellOS/BlockNote のほかの issue
-
難易度 2/5 1〜3時間 初心者へのやさしさ 88/100
TypeCellOS/BlockNote#3211 ·
メンテナーはふだん 1 日以内に返信
-
難易度 2/5 1〜3時間 初心者へのやさしさ 82/100
TypeCellOS/BlockNote#2949 · コメント 1 件 ·
メンテナーはふだん 1 日以内に返信
-
a11y
難易度 2/5 1〜3時間 初心者へのやさしさ 68/100
TypeCellOS/BlockNote#2855 ·
メンテナーはふだん 1 日以内に返信
-
a11y
難易度 2/5 1〜3時間 初心者へのやさしさ 62/100
TypeCellOS/BlockNote#2829 · コメント 1 件 ·
メンテナーはふだん 1 日以内に返信
-
Comment thread actions: hover-only controls and unlabeled buttons in ThreadsSideba対応中かも このイシューにリンクされたプルリクエストがオープン中、またはマージ済みです。 オープンa11y
難易度 2/5 1〜3時間 初心者へのやさしさ 72/100
TypeCellOS/BlockNote#2824 ·
メンテナーはふだん 1 日以内に返信
TypeCellOS/BlockNote の issue をすべて見る
似ている issue
-
area:editor bug issue-form:bug P0 platform:windows review: low
難易度 2/5 1〜3時間 初心者へのやさしさ 64/100
1lck/Lithe-IDEA#1388 ·
メンテナーはふだん 1 日以内に返信
-
fix(api): isToolAuditEnabled swallows database read errors on auditToolOperations without loggingオープンarea: backend bug priority: low
難易度 2/5 1〜3時間 初心者へのやさしさ 82/100
snapotter-hq/SnapOtter#3238 ·
メンテナーはふだん 1 日以内に返信
-
rig up --plan times out after 5 s and reports an UNKNOWN outcome while the daemon completes the planオープン
難易度 2/5 1〜3時間 初心者へのやさしさ 82/100
メンテナーはふだん 1 日以内に返信
-
難易度 2/5 1〜3時間 初心者へのやさしさ 72/100
modelcontextprotocol/servers#5216 ·
メンテナーはふだん 1 日以内に返信
-
難易度 2/5 1〜3時間 初心者へのやさしさ 76/100
メンテナーはふだん 1 日以内に返信