安全なログイントークン管理、フロントエンドの必須戦略
November 22, 2024
序文
現代の技術環境において、ユーザー認証方式は日々進化しており、それに伴ってセキュリティ方式も絶え間なく進化しています。高い拡張性と柔軟性を持つトークンベースの認証方式は急速に標準化され、特にセキュリティと効率性の理由から、従来のセッションベースの認証方式に取って代わるケースが増加しています。QueryPieもセキュリティ向上のために、製品にトークンベースの認証方式を導入しました。
フロントエンドにおける新たな挑戦
ウェブフロントエンドにおける認証の実装歴はそれほど長くありません。従来のセッションベースの認証方式では、サーバーがユーザーセッションを管理していました。フロントエンドでの認証とは、フォームを適切に作成し、サーバーが指定したエンドポイントにIDとパスワードを正しく送信することに終始していました。しかし、ここ数年、フロントエンド領域は多くの技術的な変化に直面しています。
- AJAXの登場とSingle Page Application(SPA)構造により、フロントエンド開発者はバックエンドとJSONベースのAPI通信でデータをやり取りするようになりました。
- Web Storage APIの登場により、フロントエンドは永続性(Persistence)を容易に処理できるようになりました。
- NextJSのようなSSR(サーバーサイドレンダリング)サーバーの登場により、以前サーバーで行われていた役割がフロントエンドに移行しました。
これらの変化は、トークンベースの認証方式と連携して、フロントエンドに安全なトークンの送受信と永続性管理という新たな課題を投げかけました。
安全なトークン、そうでないユーザー環境
トークンベースの認証は、開発の利便性だけでなく、トークンの改ざんがほぼ不可能であるため、セキュリティの面でも優れています。しかし、トークン自体が盗まれてしまった場合、話は変わります。
脅威の種類
トークンを盗まれる主な経路は、ユーザーのブラウザです。ユーザーが古いブラウザを使用している場合、そのブラウザ自体の脆弱性を突かれる可能性があります。最近では、Chromeのように常に最新バージョンを使用させるエバーグリーンブラウザの普及により、ブラウザの脆弱性は迅速に修正されています。
むしろ脆弱な部分は、ブラウザが提供するさまざまなセキュリティレベルを満たしていないJavaScriptコードやサーバー設定にある可能性が高いです。このような状況を悪用し、攻撃者はクロスサイトスクリプティング(Cross Site Scripting、XSS)、クロスサイトリクエストフォージェリ(Cross Site Request Forgery、CSRF)、セッション ハイジャッキング(Session Hijacking)などの攻撃を通じてトークンを盗む、またはそれに準じた攻撃を行うことができます。
クロスサイトスクリプティング(XSS)
https://owasp.org/www-community/attacks/xss/
クロスサイトスクリプティング(XSS)攻撃は、悪意のあるスクリプトが無害で信頼されているウェブサイトに注入されるタイプのインジェクション攻撃です。XSS 攻撃は、攻撃者がウェブアプリケーションを利用して、一般的にブラウザ側のスクリプトの形式で悪意のあるコードを別のエンドユーザーに送信する際に発生します。この攻撃が成功する原因となる脆弱性は広範囲にわたり、ウェブアプリケーションがユーザーからの入力を検証やエンコードなしで出力に含める場所で発生します。XSS 攻撃では、通常、攻撃者がユーザーのブラウザで実行されるスクリプトを悪用し、ユーザーのセッションを乗っ取ったり、個人情報を盗んだり、悪意のある行動を実行したりすることができます。
XSS(クロスサイトスクリプティング)攻撃は、ユーザーのブラウザに悪意のあるスクリプトを実行させる方法で、伝統的なコードインジェクション攻撃です。様々な攻撃方法がありますが、最も一般的な例を以下に示します。
- 攻撃者は、
vulnerable-site.com/search?query=${query}
のような形式で接続し、query
パラメーターがそのまま結果ページに出力される脆弱性を確認します。 - 攻撃者は次に、
vulnerable-site.com/search?query=<script>...悪意のあるコード...</script>
と入力し、結果ページに悪意のあるコードが送信されるリンクを作成します。 - その後、攻撃者はメッセンジャーやコミュニティなどを利用して、悪意のあるコードが送信されるリンクに他のユーザーがアクセスするように誘導します。
- もし、そのサイトの利用者のアクセス・トークンがJavaScriptを通じて取得できる状態であれば、トークンの窃取のリスクにさらされることになります。
クロスサイトリクエストフォージェリ(CSRF)
https://owasp.org/www-community/attacks/csrf クロスサイトリクエストフォージェリ(CSRF)は、エンドユーザーが現在認証されているウェブアプリケーションで望ましくない操作を強制的に実行させる攻撃です。ソーシャルエンジニアリング(例えば、メールやチャットでリンクを送信するなど)の助けを少し借りて、攻撃者はウェブアプリケーションのユーザーを騙して、自分が選んだ操作を実行させることができます。 もし被害者が通常のユーザーであれば、成功した CSRF 攻撃は、ユーザーに資金の移動、メールアドレスの変更など、状態を変更するリクエストを実行させることができます。もし被害者が管理者アカウントであれば、CSRF攻撃はウェブアプリケーション全体を危険にさらす可能性があります。
CSRF攻撃自体はトークンを盗む脅威ではありませんが、XSS攻撃と同様に、攻撃者はウェブサイトの設計上の隙間とユーザーの認証状態を利用して、ユーザーに害を及ぼす行為を強制することができます。これは、トークンの盗難と同じくらい重大な脆弱性です。
- 攻撃者は、ユーザーにフィッシングメールを送信し、
vulnerable-shop.com
のサイトのように見せかけたscam-shop.com
のリンクをクリックさせようとします。 vulerable-shop.com
にログインしていたユーザーがそのリンクをクリックします。scam-shop.com
のページには、以下のようなスクリプトが含まれており、ページが読み込まれると自動的に実行されます:
<form id="form" action="https://vulerable-shop.com/api/purchase" method="POST">
<input type="hidden" name="item_id" value="$expensive_item">
<input type="hidden" name="address" value="$attackes_house">
<input type="hidden" name="amount" value="10000">
<button type="submit">Purchase</button>
</form>
<script>
document.getElementById('form').submit();
</script>
ユーザーがすでにログインしており、適切なセキュリティ設定がなされていない場合、ユーザーは知らぬ間に攻撃者によって高額な商品を 10,000個も購入して送信させられることになります。
CSRFは、クッキーの特性を利用した攻撃です。クッキーは以下のように動作します:
- クッキーはユーザーのブラウザに保存されます。
- クッキーは、そのクッキーに設定されたドメインに対するリクエスト時に常に含まれます。
フロントエンド認証セキュリティ: QueryPieのREDチームと共に行うベストプラクティスと脅威診断
認証の実装は非常に難易度が高いです。特に一般的な機能開発だけでなく、セキュリティもしっかりと考慮しなければなりません。QueryPie はセキュリティソリューションを作成する過程で、フロントエンドチームが認証機能にしっかりとセキュリティを組み込むために多大な努力とリサーチを行いました。認証を実装する過程で、QueryPieのREDチームも重要な役割を果たしました。REDチームは自社のプロダクトを対象にホワイトハッキングを行い、潜在的な脆弱性を発見し、それを改善するために大きな貢献をしました。そのおかげで、より安全な認証システムを構築することができました。
文書の初めに述べたように、フロントエンドでの認証処理はまだ十分に成熟した分野ではありません。インターネット上には認証実装方法に関する様々なガイドがありますが、実際にはセキュリティ的に脆弱なものが多いため、正しい情報を選別するのは簡単ではありません。
QueryPieで実際にフロントエンド開発者が書いたコードにどのような脅威があるのか、いくつかの例を通じてフロントエンドコードにおける脅威を診断し、ベストプラクティスを提案します。この文書を読んだ後は、もうフロントエンドでの認証処理について悩む必要はなくなるでしょう。
例 - 初心者フロントエンド開発者によるSPA認証の実装
以下は、Single Page Application(SPA)でよく見られるログインと認証が必要な API リクエストのコードです。
async function login(id, pw) {
const res = await fetch('<cross_origin_api_url>/api/auth', {
method: 'POST',
body: encrypt({id, pw}),
mode: 'cors'
});
const token = await res.json();
localStorage.setItem('accessToken', token.accessToken);
}
async function getProtectedResource(id) {
const accessToken = localStorage.getItem('accessToken');
const res = fetch(`<cross_origin_api_url>/api/protected-resource/${id}`, {
headers: {
Authorization: `Bearer ${accessToken}`
},
mode: 'cors'
});
}
このコードには以下のような履歴があります:
- バックエンドとのAPI規約:
Authorization
ヘッダーにトークンを含める。Access-Control-Allow-Origin
ヘッダーは開発の便宜上、*
に設定されている。
- トークンの永続性の実装:
localStorage
APIを使用してトークンを保存。
上記のコードと履歴を見た場合、露呈する脅威は以下の通りです。
Authorization Header → XSS攻撃
Authorization
ヘッダーを通じてAccess Tokenを渡す方式はXSS攻撃に脆弱です。Authorization
ヘッダーを通じてトークンを渡すためには、トークンはJavaScriptのメモリやストレージ領域に保存される必要があります。もし攻撃者がXSS攻撃に成功すれば、ページ内のJavaScriptコードにアクセスでき、その結果、トークンやそれを取得できる関数がグローバルスコープに露出していれば、トークンを盗むことができます。
LocalStorage → XSS攻撃
上記のコードでは、localStorage APIを使用して永続性を実現しています。localStorageはブラウザ全体からアクセス可能で、キーさえ分かれば攻撃者がトークンを奪取することができます。したがって、XSS攻撃が成功すれば、localStorageに保存されたトークンは非常に簡単に盗まれることになります。
Access-Control-Allow-Origin: * → セッションハイジャック、CSRF
ブラウザはCross-Originリクエストに対して、Cross-Origin Resource Sharing(CORS)ポリシーを使用して、機密情報を軽々しく送信しないように保護します。しかし、Access-Control-Allow-Origin
ヘッダーを*
に設定すると、すべての外部ドメインからのリクエストを受け入れてしまいます。これにより、セッションハイジャックやCSRF攻撃のリスクが増大します。CORS設定自体が直接的にCSRFの脆弱性に結びつくわけではありませんが、攻撃者が脆弱性を悪用した場合、被害がさらに大きくなる可能性があります。
認証方式の改善方法
フロントエンドでトークンを安全に管理するためには、逆説的に、コードレベルでトークンを管理するのではなく、セキュリティ設定が施されたクッキーを使用して受動的に管理する方が効果的です。これを実現するために、以下の対策を推奨します。
HTTPSベースの通信
今では当然のことかもしれませんが、HTTPSを使用しないと、どんなにコードレベルでセキュリティを強化しても、攻撃者による Man in the Middle(MitM)攻撃に脆弱になります。HTTPSは基本中の基本であり、必ず使用する必要があります。
永続性はWeb Storage → Cookieに変更
Web Storage APIはXSS脆弱性を抱えていますが、機密データ(トークンなど)はCookieを使用してブラウザに保存する方が安全です。Cookie自体にもXSSおよびCSRF脆弱性がありますが、追加の設定を施すことで、これらのリスクを最小限に抑えることができます。
- HTTP Only: JavaScriptからcookieへのアクセスを禁止することで、XSS攻撃によるcookieの盗難を防ぎます。このクッキーは基本的にサーバーが生成します。
- SameSite=Strict: 同一サイトからのリクエストでない限り、cookieをヘッダーに含めません。これによりCSRF攻撃を大幅に軽減できます。もし、特別な理由で同一サイトのAPIリクエストが難しい場合は、少なくともSameSite=Laxに設定します。
- Secure: ブラウザとサーバーが共にHTTPSの環境でのみcookieを送信するように設定します。これによりMitM攻撃を防ぐことができます。
適切なCORSポリシー設定
もしサーバーとオリジンが異なるWebアプリケーションの場合、Access-Control-Allow-Origin
ヘッダーを許可された一部のドメインにのみ動的に割り当てるように設定することがセキュリティに役立ちます。
例えば、people.abc.com
からdefg.com/api
にCORSリクエストを送信する場合、defg.com/api
の開発者は、people.abc.com
からのリクエストに対してのみ値を残すように設定します。
できるだけCORSポリシーを開放するのではなく、WebアプリケーションとAPIエンドポイントが同じドメインを使用し、SameSite環境を作成することがより安全です。
Access-Control-Allow-Origin: people.abc.com
cookieベースでCORSリクエストを通じて認証を行う場合、サーバーは追加のヘッダー設定を行う必要があります。これにより、cookie に保存されたアクセス・トークンをサーバーに送信できます。
Access-Control-Allow-Credentials: true
改善結果
上記のソリューションに基づいて、元のコードはどのように改善されるのでしょうか?
async function login(id, pw) {
await fetch('https://<cross_origin_api_url>/api/auth', {
method: 'POST',
body: encrypt({id, pw}),
mode: 'cors'
});
}
async function getProtectedResource(id) {
const res = fetch(`https://<cross_origin_api_url>/api/protected-resource/${id}`, {
mode: 'cors',
credentials: 'include'
});
}
フロントエンドコードは驚くほどシンプルになりました。フロントエンドで行うべきロジックはほとんどなく、設定だけで完了します。しかし、実際には背後で多くのことが行われていることがわかります。これを分解して、どのように認証が行われるかを理解しましょう。
この図は、例示されたコードでlogin
関数が呼ばれたとき、ブラウザとサーバー間で行われる通信の流れを示しています。
- ブラウザはリクエストを送信する前に、クロスサイトへのリクエストかどうかを検証します。
- クロスサイトであれば、プレフライトリクエストをサーバーに送信します(
OPTIONS
メソッドが使用されます)。 - サーバーは
OPTIONS
メソッドリクエストに対して、許可されたドメインかどうかを判断し、クロスサイトリクエストを許可するヘッダー(Access-Control-Allow-Origin
、Access-Control-Allow-Credentials
)を送信します。 - ブラウザはプレフライトレスポンスを確認し、サーバーが適切なヘッダーを送信した場合、元のリクエストを再度送信します。
- サーバーはリクエストの認証を確認し、cookieにhttpOnly、Secure、SameSite=Laxを設定します。
- ブラウザはそのcookieをブラウザに保存します。
- ブラウザ - 対象リソースサーバーにCORSプレフライトリクエストを送信します。これはサーバーがCORSヘッダーをどう設定しているかを検証する手順です。
- サーバー - 対象リソースへのCORSプレフライトリクエストが来ると、
Access-Control-Allow-Origin
ヘッダーを適切に返すかどうかを決定します。 - ブラウザ - サーバーがCORSヘッダーを正しく設定していれば、実際のリクエストを送信します。credentialsフィールドがincludeに設定されている場合、
Access-Control-Allow-Credentials: true
かを確認し、cookieをリクエストヘッダーに含めて送信します。 - サーバー - リクエストに含まれたcookieからtokenを取り出し、内部で認証を行います。ログインしていない場合やフロントエンドでセキュリティ設定が適切に行われていない場合、cookieは存在しないため、リソースへのアクセスをブロックできます。
結果として、XSSやCSRFをかなり防御できる形となり、セキュリティレベルも向上しました。
さらに改善する
私たちのフロントエンドチームも、従来のWeb Storageベースのトークン管理方式からCookie方式に移行することで、セキュリティレベルを大幅に改善し、認証ロジックも簡素化することができました。ただし、セキュリティの分野では常に完璧な方法は存在しないため、結果的にトークンが盗まれても被害を最小限に抑えるための対策を設定する必要があります。
トークンローテーション
トークンの有効期限を短くし、頻繁にトークンを更新することです。これをトークンローテーションと呼びます。
有効期限が長いアクセストークンは、一度盗まれると攻撃者がシステムに与える影響が大きくなります。したがって、トークンが盗まれた場合でも、すぐにそのトークンの権限が無効になるように、アクセストークンに短い寿命を与える必要があります。
リフレッシュトークン
トークンの有効期限が短いと、一般的なユーザーは頻繁にログインする必要があり、不便を感じることがあります。これを防ぐために、実際の認証と認可に使用されるアクセストークンの時間は短く設定し、その一方でアクセストークンの再発行のためにリフレッシュトークンを利用します。ユーザーは、リフレッシュトークンが有効な限り、再度ログインせずにトークンローテーションを実現することができます。
安全なWebサービスのためのフロントエンドトークン認証実装戦略
最終的に、Webセキュリティの基本原則を遵守しながら、安全なフロントエンドトークン認証方式を実装することは非常に重要です。複雑さを減らし、重要なセキュリティ処理をサーバー側で実行することは、攻撃者の攻撃対象面を減らすのに効果的です。フロントエンドでのトークン管理は、Cookieベースの受動的なセキュリティアプローチによって強化でき、ブラウザのセキュリティポリシーを活用して認証プロセスを安全に処理できます。
また、トークンの盗難に備えて、トークンの寿命を短く設定し、定期的に更新するトークンローテーション戦略を導入することで、セキュリティを高めると同時にユーザー体験も改善することができます。このようなアプローチを通じて、QueryPieが提案する安全なフロントエンドトークン認証を実現し、より安全なWebサービスを提供できるようになります。
参考文献
気になりますか?
魔法を明かしましょう!
限定コンテンツをアンロックするには、フォームにご記入ください!