。中の人です。
このアプリnot/on は、 note × Notion の書き味と表現力を目指した記事投稿/共有サービスです。
Markdown で書きながら(知らなくても大丈夫!)、公開イメージを確認しながら投稿できます。
ちなみにこのnot/on は、記事を書いてすぐに公開できる身軽さを大事にしているサービスです。
ただし「身軽さ」と「安全性」は別の話です。
今回、記事を公開する最後の工程に、生成されたHTMLの安全性を検証・無害化する仕組みを追加しています。
この記事では、その取り組みの背景と考え方をまとめます。
背景:なぜ必要だったか
not/on のエディタは、書いた内容をブラウザ側でMarkdownからHTMLに変換し、その場でサニタイズ(無害化)してからプレビュー表示しています。
ここまでは問題ありません。
ただし、この無害化処理はあくまでエディタというクライアント側で行われているものです。
仮に誰かがエディタを経由せず、API に直接「描画済みのHTML」を送りつけて公開できてしまえば、クライアント側の無害化はまるごと素通りしてしまいます。
公開されたページは not/on 自身のドメインで配信されるため、これが成立すると、記事を読んだ人のブラウザで不正なスクリプトが動いてしまう——という深刻な事態になりかねません。
つまり、「入り口」だけでなく「出口」でも守る必要がある、ということです。
方針:MDパーサーとは切り離した、公開直前の最終検問
今回の実装で最も大事にしたのは、既存のMarkdown→HTML変換の仕組みには一切手を加えないということでした。
予約語や外部サイトのiframe埋め込みなど、not/on らしい表現力はそのまま残したい。
そこで、変換ロジックとは完全に独立した「検証・無害化」の処理を新たに用意し、記事を公開する直前の、たった一箇所にだけ差し込みました。
流れとしてはこうなります。
本文を書く(Markdown)
→ HTML変換(従来どおり・無変更)
→ 【NEW】公開直前の検証・無害化
→ 公開(無害化されたHTMLをパブリッシュ)
この検証ロジックは、危険なものを個別に探して消していく方式ではなく、「安全だと確認できたタグ・属性・値だけを使って、HTMLを一から組み立て直す」という考え方を採っています。見つけたものを消す方式は、想定していなかった抜け道に弱いという弱点がありますが、許可したものだけを残す方式であれば、想定外の手口に対しても既定で安全側に倒れます。
要点:何を、どこまで守るか
検証ロジックのソースや詳細の仕様については非公開としますが、考え方のポイントを紹介します。
スクリプトを実行できてしまう要素や、クリックしなくても発火してしまうイベント処理は、テキストの中に埋め込まれていても、画像やSVGの中に隠されていても、確実に取り除く。
リンクや画像の参照先が、見た目は普通のURLでも実はスクリプトを実行する仕組みになっていないかを検査する。
外部サイトを埋め込む機能(iframe)はそのまま使えるようにしつつ、埋め込んだ外部ページが not/on 自体を乗っ取るような操作(ページ全体の強制的な移動など)はできないよう、埋め込みの権限を厳しく絞る。
万が一、検証中に想定外の事態が起きた場合は、危険なまま公開してしまうより「公開自体を止める」方を選ぶ。
これらの検証は、通常の記事であれば見た目や動作に一切影響が出ないよう、実際の記事に近いパターンで何度も動作確認を行っています。
補足:検証と実装の分担
今回の取り組みは、次のような分担で進めています。
| タスク | LLMモデル |
|---|---|
| 脆弱性の検証 | Claude Fable 5 |
| 実装計画の策定 | Claude Fable 5 |
| 実装 | Claude Opus 4.8 |
Mythos級と言われる現時点でAIモデルのトップクラスと考えられるClaude Fable 5 がnot/on の公開フローを精査し、どこにどんなリスクがあるか、どういう考え方で対処すべきかを整理してくれています。
また、そのFableが落とし込んだ実装計画をもとに、Claude Opus 4.8 が検証ロジックの実装を担当しました。
「設計・戦略」と「実装」で役割を分け、抜け漏れのない検証観点と、地に足のついた実装の両方の両立を実現しています。
おわりに
not/on はこれからも、書きやすさ・公開しやすさを大事にしていきます。
同時に、今回のような「見えないところの安全性」も継続的に(Claudeに頼って)見直していくつもりです。
今後も、開発の裏側で気づいたことがあれば、こういった形で発信していきます。