// Privacy & Threat Model · Zero Egress

Your manuscript
never leaves
this machine.

るびこんは、入力された本文をネットワークに送り出しません。「送らない方針」という話ではなく、そもそも送り出す経路が存在しない——この一点を、ソースコード・ビルド成果物・配信ヘッダの三層に分けて確かめていきます。読み手としては技術者を想定し、なるべく密度を落とさずに書きました。

ZERO EGRESS CLIENT-SIDE ONLY CSP ENFORCED NO RUNTIME DEPS MIT · OPEN SOURCE

主張ではなく、検証から始める

この文書の結論は、読む前に自分で確かめられます。手順はごく単純です。本サイトを開き、開発者ツール(F12、または Ctrl/Cmd+Shift+I)の Network パネルを開いたまま、本文を貼り付けて変換してみてください。

変換しても、ネットワークリクエストは 一本も発生しません。XHR も Fetch も WebSocket も、ビーコンも、画像の GET によるピングも記録されません。もう一歩踏み込むなら、Consoleperformance.getEntriesByType('resource') を実行すると、ページが読み込んだリソースの URL が全部並びます。そこに自オリジン(rubicon.pages.dev)以外の宛先は出てきません。

この先は、なぜそうなるのかをコードのレベルまで分解した説明です。先に検証結果(通信ゼロ)があって、あとはその仕組みを追っていくだけ。この順序には意味があります——結論は実際に観測できる事実であって、こちらの言い分を信じてもらう必要がないからです。

何を「漏れない」と言っているのか

プライバシーの話は、何を守りたいのか(保護対象)と、何から守るのか(脅威)をはっきりさせないと意味を持ちません。守りたいものは明確で——あなたが入力欄に貼り付けた本文そのものです。未公開の原稿、筆名や作風、伏せ字や傍点の付け方といった執筆上の癖まで含めた、テキスト全体を指します。

これに対して、想定する脅威は次の四つです。

この四つすべてに対する答えは、最終的に一つの事実に行き着きます——本文がプロセスのメモリから外へ出ていく経路が、実装のどこにもない。送信が起きなければ、受信も蓄積も盗み見も解析も成り立ちません。保存もしていません。以下では、この「経路がない」を四つの層(アプリのコード/ビルド成果物/ブラウザのオリジン境界/配信時の CSP)に分けて、一つずつ確かめていきます。

この文書が扱わない範囲も書いておきます。これはあくまで本文の機密性についての話で、サービスが落ちない保証(可用性)や、あなたの端末そのものがすでにマルウェアに感染している場合の防御までは対象にしていません。後者は、どんな Web アプリの設計をもってしても防げない領域です。

本文の全経路を追跡する

入力された文字列がどこを通るのかを、関数の呼び出し単位で端から端まで追ってみます。途中でネットワークの境界もストレージの境界も跨がないことを、経路そのもので示します。

1<textarea>.value を読み取って、JavaScript の文字列としてメモリ上に持つ。DOM プロパティを読むだけ。ここまでネットワークは関与しません。
2パーサーが文字列を行ごとに分け、正規表現でルビ・傍点・字下げなどのトークンを取り出して、中間表現(AST)に変換する。入力は文字列、出力はオブジェクトの配列。副作用も I/O もない純粋な関数です。
3レンダラーが AST を、選んだ出力先サイトの記法の文字列に組み立て直す。入力はオブジェクトの配列、出力は文字列。これも純粋な関数です。
4結果の文字列を textContent で結果カードに表示する。必要なら、あなたの操作で navigator.clipboard.writeText() を呼んでローカルのクリップボードへコピー。表示は DOM、コピーは OS への操作。どちらもネットワークではありません。

この経路のどこにも、fetch / XMLHttpRequest / WebSocket / sendBeacon / new Image().src といった送信のための命令は出てきません。中核(パーサー・レンダラー・トークナイザー・AST の構築)は、引数の文字列を受け取って値を返すだけの関数の集まりで、document への参照すら持っていません。本文は プロセスのメモリから画面までで完結します。これが設計上の前提(不変条件)です。

送信の命令はソースにない

ソースツリー全体(src/ 配下の JavaScript すべて)を対象に、ネットワーク送信を起こしうる API を片っ端から検索しました。探したのは fetch( / XMLHttpRequest / .open( / .send( / WebSocket / EventSource / navigator.sendBeacon / new Image(、それに axios のような HTTP ライブラリの参照です。

実際に実行されるコードとしては、一件もヒットしませんでした。引っかかったのは二つだけ。一つは navigator.clipboard.writeText()(これはネットワークではなく OS のクリップボードへの書き込みです)、もう一つは「外部送信は一切行わない」という趣旨のコメント文字列です。前者はローカルの操作、後者は実行されないただのテキストです。

送信の命令がそもそも書かれていない、というのは「送信しないように気をつけて実装した」よりずっと強いことです。送信するための語彙が、コードベースに一度も登場しない。気をつけたかどうかの話ではなく、有無の話だからです。

配信バンドル唯一の通信命令を解剖する

誠実さのために、いちばん鋭いツッコミに先回りしておきます。ビルドした後の配信物(ミニファイ済みの dist/assets/*.js)を調べると、fetch(ちょうど一か所だけ見つかります。ソースには無いものが成果物にある以上、これは説明しておかないといけません。

該当箇所は、ミニファイされてこんな形になっています。

function o(s){ if(s.ep) return; s.ep=!0;
  const r=e(s); fetch(s.href,r); }

これは Vite が自動で吐く モジュールプリロード・ポリフィル__vitePreload という仕組み)です。役目は、<link rel="modulepreload"> に並んだコード分割チャンクを、対応していないブラウザのために先読みしておくこと。fetch の引数 s.href は、その link 要素の href、つまり このサイト自身の、同じオリジンの JS チャンク/assets/*.js)を指します。ここにユーザーの入力が入り込む余地はありません。取りに行くのはいつでも自分自身のプログラムの一部で、GET でスクリプトを読むだけです。

しかも今回のビルドでは、出力された dist/index.htmlrel="modulepreload" のリンクが 一つもありません(単一バンドル構成なので、Vite が preload リンクを出していない)。先読みする対象がないということは、この関数 o() を呼ぶ入口もない——つまり 一度も実行されない死コードです。仮にこの先コード分割が入って呼ばれるようになったとしても、送り先は自オリジンの .js に限られますし、あとで触れる CSP の connect-src 'self' が、それより外への逸脱を止めます。

まとめると、成果物にある唯一の fetch は、ビルドツールが付けたもので、宛先は自オリジンに限られ、今回のビルドではそもそも実行されません。本文を運ぶ経路ではないということです。ここを隠さず解剖しておくことが、残りの説明の信頼性につながると考えています。

CSP を一条ずつ読む

配信時、本サイトは次の Content-Security-Policy をレスポンスヘッダとして付けます(public/_headers に定義していて、ビルド成果物の dist/_headers にも同じ内容で出力されます)。

Content-Security-Policy:
  default-src 'self';
  script-src 'self';
  style-src 'self' 'unsafe-inline';
  img-src 'self' data: https://www14.a8.net;
  connect-src 'self';
  font-src 'self' data:;
  frame-ancestors 'none';
  base-uri 'self';
  form-action 'self'

各ディレクティブが何をしていて、本文の機密性にどう効くのかを、一つずつ見ていきます。

default-src 'self'
個別に指定していないリソース取得の既定値を、自オリジンに絞ります。以下の各ディレクティブは、この既定をリソースの種類ごとに上書きしたり追認したりするものです。フォールバックが 'self' である時点で、明示的に許可しない限り外部は塞がっています。
script-src 'self'
スクリプトの取得元を自オリジンに限ります。'unsafe-inline''unsafe-eval' も与えていないので、インラインの <script>evalon* 属性によるコード実行は拒否されます。これで外部の解析タグや広告 SDK を注入できなくなり、同時に XSS の実行段も一つ塞げます。第三者の JS が走らない以上、第三者が本文を読み出して送る、という役回り自体が成立しません。
connect-src 'self'
本文の機密性にとって、ここがいちばん効く条項です。fetch / XMLHttpRequest / WebSocket / EventSource / navigator.sendBeacon の接続先を自オリジンに限ります。仮にコードが(あるいは将来の依存が)外部へ送ろうとしても、ブラウザが接続そのものを拒否します。コードに送信が無いこと(03・04)に加えて、ブラウザの側でも外部の宛先が遮断される。これが二重化の片方です。
img-src 'self' data: https://www14.a8.net
画像の取得元を、自オリジン・data: URI・A8.net の指定ホストに限ります。A8.net を許可しているのは、将来のアフィリエイトバナー(<img> での表示)を見越した先回りの設定ですが、許しているのは 画像を読み込むことだけです。script-src'self' のままなので広告由来の JS は走らず、connect-src 'self' のままなので広告ホストへ能動的に接続することもありません。画像読み込みの GET に本文を載せる、といった実装も存在しません。
font-src 'self' data:
フォントの取得元を自オリジンと data: に限ります。Google Fonts のような外部フォントは読み込みません。外部フォントは「どのページを見たか」を第三者に伝えてしまうことがありますが、その経路自体を断っています。
frame-ancestors 'none'
本サイトを <iframe> などで他サイトに埋め込むことを禁じます。クリックジャッキングや、埋め込み元からの入力の盗み取りを防ぎます。後述の X-Frame-Options: DENY と意味が重なりますが、CSP 側のほうが新しく、より細かく制御できます。
base-uri 'self'
<base> 要素で基準 URL を書き換えることを、自オリジンに限ります。注入された <base> で相対 URL の解決先を外部にすり替え、スクリプトやフォームの送り先を乗っ取る、という攻撃を封じます。
form-action 'self'
フォームの送信先を自オリジンに限ります。もっとも本サイトはそもそも <form> を持たず(変換は <button type="button"> と JS のハンドラーで処理しています)、入力した本文がフォーム送信で外へ飛ぶ経路は二重に存在しません。

あわせて、次のレスポンスヘッダも付けています。

一点だけ補足を。_headers は Cloudflare Pages がレスポンスに付けてくれる仕組みなので、本番で実際に適用されるかどうかは配信環境側の責任で、コードだけでは断言できません。コードの側で確実に言えるのは、ポリシーが正しく定義されていて、ビルド成果物にも同じ内容で出力されている、というところまでです。実際に効いているかは、配信後に先ほどの方法(レスポンスヘッダの確認)で照らし合わせられます。

二重防壁:オリジン境界とCSPは別の層で効く

送信が起きないことは、性質の違う二つの仕組みで支えられています。よく混同されるので、分けて説明します。

一つめは、ブラウザの同一オリジンポリシー(SOP)と CORS です。これは「どの応答をスクリプトが読めるか」を決める標準の仕組みで、クロスオリジンの読み取りを原則として禁じます。ただし SOP が縛るのは主に 読み取りのほうで、送信(とくに no-cors の単純リクエストやビーコン)は完全には縛りません。だから SOP だけでは「外に出さない」の証明には足りません。

二つめが、CSP の connect-src です。これは「どこへ接続してよいか」を出口の側で縛ります。'self' を指定しているので、応答が読めるかどうか以前に、自オリジン以外への接続要求そのものがブラウザに拒否されます。SOP が読み取りの壁なら、connect-src送信の壁です。

この二つは独立していて、片方をすり抜けてももう片方が残ります。さらにその手前で、そもそもアプリのコードに送信の命令がありません(03)。「命令がない」→「接続が拒否される」→「読み取りも禁じられる」の三段で、外への送信は重ねて塞がれています。どこか一点が破られたら終わり、という作りにはなっていません。

なぜ innerHTML を使わないかが効く

変換結果を表示するときは textContent だけを使い、innerHTML は使いません。これは見た目の好みではなく、注入を防ぐための判断です。

入力本文には、ユーザーが書いた任意の文字列が入ります。<script><img onerror=...> のような断片が混じることもあります。これを innerHTML に流し込むと、ブラウザは文字列を HTML として解釈してしまい、注入されたタグやイベントハンドラーが動いてしまうことがあります。これが DOM-based XSS の典型的な入口です。XSS が成立すれば、攻撃用のスクリプトがページの中で動き、本文を読み出して外へ送る、という役回りができてしまう——機密性の前提が崩れます。

textContent は、文字列を必ずプレーンテキストとして差し込みます。<> はタグではなく、ただの文字として表示されるだけで、タグもハンドラーも動きません。入力に何が入っていようと、テキストとして描かれるだけです。

この選び方は、script-src 'self'(インライン実行の禁止)と補い合います。仮に注入の経路があっても CSP が実行を止めますし、そもそも textContent が注入の経路を作りません。入口を塞ぐ実装実行を止めるポリシーの二段で、XSS で本文が漏れる筋道を断っています。

永続化しない:履歴は残らない

送信を断っても、ローカルにため込んでいれば、あとで漏れる余地が残ります。そこで本ツールは、ブラウザのどんな保存領域にも本文を書き込みませんlocalStorage / sessionStorage / IndexedDB / document.cookie / Cache API / window.name への書き込みを片っ端から検索した結果、ソースでも配信バンドルでも ゼロ件でした。

その結果、変換した本文はセッションのメモリ上にしか存在せず、タブを閉じれば消えます。変換履歴という形で端末に残ることもありません。共有のパソコンや貸し出し端末でも、あとから使った人が前の本文を掘り出す経路がない、ということです。Cookie も発行しないので、トラッキング用の識別子も生まれません。

ランタイム依存ゼロ:サプライチェーン面を畳む

送信がなくても、第三者のライブラリが紛れ込めば、そのコードが何をするかはまた別の問題になります。最近の脅威でいえば、正規のパッケージが乗っ取られたり、悪意のあるアップデートが配られたりする サプライチェーン攻撃です。本ツールは、この面を構造から小さくしています。

package.json の依存は vitevitestdevDependencies だけです。これらはビルドとテストの段階でしか使われず、配信される本番バンドルには第三者のランタイムコードが入りません。出荷される JS は、自分たちの src/ 由来のコードだけでできています。つまり本番でユーザーのブラウザ上を動くコードに、外部由来の実行コードが存在しないということです。

これは「依存を信頼する」のではなく「本番に依存を持ち込まない」という構えです。信頼しなければならない第三者がゼロなら、その誰かが侵害されるリスクもゼロになります。点検すべき対象は、自分たちのコードの範囲に収まります。

正直に開示する、本文とは無関係な外部要素

「本文を送らない」ことと、「ウェブサイトとして外部要素がまったくない」ことは別です。後者を装うと、文書全体がかえって信用できなくなります。本文の機密性とは層が違う外部要素を、隠さずに挙げておきます。どれも入力本文にはアクセスできません。

ホスティングのアクセスログ

本サイトは Cloudflare Pages で静的に配信されます。ウェブサイトである以上、配信事業者がリクエストごとに IP・User-Agent・日時・取得したパスなどを インフラ層のログとして記録することはあり得ます。これはどんなウェブサイトにも共通する仕組みで、アプリのコードには現れません(コードからは、あるともないとも言えない層です)。

ただし本文はリクエストに乗りません(送信していないので)。だからこのログに本文が含まれることはありません。記録されうるのは「接続があった」という事実だけで、「何を変換したか」ではありません。

アフィリエイト広告(A8.net)

運営費のために、将来 A8.net のバナーを載せることがあります。載せたとしても、CSP は広告に 画像の表示(img-src)だけを許し、script-src 'self' が広告由来の JS を、connect-src 'self' が広告ホストへの接続を止めます。広告がページ内のコードを動かしたり、入力欄に触れたりする経路は、ポリシー上ありません。

今のところ広告タグは置いておらず、表示枠(プレースホルダ)だけがあります。Cookie などが使われるとしても、それは広告画像の配信に伴うもので、本文とは関係ありません。

この層をあえて開示するのは、前半の話を強くするためです。隠す必要のある部分がないからこそ、本文については「出ない」と言い切れます。

結論

Zero egress. Zero tracking. Zero cookies. Verifiable in your browser.

ここまでを、ひと言で

技術的な話が続きました。コードに馴染みのない方のために、要点だけ、ふだんの言葉で言い換えておきます。

ひとことで言えば——あなたの原稿は、この画面の外に出ません。上の技術的な説明は、その事実を隅々まで裏づけているだけです。

このポリシーや実装についての問い合わせは、運営(Takeyabu Studio)まで。るびこんは MIT ライセンスのオープンソースで、ここに書いたことはすべて誰でも確かめられます。

最終更新:2026年6月