先に、一回のタブで終えられる照合を書いておきます。開発者ツールの Network を開き、アドレスバーの全体と、ドキュメントリクエストのリクエスト行を並べてください。アドレスバーには # の後ろの鍵があってよい。同じ文字列がリクエスト行に出てはいけません。出たら、ブラウザの既定動作ではありません。実装がフラグメントをクエリに書いたか、スクリプトが読んでリクエストへ入れたかのどちらかです。
クエスチョンとハッシュは、同じ層の保護ではない
一度きりの暗号文リンクは、よく「鍵が URL にある」と説明されます。この一文は粗すぎます。別物の事実を一つに重ねています。URL はクエリとフラグメントを同時に持てます。クエスチョン ? がクエリを開き、ハッシュ記号 # がフラグメントを開きます。どちらもアドレスバーに出ます。受け手がコピーするときも、たいてい全体を持ちます。HTTP の扱いは同じではありません。
クエリはリクエスト対象の一部になります。s.html?id=abc123 を開くと、サーバー、リバースプロキシ、CDN、アクセスログは設計上 id=abc123 を見ます。フラグメントは、そのドキュメントリクエストの一部ではありません。s.html?id=abc123#鍵 を開くと、ブラウザがサーバーへ取りに行くのは id 付きのページです。ハッシュ記号の後ろは、このタブに残り、ページのスクリプトが location.hash で読みます。
したがって「鍵が URL にある」が答えるのは、人が全体を見られるかどうかだけです。サーバーが鍵を見られるかどうかには答えていません。鍵に向くのはハッシュ記号の後ろです。ただし実装が本当にフラグメントへ残している場合に限ります。楽だからとクエリへ書いたなら、話は別です。
仕様が書いていること:フラグメントはクライアント側
これはある製品の私的な約束ではありません。RFC 3986 の 3.5 節は、フラグメントを「二次リソース」の識別子として定義します。ハッシュ記号が現れ、URI の末尾まで続きます。その解釈は、取り出した文書の種類に依存します。処理するのはクライアントです。URI スキームが書き換えるのではありません。
RFC 9110 の 7.1 節は、同じ規則を HTTP へつなぎます。ブラウザが導く対象 URI にフラグメントは含まれません。フラグメント識別子はクライアント向けだからです。ドキュメントを取りに行くとき、リクエスト行にハッシュ記号とその後ろは出てはいけません。
Referer も同じ切り方です。RFC 9110 は、ユーザーエージェントが Referer を作るとき、フラグメントと userinfo を入れてはならないと書いています。MDN の Referer の説明も一致します。ヘッダーにあってよいのはオリジン、パス、クエリであり、# フラグメントではありません。W3C Referrer Policy は、URL を referrer にする手順の最初でフラグメントを空にし、そのあとパスとクエリを残すかを決めます。
この三つの文書が証明するのは一件だけです。仕様どおりのブラウザは、ハッシュ記号の後ろの鍵を HTTP リクエスト行にも Referer にも書きません。ページのスクリプト、拡張機能、あなたが別の場所へ貼ったテキストまで、自動でこの段を消すことは証明していません。
鍵をクエスチョンへ置くと、ログに平文が出る
鍵を ?id=abc123&key=... と書くと、実装は楽です。サーバーは同じクエリで暗号文を取り、代行して復号できます。「オンラインで暗号化する」ページではよく見ます。「ホスティング側は鍵を見るべきではない」とは正反対です。
クエリはリクエスト行に出ます。大半のアクセスログ、リバースプロキシのログ、一部の CDN レポートにも入ります。鍵がクエリに入った時点で、「暗号文だけを持つ」はログの層ですでに成立しません。運用者がその日の access log を開けば、復号に必要な後半が見えます。
鍵を POST 本文へ移す案も出ます。URL ログは避けられます。それでも鍵はブラウザを出て、あなたが「ゼロ知識」と呼んだ機械へ届きます。フラグメントの値打ちはその逆です。鍵は HTTP の欄として出る必要がありません。ページは、このタブで location.hash を読むだけで足ります。
| 置く場所 | 人は見られるか | この HTTP は見られるか |
|---|---|---|
| クエスチョンの後ろのクエリ | 見られる。アドレスバーとコピーしたテキストに出る | 見られる。リクエスト行、プロキシ、アクセスログが記録する |
| ハッシュ記号の後ろのフラグメント | 見られる。アドレスバーとコピーしたテキストに出る | 既定では見られない。リクエスト行と Referer は仕様上この段を持たない |
| POST の欄 | アドレスバーにはない | 見られる。リクエスト本文がサーバーへ届く |
| 端末のメモリだけ。URL には書かない | 別の安全な経路を開かない限り、相手は見られない | 見られない。ただし一度開くリンクでは、受け渡しが完了しない |
正しい分け方:位置指定はクエスチョン、鍵はハッシュ
一度きりの暗号文リンクは、同時に二役を果たします。サーバーへ「どの暗号文を取るか」を伝え、受け手のブラウザへ「どの鍵でこの端末で復号するか」を伝えます。この二役を、同じ HTTP の欄に載せてはいけません。
照合できる分け方は s.html?id={id}#{key} です。クエリが持つのは位置指定の id だけです。ハッシュが持つのは鍵だけです。作成時、ブラウザは Web Crypto で AES-256-GCM を行い、出してよいのは暗号文だけです。閲覧時、スクリプトは location.hash を読み、サーバーへ暗号文を求め、このタブで復号します。サーバーは設計上、暗号文を一時保存し、読んだあと消滅させます。
MakePwd のワンタイムリンクは、この境界で実装しています。作成ページも閲覧ページも、開いてすぐ使え、アカウントはありません。閲覧ページは受け手に公開で、ログインを求めません。これは実装の選択であり、「ハッシュ記号イコール暗号プロトコル」ではありません。フラグメント自体に機密性はありません。鍵が HTTP の欄としてタブを出るのを避けるだけです。
本物のパスフレーズ、身分証番号、未マスクの表で実験しないでください。捨てられるテスト文を用意し、鍵はこの確認のためだけに作った一度きりのリンクにしてください。照合するのはリクエスト行とアドレスバーであり、秘密をもう一度広げることではありません。
Network とアドレスバーで、その場で照合する
前の記事では、カナリアで「平文が業務データとして端末を出たか」を見る方法を書きました。本稿は範囲を狭めます。鍵は HTTP に入ったか。手順は単独で終えられます。先に前稿を読む必要はありません。
害のないテスト文を作り、生成された完全なリンクをコピーします。ハッシュ記号の位置を見てください。前が s.html?id=...、後ろが鍵です。きれいなタブを開き、開発者ツールを開き、Preserve log を入れ、貼り付けて開きます。
- アドレスバーと、ドキュメントリクエストのリクエスト行を照合します。アドレスバーは
#とその後ろを残してよい。ドキュメントの URL に見えるのは path と?id=だけであり、ハッシュの後ろの鍵は出てはいけません。 - 続いて XHR / Fetch を見ます。暗号文を取るインターフェースは、設計上
idを持てます。同じ鍵が本文やクエリに再登場してはいけません。作成インターフェースの body は暗号文であり、いま入力したテスト文ではありません。 - 分析送信の query と body は別に開きます。ページのパスは出てよい。
location.hrefを全体のまま書いていたら、鍵は「HTTP の既定動作では入らない」から「スクリプトが自ら送った」へ移ります。これは実装の問題であり、仕様が壊れたのではありません。
三箇所がきれいでも、支えられる結論は狭いです。使っているこのブラウザ、この一回の表示では、観察できた HTTP の欄として鍵はタブを出ていない。ブラウザ、版、ページのスクリプトを変えたら、もう一度行ってください。
この層の保護が効かなくなるとき
フラグメントが防ぐのは、「この HTTP が鍵をホスティング側へ渡した」ことです。次の場面では、鍵はもともと HTTP の既定動作に頼っていません。仕様は助けになりません。
第一に、完全なリンクをチャット、メール、チケット、LINE に貼ったときです。受け側が見るのは、人の目に見える全体です。ハッシュ記号の後ろも、その履歴へ入ります。クライアントによってはハッシュを落とし、クエスチョンより前だけをプレビューします。そのとき受け手は鍵のないページを開き、閲覧ページはサーバーへ別の鍵を求めず、鍵がないと示すべきです。どちらにせよ、持っている人が使える証憑を、別のシステムへ渡しています。
第二に、ページのスクリプトと拡張機能は location.hash を読めます。閲覧ページが動く理由であり、XSS や悪意のある拡張が鍵を取れる理由でもあります。RFC 9110 の 17.11 節はすでに注意しています。フラグメントはリクエストに入りませんが、ユーザーエージェント、拡張機能、応答とともに来たスクリプトからは見えます。リダイレクトが元の URL のフラグメントを引き継ぐと、このサイトのフラグメントが別オリジンへ運ばれることもあります。
第三に、ブラウザ履歴、画面共有、クリップボードです。アドレスバーの全体は、端末の履歴に残ります。タブを会議室へ映せば、ハッシュ記号の後ろも壁に出ます。どれもサーバーを通りません。「フラグメントは HTTP に入らない」では反論できません。
第四に、URL を書き換える遷移ページや短縮 URL です。中間ページが path と query だけを転送すると、受け手が開いた時点でハッシュはもうありません。先に JavaScript で完全な href を読んでから飛ぶなら、鍵は中間者のフロントへ入ります。短縮 URL はとくに、渡したその文字列が、まだ # 付きの元の列かどうかをその場で見てください。
逆に言いがちな三つの点
「ハッシュ記号の後ろの方が安全だから、全体を自由に転送してよい。」成立しません。サーバーに対して安全であることと、グループチャットの履歴に対して安全であることは違います。完全な URL は証憑です。持った人が閲覧ページを開き、復号できます。
「Referer が外向きリンクへ鍵を漏らすかもしれない。」現行の仕様では、ブラウザが Referer を作るときフラグメントを落とさねばなりません。本当に見るべきは、ページ自身が location.href を分析、ログ、第三者スクリプトへ書くことです。照合は、いまも Network の送信内容です。「外向きリンクがあるから必ず鍵が漏れる」という仮定ではありません。
「閲覧ページは先にログインさせるべきだ。そうしないと誰でも開ける。」「完全なリンクを誰が持つか」と「誰がアカウントを持つか」を混ぜています。一度きりの暗号文リンクのアクセス制御は、文字列そのものです。受け手にログインを課しても、サーバーから鍵が見えなくなるわけではありません。鍵はもともとサーバーへ送るべきではありません。MakePwd の閲覧ページは受け手に公開です。作成も閲覧も、アカウントは不要です。
よくある質問
ハッシュ記号の後ろは、サーバーへ送られますか?
既定では送られません。RFC 9110 は、対象 URI にフラグメントを含めないと書いています。Network を開くと、ドキュメントリクエストのリクエスト行に # とその後ろの鍵は出てはいけません。サーバーが設計上見るのは、パスと ?id= だけです。
なぜ鍵をクエスチョンの後ろに置かないのですか?
クエスチョンの後ろのクエリは HTTP リクエスト行に入り、ホスティング側とアクセスログから見えます。鍵がクエリに入った時点で、「暗号文だけ」はログの層ですでに成立しません。位置指定の id はクエスチョンへ置いてよい。鍵はハッシュ記号へ置いてください。
完全なリンクをチャットに貼っても安全ですか?
サーバーは依然として鍵を見ません。チャット履歴、チケット、ブラウザ履歴は見ます。完全な URL は、持っている人が使えます。渡す必要があるなら、相手が受け取ったのがハッシュ付きの元の列かを確認し、その列は相手の履歴に残ると考えてください。
閲覧ページを開くのに、ログインは必要ですか?
不要です。閲覧ページは受け手に公開です。クエリの id で暗号文を取り、ハッシュの鍵でこのタブで復号します。MakePwd にアカウントとパスワード保管庫はありません。作成も閲覧も、開いてすぐ使えます。
次に一度きりの秘密を送るとき、覚えておく三つのこと
第一に、記号を見て、「鍵が URL にある」という空の一文を見ない。クエスチョンは HTTP に入り、ハッシュは既定では入りません。第二に、作成時は出域が暗号文であること、閲覧時はリクエスト行に # の後ろがないことを確認する。第三に、完全なリンクを渡す前に、その列がチャット履歴、チケット、ハッシュを落とす短縮 URL に入るかを自分に問う。後ろの二つが失敗しても、サーバーがゼロ知識かどうかとは無関係です。
平文が業務データとしてタブを出たかを見るなら、それは前の記事の範囲です。カナリアで Network の本文と分析送信を探します。本稿は、「なぜ鍵をハッシュ記号の後ろに置けるか」を、仕様とリクエスト行に照合できる判断へ落とすだけです。