paint-brush
認証に UUID を決して使用しないでください: 生成の脆弱性とベスト プラクティス@mochalov
2,066 測定値
2,066 測定値

認証に UUID を決して使用しないでください: 生成の脆弱性とベスト プラクティス

Ivan Mochalov8m2024/05/01
Read on Terminal Reader

長すぎる; 読むには

認証、脆弱性の発見、安全な実装戦略のために UUID を使用する際のリスクとベスト プラクティス。
featured image - 認証に UUID を決して使用しないでください: 生成の脆弱性とベスト プラクティス
Ivan Mochalov HackerNoon profile picture
0-item

認証用のUUID

今では、深いフラストレーションを感じながら「パスワードを回復」ボタンをクリックしたことがない人はほとんどいません。パスワードが間違いなく正しいように見えても、パスワード回復の次のステップは、電子メールのリンクにアクセスして新しいパスワードを入力することで、ほとんどの場合スムーズに進みました (誰かを騙さないでください。ステップ 1 で、不快なボタンを押す前にすでに 3 回入力しているので、新しいパスワードではありません)。


ただし、電子メール リンクの背後にあるロジックは、その生成を安全でないままにしておくと、ユーザー アカウントへの不正アクセスに関する脆弱性が大量に発生するため、十分に精査する必要があります。残念ながら、多くの人が遭遇する可能性のある UUID ベースのリカバリ URL 構造の例を次に示します。これは、セキュリティ ガイドラインに準拠していません。


 https://.../recover/d17ff6da-f5bf-11ee-9ce2-35a784c01695


このようなリンクが使用されている場合、一般的には誰でもパスワードを入手できることを意味します。これは単純なことです。この記事では、UUID 生成方法について詳しく説明し、その適用に対する安全でないアプローチを選択することを目的としています。

UUIDとは何か

UUID は、十分に複雑で、十分に一意であるという 2 つの重要な属性を持つ疑似ランダム識別子を生成する際によく使用される 128 ビットのラベルです。これらは、バックエンドから ID が出てフロントエンドでユーザーに明示的に表示されるか、または一般的に API 経由で送信され、監視可能な状態になるための重要な要件です。ID = 123 (複雑さ) と比較して推測やブルート フォース攻撃が困難になり、生成された ID が以前に使用された ID (例: 0 から 1000 までのランダムな数字) と重複した場合の衝突を防止します (一意性)。


「十分な」部分は、実際には、まず、Universally Unique IDentifier のいくつかのバージョンから来ており、重複のわずかな可能性を残していますが、これは追加の比較ロジックによって簡単に軽減され、発生条件がほとんど制御されていないため脅威にはなりません。次に、さまざまな UUID バージョンの複雑さの取り方は記事で説明されており、一般に、さらなるコーナーケースを除いて非常に良好であると想定されています。

バックエンドの実装

データベース テーブルの主キーは、UUID と同様に複雑で一意であるという原則に基づいているようです。多くのプログラミング言語やデータベース管理システムでその生成に組み込みの方法が広く採用されているため、UUID は保存されているデータ エントリを識別するための第一の選択肢として、また一般的なテーブルや正規化によって分割されたサブテーブルを結合するためのフィールドとしてよく使用されます。特定のアクションに応答してデータベースから取得したユーザー ID を API 経由で送信することも、一時的な ID を追加生成せずにデータ フローを統合するプロセスを簡素化し、実稼働データ ストレージの ID にリンクするための一般的な方法です。


パスワード リセットの例で言えば、アーキテクチャには、ユーザーがボタンをクリックするたびに生成された UUID を含むデータ行を挿入する操作を担当するテーブルが含まれる可能性が高くなります。このテーブルは、user_id によってユーザーに関連付けられたアドレスに電子メールを送信し、リセット リンクが開かれると、そのユーザーが持つ識別子に基づいてどのユーザーのパスワードをリセットするかをチェックすることで、回復プロセスを開始します。ただし、ユーザーに表示されるこのような識別子にはセキュリティ ガイドラインがあり、UUID の特定の実装は、さまざまな成功度でそれらを満たしています。

古いバージョン

UUID 生成のバージョン 1 では、128 ビットを、識別子を生成するデバイスの 48 ビットの MAC アドレス、60 ビットのタイムスタンプ、増分値用に 14 ビット、バージョン管理用に 6 ビットに分割します。これにより、一意性の保証は、コード ロジックのルールから、生産中のすべての新しいマシンに値を正しく割り当てることになっているハードウェア メーカーに移行されます。有用な変更可能なペイロードを表すために 60+14 ビットのみを残すと、特にその背後にこのような透明なロジックがある場合は、識別子の整合性が低下します。次に、結果として生成された UUID v1 の番号のシーケンスを見てみましょう。


 from uuid import uuid1 for _ in range(8):    print(uuid1())
 d17ff6da-f5bf-11ee-9ce2-35a784c01695 d17ff6db-f5bf-11ee-9ce2-35a784c01695 d17ff6dc-f5bf-11ee-9ce2-35a784c01695 d17ff6dd-f5bf-11ee-9ce2-35a784c01695 d17ff6de-f5bf-11ee-9ce2-35a784c01695 d17ff6df-f5bf-11ee-9ce2-35a784c01695 d17ff6e0-f5bf-11ee-9ce2-35a784c01695 d17ff6e1-f5bf-11ee-9ce2-35a784c01695



ご覧のとおり、「-f5bf-11ee-9ce2-35a784c01695」の部分は常に同じです。変更可能な部分は、シーケンス 3514824410 - 3514824417 の 16 ビット 16 進表現です。これは表面的な例です。通常、生産値は、間にかなりの時間差をおいて生成されるため、タイムスタンプ関連の部分も変更されます。60 ビットのタイムスタンプ部分は、ID のより大きなサンプルで、識別子のより重要な部分が視覚的に変更されることも意味します。核心は同じです。UUIDv1 は、一見ランダムに見えても簡単に推測できます。


指定された 8 つの ID のリストから最初と最後の値だけを取得します。識別子は厳密に生成されるため、結果として、指定された 2 つの ID の間には 6 つの ID しか生成されないことは明らかです (16 進数で変更可能な部分を減算することにより)。また、それらの値も確実に見つけることができます。このようなロジックの外挿は、これら 2 つの境界値を知ることで UUID をブルート フォースで調べようとする、いわゆるサンドイッチ攻撃の根底にある部分です。攻撃の流れは単純です。ユーザーは、ターゲット UUID の生成前に UUID A を生成し、その直後に UUID B を生成します。静的な 48 ビット MAC 部分を持つ同じデバイスが 3 つの生成すべてを担当していると仮定すると、ターゲット UUID が配置されている A と B の間の潜在的な ID のシーケンスがユーザーに対して設定されます。生成された ID とターゲット間の時間的近接性に応じて、範囲はブルート フォース アプローチにアクセスできるボリュームになる可能性があります。つまり、すべての可能性のある UUID をチェックして、空の中から既存のものを見つけます。


前述のパスワード回復エンドポイントを使用した API リクエストでは、既存の URL を示す応答が見つかるまで、結果的に UUID を含む数百または数千のリクエストが送信されることになります。パスワード リセットでは、ユーザーが可能な限り近くで制御する 2 つのアカウントに回復リンクを生成し、アクセス権はないがメール/ログインのみを知っているターゲット アカウントの回復ボタンを押すことができる設定になります。回復 UUID A および B を持つ制御アカウントへの文字がわかるため、実際のリセット メールにアクセスしなくても、ターゲット アカウントのパスワードを回復するためのターゲット リンクをブルート フォース攻撃で取得できます。


脆弱性は、ユーザー認証に UUIDv1 のみを使用するという概念から生じています。パスワードのリセットへのアクセスを許可するリカバリ リンクを送信することで、ユーザーはリンクをたどることで、リンクを受信するはずだったユーザーとして認証されると想定されます。これは、UUIDv1 が単純なブルート フォース攻撃にさらされているために認証ルールが失敗する部分です。これは、隣のドアの両方の鍵の外観を知っていれば誰かのドアを開けることができるのと同じです。

暗号的に安全でない関数

UUID の最初のバージョンは、主にレガシーと見なされています。その理由の 1 つは、生成ロジックが識別子サイズのより小さな部分のみをランダム値として使用するためです。v4 などの他のバージョンでは、バージョン管理用のスペースをできるだけ少なくし、最大 122 ビットをランダム ペイロードとして残すことで、この問題を解決しようとしています。一般に、可能なバリエーションの合計は2^122となり、これは現時点では識別子の一意性要件に関する「十分な」部分を満たし、セキュリティ標準を満たしていると考えられています。生成の実装によってランダム部分に残されるビットが何らかの理由で大幅に減少すると、ブルート フォース攻撃の脆弱性が生じる可能性があります。しかし、製品ツールやライブラリがない場合、そうなるのでしょうか?


暗号化について少し触れて、JavaScript の UUID 生成の一般的な実装を詳しく見てみましょう。以下は、疑似乱数生成にmath.randomモジュールを使用するrandomUUID()関数です。

 Math.floor(Math.random()*0x10);


そして、ランダム関数自体は、簡単に言えば、この記事のトピックの興味深い部分です。

 hi = 36969 * (hi & 0xFFFF) + (hi >> 16); lo = 18273 * (lo & 0xFFFF) + (lo >> 16); return ((hi << 16) + (lo & 0xFFFF)) / Math.pow(2, 32);


疑似乱数生成には、十分にランダムな数値のシーケンスを生成するために、その上で数学的演算を実行するためのベースとしてシード値が必要です。このような関数はシード値のみに基づいているため、以前と同じシードで再初期化すると、出力シーケンスが一致することになります。問題の JavaScript 関数のシード値は、変数 hi と lo で構成され、それぞれが 32 ビットの符号なし整数 (10 進数で 0 から 4294967295) です。暗号化には両方の組み合わせが必要であり、大きな数値の因数分解の複雑さに依存するため、2 つの初期値の倍数を知っても、その逆数を決定することはほぼ不可能です。


2 つの 32 ビット整数を組み合わせると、UUID を生成する初期化関数の背後にある hi 変数と lo 変数を推測する2^64通りのケースが考えられます。hi 値と lo 値が何らかの方法でわかっている場合、生成関数を複製して、シード値の露出によりそれが生成する値と将来生成される値をすべて知るのに手間はかかりません。ただし、セキュリティ標準の 64 ビットは、意味を成すには測定可能な期間でブルート フォース攻撃に耐えられないと考えられます。いつものように、この問題は特定の実装に起因します。Math.random Math.random()は、hi と lo のそれぞれからさまざまな 16 ビットを取り出して 32 ビットの結果にしますが、その上のrandomUUID() .floor()操作により値をもう一度シフトし、意味のある部分は突然 hi からのみ取得されるようになります。これは生成にはまったく影響しませんが、生成関数シード全体の可能な組み合わせが2^32しか残らないため、暗号化アプローチが崩壊します (lo は任意の値に設定でき、出力に影響を与えないため、hi と lo の両方をブルートフォースする必要はありません)。


ブルートフォース フローは、単一の ID を取得し、それを生成した可能性のある高い値をテストすることから構成されます。ある程度の最適化と平均的なラップトップ ハードウェアを使用すれば、数分しかかからず、Sandwich 攻撃のようにサーバーに大量のリクエストを送信する必要はなく、すべての操作をオフラインで実行できます。このようなアプローチの結果、バックエンドで使用される生成関数の状態が複製され、パスワード回復例で作成されたリンクと将来のリセット リンクがすべて取得されます。脆弱性の発生を防ぐ手順は簡単で、 crypto.randomUUID()などの暗号的に安全な関数の使用が推奨されます。

まとめ

UUID は素晴らしい概念であり、多くのアプリケーション分野でデータ エンジニアの作業を大幅に楽にします。ただし、認証に関連して使用すべきではありません。この記事では、UUID の生成手法の特定のケースにおける欠陥が明らかにされています。もちろん、すべての UUID が安全ではないという考え方にはなりません。ただし、基本的なアプローチは、セキュリティのために UUID をまったく使用しないように人々を説得することです。これは、そのような目的で UUID を使用するか、または UUID を生成しない方法に関する複雑な制限をドキュメントに設定するよりも効率的で、安全です。