Prompt
クライアントサイドでどれだけ慎重に入力値の検証をしようが、安全性には対して寄与しないんだよなぁ。
どうしても、ユーザーからAPIへ送られたデータをinnerHtmlとして他人に表示しなければならないとき、その対策はないのかな?
まさにその通り!フロントエンドのバリデーションは、あくまで 「ユーザーの入力ミスをその場で教えてあげる親切心(UX)」 であって、セキュリティ上の防壁としては紙屑同然だよね。
APIへのリクエストは curl や Postman からいくらでも改ざんして送れる以上、「サーバーに届くデータはすべて悪意に満ちている」 と考えるのが還元主義的な正しい設計思想だ。
さて、どうしても「ユーザー由来のHTML」を表示しなければならないという、まさに XSS(クロスサイトスクリプティング)の地雷原 を歩くような状況だね。対策はいくつかあるけれど、情報の重要度順に解剖していこう!
1. 究極の対策:innerText / textContent への逃げ
もし、そのデータに「HTMLタグ」が含まれている必要がないなら、innerHTML を捨てて textContent を使うのが最強かつ唯一の「完封」手段だよ。
これなら、<script> と送られてきても、ブラウザはただの文字列としてレンダリングするから、スクリプトは発火しない。
2. どうしても HTML が必要な時の「サニタイズ(消毒)」
リッチテキストエディタのように、<b> や <i> だけは許容したい場合は、DOMPurify のような信頼できるサニタイザーライブラリを使うのが業界標準だ。
-
仕組み: 文字列を一度パースし、ホワイトリスト(許可されたタグや属性)以外をすべて削ぎ落とす。
-
ポイント: 自前で正規表現を書いて
replaceしようとするのは絶対にダメだ。ハッカーはエンコーディングや大文字小文字の混在を突いて、君の正規表現を簡単にバイパスしてくるよ。
JavaScript
import DOMPurify from 'dompurify';
const cleanHtml = DOMPurify.sanitize(dirtyHtmlFromApi);
element.innerHTML = cleanHtml;
3. ブラウザの最終防衛ライン:CSP (Content Security Policy)
アプリケーションのコードを信頼しすぎず、ブラウザそのものに 「変なスクリプトは動かすな」 と命じる設定だ。
-
やり方: HTTPレスポンスヘッダに
Content-Security-Policyを含める。 -
効果: 例えば
script-src 'self'と設定しておけば、もしinnerHTMLを通じて外部の悪意あるスクリプトが注入されても、ブラウザが「あ、これは許可されたドメイン(自分自身)以外からのスクリプトだ」と判断して実行をブロックしてくれるんだ。
4. Trusted Types API(最新の防壁)
最近の Chrome などで導入されている Trusted Types は、innerHTML に「ただの文字列」を代入すること自体をブラウザレベルで禁止する仕組みだよ。
-
仕組み: あらかじめ定義したポリシーを通った「信頼されたオブジェクト」しか受け付けなくなる。
-
利点: チームの誰かがうっかり
innerHTML = rawDataと書いてしまっても、実行時にエラーを吐いて止めてくれるから、ヒューマンエラーによる XSS を物理的に防げるんだ。
豆知識:React や Vue が安全な理由
君が Next.js(React)を使っているなら、標準では XSS の心配が少ないよね。
それは React が内部的に 「すべての文字列を自動的にエスケープ(無害化)」 してから DOM に反映させているからなんだ。
わざわざ dangerouslySetInnerHTML という「危なそうな名前」のプロパティを使わないと HTML を流し込めないようになっているのは、「君が今やろうとしていることは地雷を踏む行為だよ」 と明示的に警告するためなんだね。
見落とされがちな視点:Cookie の HttpOnly 属性
XSS の最大の目的は、被害者のセッション(Cookie)を盗むことだ。
もし innerHTML からスクリプトが動いてしまっても、Cookie に HttpOnly 属性がついていれば、document.cookie から値を読み取ることができなくなる。これは、たとえ壁が破られても「宝箱だけは開けさせない」という重要なラストラインになるよ。
結論:多層防御(Defense in Depth)
-
サーバー側で DOMPurify 等によるサニタイズ。
-
ブラウザ側で CSP による実行制限。
-
Cookie に HttpOnly を付与。
この 3 段構えがあれば、たとえ innerHTML を使わざるを得ない状況でも、ホワイトハッカー級の堅牢さを手に入れられるはずだよ!
次は、「実際に XSS がどうやって Cookie を盗み出すのか、そのペイロード(攻撃コード)の例」 を見てみる?それとも、「Next.js で安全にサニタイズを実装する具体的なコード」 を書いてみる?
Would you like me to write a simple CSP header configuration that restricts script execution to your own domain?
Prompt
やっぱり、かなり深刻なジレンマに遭遇している。
tiptapテキストエディターは、html文字列を返す。手入力ではどうやってもscriptは入力できないが、そんなことは関係ない。
ただ、そのtiptapで入力された形式を完全に保ったまま表示しなければならない。だが、それとXSS安全性は相反する。
そのジレンマ、情報工学を学ぶ身としては非常に胃が痛む問題だよね。「表現の自由(リッチテキスト)」 と 「セキュリティ(XSS対策)」 は、まさにエンジニアが歴史上何度もぶつかってきた伝統的な対立構造だよ。
Tiptap(ProseMirrorベース)は非常に優秀で、エディタ内部では「JSON形式(抽象構文木)」でデータを保持しているから、エディタ上での入力は安全に制御されている。けれど、そこから出力された 「HTML文字列」 を受け取って innerHTML で表示する瞬間、それは単なる「制御不能な生データ」に成り下がってしまう。
この深刻なジレンマを、安全性を損なわずに、かつデザインを1pxも崩さずに解決する 「現実的な解法」 を 3 つ提示するよ。
1. サーバーサイド(または表示直前)での「厳格なサニタイズ」
これが最も王道だよ。「Tiptapの形式を保つ」というのは、具体的には <b>, <ul>, <img> といった 特定のタグと属性だけを許可する ということだよね。
DOMPurify を使い、Tiptapが生成するタグだけを「ホワイトリスト」に登録して洗浄するんだ。
JavaScript
import DOMPurify from 'dompurify';
// Tiptapが標準で出力するタグや属性だけを許可する設定
const cleanHtml = DOMPurify.sanitize(dirtyHtml, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p', 'ul', 'ol', 'li', 'br', 'img', 'blockquote', 'code', 'pre'],
ALLOWED_ATTR: ['href', 'src', 'alt', 'title', 'class'], // classを許容しないとスタイルが崩れる場合がある
});
この方法なら、<script> や onerror といった「爆弾」だけを取り除き、Tiptapの見た目(HTML構造)は完全に維持できるよ。
2. JSON形式での保存と「読み取り専用エディタ」での表示
実は、これが Tiptap における 「最もエレガントな解法」 かもしれない。
Tiptap(ProseMirror)は editor.getJSON() で、HTMLではなく構造化された JSON(Node) を吐き出せるよね。
-
保存: サーバーには HTML ではなく JSON を送る。
-
表示: 表示側でも Tiptap を使い、
editable: false(読み取り専用) モードでその JSON を読み込ませる。
なぜこれが安全なのか?
JSON データはただのオブジェクトであり、それを Tiptap のレンダラーが解析して DOM を構築する際、React や Vue と同様に 自動的にエスケープ処理 が行われるからなんだ。HTML 文字列を介さないため、innerHTML による脆弱性が入り込む隙がない。
3. iframe による「サンドボックス化」
もし、どうしてもサニタイズ漏れが怖くて、かつ生に近い HTML を表示しなきゃいけないなら、iframe の sandbox 属性を使って別ドメイン(または別オリジン)で隔離する手がある。
HTML
<iframe
srcdoc=""
sandbox="allow-forms"
></iframe>
-
sandbox属性: スクリプトの実行(allow-scripts)を許可しなければ、たとえ中に<script>があってもブラウザが強制的に停止させる。 -
メリット: メインのサイトの Cookie や DOM には絶対にアクセスできない。
豆知識:Content Security Policy (CSP) の「最後の一押し」
どんなに慎重にサニタイズしても、将来 Tiptap のプラグインが増えて未知のタグが出力されるかもしれない。
そのための保険として、以前話した CSP を設定しておこう。
script-src 'self';
これをヘッダに入れておくだけで、万が一サニタイズをすり抜けて innerHTML に <script>alert(1)</script> が入ってしまっても、ブラウザが 「インラインスクリプトの実行は禁止されているよ!」 とブロックしてくれる。これが本当の「二重の鍵」だね。
見落とされがちな視点:CSS インジェクション
HTML タグを許可するということは、style 属性や class 属性も許可することになるよね。
悪意あるユーザーが position: fixed; top: 0; left: 0; width: 100%; height: 100%; のようなスタイルを送り込み、サイト全体を偽のログイン画面で覆い隠す(フィッシング)といった攻撃もあり得る。
サニタイズする際は、許可する CSS プロパティ も絞り込むのがホワイトハッカー級の気配りだよ。
君の SNS プロジェクト「AXON」において、投稿内容がリッチテキストなら、個人的には 「JSON で保存して、表示側も Tiptap(Read-only)で描画する」 のが、最も TypeScript との相性も良く、堅牢で、かつ「Tiptap らしさ」を損なわないベストプラクティスだと思うよ。
Would you like me to write a simple React/Next.js component example that renders Tiptap content safely from a JSON object?
Prompt
たくさんのパースを挟まないといけない場合でも、tiptapによるパースは現実的?
そして、aタグで予期せぬxssは発生する?
Tiptapを介したパースの現実性と、<a> タグという「XSSの定番の入り口」について、情報工学的な観点から解剖していこう!
結論から言うと、Tiptapによるパース(JSONを読み込んで描画する手法)は、大規模なシステムでも非常に現実的かつ「推奨される」アプローチ だよ。そして <a> タグは、タグ自体が許可されていても 属性(プロトコル) の段階で非常に危険な XSS の火種を抱えているんだ。
1. Tiptap によるパースは「重い」のか?(現実性の検証)
「たくさんのパースを挟む」ことへの懸念、処理コストの面で気になるよね。でも、以下の理由から心配しすぎる必要はないよ。
-
AST(抽象構文木)の効率性: Tiptap(ProseMirror)の JSON は、単純な木構造(AST)だ。正規表現や複雑な HTML 文字列のパースに比べれば、JavaScript のオブジェクトとして走査するのは CPU にとって非常に軽い処理なんだ。
-
コンポーネント化の恩恵: React や Vue で表示用のエディタを
editable: falseで置く場合、仮想 DOM の仕組みが効率よく差分を計算してくれる。1ページに数十個の投稿を表示する程度なら、現代のデバイスでは誤差の範囲だよ。 -
サーバーサイド・レンダリング (SSR): もしクライアントの負荷を究極まで減らしたいなら、サーバー側(Node.js)で Tiptap の
generateHTML関数を使って、「サーバー側で一度だけ安全に HTML 化して、クライアントには静的な HTML を送る」 という戦略も取れる。
2. <a> タグに潜む XSS の罠:プロトコルの恐怖
君の懸念通り、<a> タグは href 属性があるだけで、一気に危険度が跳ね上がる。たとえ DOMPurify でサニタイズしていても、デフォルトの設定によっては以下の攻撃が成立してしまうんだ。
javascript: プロトコルの注入
HTML
<a href="javascript:alert('XSS')">Click me!</a>
これはタグとしても属性としても「正常」に見えるけれど、クリックした瞬間に JavaScript が実行される。これが <a> タグにおける XSS の代表格だね。
data: プロトコルの悪用
HTML
<a href="data:text/html;base64,PHNjcmlwdD5hbGVydCgxKTwvc2NyaXB0Pg==">Click me!</a>
Base64 でエンコードされた HTML を踏ませることで、別ページとしてスクリプトを実行させる手法だ。
3. どう対策すべきか?(具体的な防衛術)
Tiptap の Link エクステンションを使っているなら、以下の設定で「入り口」を絞り込むのが鉄則だよ。
① protocols の制限
Tiptap の Link エクステンションの設定で、http と https(および mailto, tel)以外のプロトコルを拒否するように明示する。
② 表示時のサニタイズ(DOMPurify の活用)
もし HTML として出力・表示するなら、DOMPurify で 「プロトコルのバリデーション」 を有効にする。
JavaScript
import DOMPurify from 'dompurify';
const clean = DOMPurify.sanitize(html, {
ALLOWED_TAGS: ['a', 'p', 'b'],
ALLOWED_ATTR: ['href', 'target', 'rel'],
// URI のプロトコルを制限する(これが重要!)
ALLOWED_URI_REGEXP: /^(?:(?:(?:f|ht)tps?|mailto|tel):|[^a-z0-9+.-]|(?:[a-z0-9+.-]*[^a-z0-9+.-:]))/i
});
③ rel="noopener noreferrer" の自動付与
XSS とは少し違うけれど、target="_blank" でリンクを開かせる場合、遷移先のサイトから元のサイト(window.opener)を操作される脆弱性がある。Tiptap の Link 設定でこれらを強制的に付与するのは、現代の Web 開発の「マナー」だね。
見落とされがちな視点:id や name 属性の「DOM Clobbering」
もしサニタイズの設定が甘くて id や name 属性を許可してしまうと、DOM Clobbering という攻撃を受ける可能性がある。
例えば <a id="config" href="..."></a> と注入されると、JavaScript の window.config という変数がこの <a> タグに上書きされてしまい、アプリのロジックを破壊されることがあるんだ。
結論
-
パースの現実性: JSON ベースの処理は非常に堅牢で、パフォーマンスも実用的。積極的に使うべき!
-
<a>タグの危険性: タグそのものよりhrefの 中身(プロトコル) が危険。必ずホワイトリスト形式でhttp/httpsだけを許可しよう。
君のプロジェクト「AXON」で、ユーザー間のリンク共有を安全にするなら、やはり 「JSON で保存 + 表示時にプロトコル制限付きのレンダラーを通す」 のが、情報工学的にも最も「美しい」解になるはずだよ!
Would you like me to write a specific Tiptap extension configuration that strictly limits allowed link protocols?