はじめに
ログインなしで使えるアプリは手軽だ。
メールアドレスを入力したり、パスワードを設定したりする必要がなく、ページを開いてすぐに使い始められる。一時的なメモ、イベント案内、簡単な共有ボードなど、長く使い続けることを前提としないサービスとは特に相性がいい。
一方で、ログイン画面をなくせば設計も単純になる、とは限らない。
ログイン機能は、単にユーザー名を表示するためのものではなく、次のような役割も担っている。
- 誰がデータを作成したのかを確認する(認証)
- 誰が閲覧・編集・削除してよいかを判断する(認可)
- 紛失した権限を復旧する
- 不正な操作を特定する
ここで「認証(authentication)」と「認可(authorization)」は別の概念だ。認証は利用者やプロセス、端末の同一性を確認すること、認可はリソースへのアクセス権限を与えたり確認したりすることを指す。ログインをなくす場合、これらの役割を別の方法で補うか、機能自体を制限する必要がある。
この記事では、ログインなしアプリを設計するときに考えておきたいことを、現在開発を検討している Nobo Page を例に整理する。
ログインなしでも「認可」は必要になる
ログインがあるアプリでは、ユーザーのアカウントを基準に権限を判断できる。
たとえば、「このユーザーはこのデータの作成者だから編集できる」という判断だ。ここではアカウントによる認証の結果を使って、認可を行っている。
ログインなしの場合、アカウントを使った本人確認(認証)はできない。確認できるのは「この人が作成者本人か」ではなく、多くの場合「正しい編集トークンを提示したか」「作成時と同じセッション識別子を持っているか」だ。
つまり、ログインなしで実際に行っているのは、人間の本人確認ではなく、権限を示す秘密情報を所持していることの確認に近い。代表的な手段は次のようなものだ。
- ブラウザのセッション
- Cookie
- ランダムに発行したトークン
- 閲覧用・編集用の専用URL
- 端末内に保存した識別情報
このように、URL自体が権限として機能するものは、一般に Capability URL と呼ばれる。「ログインをなくす」というより、アカウント認証を、秘密情報の所持にもとづく認可へ置き換えていると考えた方が近い。
なお、Capability URLという用語と考え方自体は妥当だが、よく参照されるW3Cの文書は2014年のFirst Public Working Draftであり、W3C勧告(Recommendation)ではない点には留意しておきたい。
共有しないなら、設計はかなり単純にできる
ログインなしアプリでも、データを他人と共有しないのであれば、考えることはかなり減る。
たとえば、データを作成したブラウザのセッションからしか閲覧できない設計だ。
データを作成
↓
サーバーに保存
↓
作成時のセッションからだけ取得可能
この場合、データを開くための権限をURLに含める必要はない。
セッションを安全なCookieで管理すれば、URLが漏れただけでデータまで見られる問題を避けやすくなる。ただし「作成したブラウザからしか取得できない」というのは便宜的な説明で、実際にアクセスできるのはそのセッションCookieを提示できるクライアントだ。セッション識別子がコピーや窃取をされれば、別のブラウザからでも利用され得る。
そのため、Cookieを使うなら、最低限次のような点を押さえておきたい。
- Cookieに
Secure、HttpOnly、適切なSameSiteを設定する - セッション識別子は暗号学的に安全な乱数で生成し、十分な長さ(128ビット以上が目安)にする
- 有効期限はCookieのMax-Age任せにせず、サーバー側でもセッションの寿命を管理する
Cookieを使うならCSRFを忘れない
Cookieベースのセッションで見落としやすいのが CSRF(クロスサイトリクエストフォージェリ) だ。
ブラウザは対象サイトへのリクエストにCookieを自動で添付する。そのため、利用者が攻撃者のページを開いただけで、意図しない編集・削除リクエストを送らされる可能性がある。状態を変更するリクエストには、次のような対策を組み合わせたい。
- 編集・削除などの副作用をGETで実行しない
- フレームワークのCSRF保護やCSRFトークンを利用する
Originヘッダーの検証やFetch Metadataを併用するSameSite属性を適切に設定する
SameSite は多くのCSRFを抑制するが、すべての構成で単独の完全な対策になるわけではない。Cookieを使う以上、CSRFは別途考える必要がある。
なお、これでXSSの問題がなくなるわけではない。悪意のあるJavaScriptがページ内で動いてしまえば、Cookieの値を直接読めなくても、現在のセッションを使ってAPIにリクエストを送ることはできる。session-onlyな設計はリスクを小さくするが、XSS対策自体が不要になるわけではない。
難しくなるのは「ログインなしで共有する」とき
ログインなしアプリの設計が特に複雑になるのは、作成したデータを他人と共有できるようにするときだ。
共有する相手もアカウントを持っていない場合、サーバーは「この人は閲覧を許可された人なのか」をアカウントでは判断できない。
そこで、ランダムな文字列を含むURLを発行し、そのURLを持っていることを権限として扱う方法がある。
閲覧用URLを持っている人 → 閲覧できる
編集用URLを持っている人 → 編集できる
管理用URLを持っている人 → 削除や設定変更ができる
便利な仕組みだが、このURLは単なるページの住所ではない。URLを持っていること自体が、パスワードを知っていることに近い状態になる。
「リンクを知っている人だけ」は非公開とは限らない
共有URLを使うサービスでは、「リンクを知っている人だけが閲覧できます」という説明がよく使われる。
ただし、この表現は少し注意が必要だ。
リンクを受け取った人は、さらに別の人へ転送できる。チャット履歴やメールにも残るし、画面共有やスクリーンショットに映ることもある。一度漏れたリンクを完全に回収することも簡単ではない。
そのため、共有URLを使うサービスで「安全に非公開です」と言い切るのは危険だ。より正確には、次のような状態である。
このリンクを知っている人はアクセスできる。
利用者に対しては、機密情報や重要な個人情報の保存には向かないことも、分かりやすく説明する必要がある。
閲覧・編集・管理の権限を分ける
共有リンクを一つだけ発行し、そのリンクを持っている人が閲覧も編集も削除もできる設計は単純だ。しかし、リンクが漏れたときの被害も大きくなる。
そのため、用途に応じて権限を分ける方法がある。
閲覧リンク
編集リンク
管理リンク
閲覧リンクでは内容を見ることだけを許可し、編集リンクでは内容の変更を許可する。削除や保存期限の変更など、影響が大きい操作は管理リンクに限定する。
Nobo Pageでも、共有機能を提供するなら、このように権限を分離する設計が候補になる。権限を分けておけば、閲覧リンクを広く共有した場合でも、編集や削除まで許可してしまうことを防げる。これは最小権限の考え方そのものだ。
権限トークンをそのままDBに保存しない
共有URLに含まれるランダムなトークンは、提示するだけで操作を許す秘密情報だ。そのため、トークンをそのままデータベースに保存するのは避けたい。
共有URL:
https://example.com/page/123#edit=abc123...
DB:
edit_token = abc123...
これでは、データベースを閲覧できる人や、何らかの理由でデータが流出した場合に、そのまま編集権限を利用される可能性がある。
代わりに、サーバー側にはトークンのダイジェスト(ハッシュ値)だけを保存する。
利用者が持つもの:
edit_token = CSPRNGで生成した128ビット以上のランダム値
DBに保存するもの:
edit_token_digest = HMAC-SHA-256(server_secret, edit_token)
ここで注意したいのは、この方式はパスワードのハッシュ化とは目的が違うということだ。
利用者が記憶するパスワードは推測されやすいため、Argon2idやbcryptのような「あえて遅いハッシュ」が必要になる。一方、暗号学的に安全な乱数で生成した十分に長いトークンは、そもそも辞書攻撃や総当たりの対象になりにくい。そのため、トークンに対しては高コストなパスワードハッシュは必須ではなく、エントロピーの確保のほうが重要だ。
単純な SHA-256(token) でも、高エントロピーのトークンであれば総当たり耐性は確保できる。サーバー側の秘密鍵を使う HMAC-SHA-256 にしておくと、DBだけが流出した場合に、秘密鍵を持たない攻撃者が同じダイジェストを計算しにくくなる。
実装方針としては、次の点も合わせて押さえたい。
- 暗号学的に安全な乱数生成器(CSPRNG)を使う
- トークンごとに権限と有効期限をサーバー側で結び付ける
- トークンを失効・ローテーションできるようにする
- 比較には、既存ライブラリの安全な(タイミング差の出にくい)比較関数を使う
- トークンをアクセスログ、分析基盤、エラー本文に出力しない
つまり「ハッシュ化すれば安全」なのではなく、これはDB単体が漏洩したときの被害を軽減するための対策だと理解しておくのが正確だ。利用者のブラウザには実際のトークンが存在するため、ブラウザ側の安全性も引き続き重要になる。
URLフラグメントを使う理由と、その後の流れ
共有トークンをURLへ含める場合、クエリパラメータではなく、URLフラグメントを使う方法がある。
https://example.com/page/123#edit=secret-token
# より後ろの部分は、通常のHTTPリクエストではサーバーに送信されず、Referer ヘッダーにも含まれない。そのため、CDNやWebサーバーのアクセスログにトークンがそのまま残ることを避けやすくなる。
ただし、サーバーが編集権限を検証するには、最終的にJavaScriptがトークンをサーバーへ送らなければならない。フラグメントに入れるだけでは話が完結しない点に注意したい。典型的には次の流れになる。
/page/123#edit=secret-token を開く
↓
JavaScriptが location.hash を読む
↓
POST本文または Authorization ヘッダーでサーバーへ送る
↓
サーバーが検証する
↓
history.replaceState() でURLからトークンを除去する
ここで送ったトークンが、APM、WAF、アプリケーションログ、エラーレポートへ記録されないようにする必要がある。また、検証後は history.replaceState() などでアドレスバーと履歴からトークンを消し、共有や戻る操作で再び露出しないようにしておきたい。
より安全側に寄せるなら、生の編集トークンを毎回JavaScriptから送るのではなく、次のような構成も候補になる。
- フラグメントのトークンを一度だけサーバーへ渡して交換する
- その操作にスコープを限定した短期セッションを発行する
- 以後は
HttpOnlyCookieでそのセッションを使う - 元のトークンをURLから除去する
ただしこの構成ではCookieを使うことになるため、前述のCSRF対策がセットで必要になる。URLフラグメントは、あくまでトークンをサーバーログへ残しにくくするための一手段であり、それ自体でトークンが安全になるわけではない。
共有機能があるとXSSの影響が大きくなる
XSSは、ユーザーが入力した文字列などを安全に処理せず、HTMLやJavaScriptとして実行してしまう脆弱性だ。ログインなしの共有アプリでは、特に慎重に考える必要がある。
たとえば、攻撃者が次のような内容をボードに保存できたとする。
<script>
// 悪意のある処理
</script>
この内容がそのままHTMLとして表示されると、共有リンクを開いた別の利用者のブラウザでスクリプトが実行される可能性がある。
共有しないsession-onlyなアプリであれば、保存した内容を一般の別利用者に踏ませる経路は狭くなる。ただし、これで Stored XSS が消えるわけではない。保存された内容は、次のような画面で別の人の目に触れることがある。
- 運営の管理・通報確認画面
- サポート対応画面
- プレビューやエクスポート機能
- 障害調査用の画面
- 将来追加される共有機能
これらの画面が保存内容を表示すれば、依然として他者に対する Stored XSS になり得る。さらに、Reflected XSSやDOM-based XSSは、共有機能の有無とは直接関係なく発生する。
主な対策としては、次のようなものがある。
- ユーザー入力をHTMLとして直接描画しない
- 文脈に応じた出力エンコード(HTMLエスケープ)を徹底する
- 安全なDOM APIを使い、
innerHTMLへの生の代入を避ける - 危険なURLスキーム(
javascript:など)を拒否する - 任意のスクリプトや埋め込みコードを許可しない
Content Security Policy(CSP)も有効だが、これは主たる対策ではなく多層防御の一つとして位置づけるのが正確だ。基本は、安全なDOM APIと、文脈に応じた出力エンコード、必要な場合だけのHTMLサニタイズである。
Markdownを扱う場合も、「Markdown入力をサニタイズしてからHTML化する」よりも、MarkdownをHTMLへ変換した後に、そのHTMLを信頼できるサニタイザーへ通すほうが安全だ。変換の過程で危険なHTMLが生成される可能性があるためだ。
HttpOnly CookieだけではXSSを防げない
セッション管理にHttpOnly Cookieを使うことは重要だ。HttpOnlyを設定すると、JavaScriptから document.cookie を使ってCookieを直接読み取ることができなくなる。
ただし、XSSが発生した場合、攻撃者のスクリプトは利用者と同じページ上で動作している。そのため、Cookieそのものを取得できなくても、ブラウザにCookieを自動送信させてAPIを呼び出せる。
Cookieを盗む
→ HttpOnlyによって防ぎやすい
現在のセッションでAPIを操作する
→ XSSがあれば可能性が残る
HttpOnly Cookieは、セッション情報の持ち出しを防ぐための対策だ。XSSによる画面の読み取りや不正操作まで防ぐものではない。
ブラウザ内に権限情報を残しすぎない
ログインなしアプリでは、利用者が再び同じデータを編集できるようにするため、権限情報をブラウザへ保存したくなることがある。たとえば、LocalStorageに編集トークンを保存する方法だ。
しかし、LocalStorageはページ上のJavaScriptから読み取れる。XSSが起きた場合、保存していた編集トークンや管理トークンを外部へ送信される可能性がある。これは sessionStorage や IndexedDB でも同様で、XSSに対しては同じように弱い。また、共有PCや他人の端末を使っている場合、その端末に権限情報が残り続ける問題もある。
そのため、何をブラウザへ保存するかは慎重に決める必要がある。言語設定やテーマ設定のような情報と、編集権限や管理権限は分けて考えるべきだ。
Nobo Pageでは、利便性のために権限を長期間保存するよりも、必要に応じて管理リンクを利用者自身に保管してもらう設計の方が安全側になる。
機密性のあるレスポンスはキャッシュさせない
閲覧・編集画面や、トークンを伴うAPIレスポンスは、内容に応じてキャッシュ制御を明示すべきだ。
何も指定しないと、ブラウザ、CDN、プロキシー、Service Workerなどに、機密性のあるレスポンスが残る可能性がある。共有端末で前の利用者の内容が表示される、といった事故にもつながる。
- 機密性のある応答には
Cache-Control: no-storeを検討する no-cacheは「保存禁止」ではなく「再検証してから使う」という指示である点に注意する- トークンを含むURLやレスポンスを、中間キャッシュに残さない
削除や権限変更をGETで実行しない
Capability URLを開いただけで、削除・公開・権限変更が実行される設計は避けたい。
リンクプレビューの生成、検索エンジンのクローラー、セキュリティスキャナーなどが、URLへ自動的にアクセスする可能性があるためだ。これらが副作用のあるURLを踏むと、意図しない削除や変更が起きてしまう。
状態を変える操作は、必ずPOSTなどの明示的なリクエストで行い、前述のCSRF対策と組み合わせる。「URLを開く」こと自体に破壊的な副作用を持たせないのが原則だ。
匿名アクセスならではの不正利用に備える
ログインがないと、誰がどれだけ使っているかを利用者単位で把握しにくい。そのため、匿名サービス特有の不正利用にも備えておきたい。
- スパム投稿や、大量のボード自動作成
- ストレージの消費を狙った濫用
- フィッシングページの作成・配布
作成・更新・閲覧の各APIには、IPだけに依存しすぎないレート制限、サイズや件数の上限、異常検知、そして通報・失効の手段を用意しておくと安全側になる。
将来的にファイル添付を提供するなら、種類とサイズの制限、Webルート外への保存、ウイルス検査、ダウンロード制御なども別途必要になる。
復元できることを前提にしない
ログインありのサービスでは、メールアドレスなどを使って本人確認し、アカウントやデータを復旧できる。
ログインなしの場合、管理リンクを紛失した利用者が本当の作成者かどうかを確認する手段がない。仮に「このボードを作ったのは自分です」と問い合わせがあっても、それを証明できなければ、管理権限を再発行するのは危険だ。
そのため、ログインなしの軽さを提供する代わりに、次のような制約が必要になる。
- 管理リンクを紛失すると復元できない
- 削除したデータは原則として戻せない
- 保存期間を過ぎたデータは復元できない
- 運営への問い合わせだけでは権限を再発行しない
また「自動削除する」「復元できない」と説明するなら、その範囲も実態と一致させたい。本体のデータを消しても、次のような場所にコピーが残っていれば、説明と矛盾してしまう。
- バックアップ
- CDNキャッシュ
- 検索インデックス
- サムネイルや変換済みファイル
- 監査ログや障害解析用データ
不便に見えるが、第三者へデータを渡さないために必要な制限でもある。
外部スクリプトは「画面」より「オリジン」で分ける
Webサービスには、アクセス解析、広告、ヒートマップ、チャットサポートなど、さまざまな外部スクリプトが追加される。外部スクリプトは、埋め込まれたページと同じ権限でJavaScriptとして動作するため、任意コード実行、機密情報の漏洩、提供元の侵害といったリスクを持ち込む。
そのため、すべてのページへ同じスクリプトを入れるのではなく、画面の役割によって分ける考え方がある。
ただし、単に「ボード画面には置かない」だけでは、両方が同じオリジンである限り隔離としては不十分なことがある。より強く分けるなら、オリジンそのものを分離したい。
www.example.com
→ 紹介・広告・アクセス解析
app.example.com
→ ボードの閲覧・編集
→ 外部スクリプトは原則禁止
Web StorageやIndexedDBはオリジン単位で分離されるため、別オリジンにすればクライアント側のデータも隔離できる。あわせて、Cookieに不用意な Domain=.example.com を設定せず、必要なオリジンへ限定することも重要だ。
Nobo Pageでは、共有する価値を残す
共有機能をなくし、session-onlyなアプリにすれば、Nobo Pageの設計はかなり単純にできる。URLに権限を持たせる必要もなく、閲覧・編集・管理リンクを分ける必要もない。
しかし、Nobo Pageでは、作成した内容をすぐ他人へ渡せることも価値の一つだ。イベント当日の案内、短期間だけ使う共有メモ、QRコードから開く情報ページなどは、作成者だけが閲覧できても用途を満たせない。
そのため、共有機能そのものをなくすのではなく、共有することで増えるリスクを前提に設計する必要がある。具体的には、次のような方針だ。
- 閲覧・編集・管理権限を分離する(最小権限)
- CSPRNGで128ビット以上のトークンを発行する
- 生の権限トークンをDBへ保存せず、ダイジェストで保持する
- 権限リンクに有効期限や失効手段を持たせる
- Cookieを使う場合は
Secure/HttpOnly/SameSiteとCSRF対策を設定する - ユーザー入力を安全に表示する(出力エンコードとHTMLサニタイズ)
- 機密性のある応答をキャッシュさせない
- 状態変更をGETで実行しない
- 匿名利用に対するレート制限や濫用対策を用意する
- ボード画面の外部スクリプトを、できれば別オリジンで制限する
- 保存期間を必要以上に長くしない
- 機密情報向けではないことを明確に伝える
ログインなしという特徴だけで危険になるわけではない。ただし、ログインが担っていた認証や認可をなくす以上、その代わりになる仕組みを曖昧なままにしてはいけない。
まとめ
ログインなしアプリは、利用開始までの負担を大きく減らせる。特に、一時的な用途や、アカウントを作るほどではない場面では、とても相性がいい。
一方で、ログインをなくすと、アカウントが担っていた認証や認可を別の方法で考える必要がある。共有しないsession-onlyなアプリであれば比較的単純な構成にできるが、それでもCookieを使う以上はCSRFやセッション管理を考える必要がある。ログインなしで他人とデータを共有する場合は、URLやトークンそのものが権限になる。
その場合は、次の点が重要だ。
- URLを単なる住所ではなく鍵として扱う
- 権限を必要以上に広くしない
- トークンを安全に生成・保存する
- XSSやCSRFによる権限漏洩・不正操作を防ぐ
- キャッシュや保存期間、復元範囲を限定する
- サービスの限界をユーザーへ正直に伝える
ログインなしの手軽さは、セキュリティを考えなくてよいという意味ではない。むしろ、ユーザーに意識させず安全に使ってもらうために、裏側では通常のログインアプリとは異なる設計が必要になる。
参考
- Authentication — NIST Computer Security Resource Center Glossary
- Good Practices for Capability URLs(W3C Working Draft)
- Cross-Site Request Forgery Prevention — OWASP Cheat Sheet Series
- Session Management — OWASP Cheat Sheet Series
- Cross Site Scripting Prevention — OWASP Cheat Sheet Series
- Same-origin policy — MDN