CHAPTER 04

第4章 認証と認可 — 本人確認とできること管理

「あなたは誰か」と「あなたは何をしていいか」は別の問い。混同が権限事故になる。パスワードのハッシュ化・fail-closed・OAuthの紐付き検証・毎リクエスト認可までを耳で吸収する

この章で分かること: 「あなたは誰か」を確かめる認証と、「あなたは何をしていいか」を決める認可。この二つは別の問いで、混同すると権限事故になる。その分け方と、パスワードの守り方、繋がった相手の確かめ方までを聴いて吸収する。

聴く台本

セキュリティの入り口には、よく似ているのに全然別物の言葉が二つある。認証と認可だ。英語だとオーセンティケーションとオーソライゼーション。頭の三文字が同じで紛らわしいんだけど、問うていることが違う。認証は「あなたは誰か」を確かめること。認可は「あなたは何をしていいか」を決めること。本人確認と、できること管理。この二つだ。

まず押さえたいのは、認証が通ったからといって、何でもしていいわけじゃない、ということ。会社のビルを思い浮かべるといい。入り口で社員証をかざして本人確認するのが認証だ。でも、社員証を持っているからって、サーバー室にも社長室にも入れるわけじゃないよね。どの部屋に入れるかは別の管理になっている。それが認可だ。本人だと分かったことと、その人にやらせていいこと。ここを一緒くたにすると事故になる。「ログインできた人は全部できる」みたいな作りにすると、一般ユーザーが管理者の操作までできてしまう。これが権限の事故、いわゆる権限昇格ってやつだ。

他社の機密データをAIに預かる事業だと、ここはとくに効いてくる。預けてくれた会社の社長は、自分の会社の全データを見ていい。でも、その会社の一般社員は一部しか見ちゃいけない。そして、システムを運用しているこっち側、ベンダーは、顧客の中身を一切見ちゃいけない。同じ「ログイン済み」でも、できることが三段で違う。認証は「誰か」までしか答えない。「何をしていいか」は、認可で別に決める。だから設計の最初から、この二つを別の問いとして分けて考える。

じゃあ認証、つまり本人確認から見ていこう。本人確認の王道はパスワードだ。ここで絶対にやっちゃいけないのが、パスワードをそのままの文字で保存することだ。そのままの文字を平文って言う。なぜダメか。データベースがもし盗まれたら、全員のパスワードがそっくりそのまま読めてしまう。しかも人は同じパスワードを使い回すから、漏れた瞬間に他のサービスまで連鎖でやられる。

だから、パスワードはハッシュ化して保存する。ハッシュっていうのは、元の文字を一方向の計算で、まったく別の文字列に潰す処理だ。一方向っていうのがミソで、計算した結果から元のパスワードを逆算できない。保存するのはこの潰した結果だけ。ログインのときは、入力されたパスワードを同じやり方で潰して、保存してある潰し結果と一致するか見る。これなら、データベースが盗まれても元のパスワードは出てこない。

ところがここで初学者がよくやる失敗がある。ハッシュって言葉を聞いて、MD5とかSHA-256みたいな計算をそのまま使ってしまうんだ。これはダメだ。理由が二つある。

ひとつめ。MD5やSHAは、同じパスワードを入れたら、いつも同じ結果になる。ということは、「よくあるパスワードを潰した結果」の一覧表をあらかじめ作っておけば、盗んだデータと照らし合わせるだけで元が分かる。この一覧表を使った逆引き攻撃を防ぐために、ソルトを混ぜる。ソルトっていうのは、利用者ごとにバラバラのランダムな文字列で、パスワードにくっつけてから潰すんだ。こうすると、同じパスワードでも人によって潰し結果が変わる。だから一覧表が一気に役立たずになる。

ふたつめ。MD5やSHAは、もともと速く計算できるように作られている。速いのは普段はいいことなんだけど、パスワードを破ろうとする攻撃者にとっても速いってことだ。一秒間に何億通りも試せてしまう。だから、わざと計算を重く、遅くする。一回潰すのに何千回も内部で計算をくり返させる。攻撃者が総当たりで試すのが現実的に割に合わなくなる。この「ソルトを混ぜる」「わざと重くする」を最初から組み込んだ専用の道具が、bcryptやargon2だ。だから実装では、ハッシュと聞いてSHAを呼ぶんじゃなく、パスワード専用のbcryptやargon2を使う。これは丸暗記でいい一線だ。

さて、本人確認が一回通ったら、毎回パスワードを聞き直すわけにはいかない。そこで、確認済みだという証を発行する。それがセッションやトークンだ。入り口で本人確認したあとに渡す「入館証」みたいなものだね。以降はその入館証を見せれば、本人だと分かる。だからこの入館証の扱いが甘いと、盗まれて、なりすましに使われる。盗み見られない経路で送る、有効期限を切る、おかしな使われ方をしたら無効にする。入館証は本人そのものと同じ価値がある、という意識で扱う。

ここで一回、第1章でも出たフェイルクローズドの考え方を思い出してほしい。フェイルクローズドっていうのは、異常が起きたとき、処理を続ける側じゃなく、止める側に倒す設計のことだ。認証でこれが効く場面がある。たとえば、本人確認のために問い合わせている外部のサービスが、たまたま落ちていて返事が来なかったとする。このとき、「返事が来ないからとりあえず通しちゃえ」とやると、確認できていない相手を中に入れてしまう。これが事故だ。正しくは逆で、確認が取れないなら入れない。迷ったら閉じる。認証が確定しないなら、拒否側に倒す。これがフェイルクローズドだ。

もうひとつ、外部サービスと繋ぐときの落とし穴を話しておきたい。最近は、グーグルとかのアカウントでログインする仕組み、OAuthをよく使う。これを実装すると、画面の上では「連携できました」と緑のチェックが出る。ここで安心しがちなんだけど、よく考えてほしい。「繋がった」ことと、「正しい相手と繋がった」ことは別の話なんだ。連携の手続きが成功したのは事実でも、その結果として手に入れた入館証が、本当に意図したこの顧客のものなのか、それとも別の誰かのものとすり替わっていないか。そこまでは画面の緑チェックは保証してくれない。

だから、繋がったあとに一段、紐付きの検証をする。手に入れた入館証を使って、相手のサービスに「このトークンは、本当に誰のものか」を問い合わせて、自分が想定している相手と一致するかを実物で照らし合わせる。一致しなければ、繋がっていても拒否する。第1章でデータベースの繋ぎ先を実物と照合した話と、構造はまったく同じだ。UIの「できました」を信じない。APIで実体を確かめる。片側の合図だけで完了と判断しないってことだね。

最後に、認可、できること管理のいちばん大事な原則だ。それは、認可は毎リクエストごとに確かめる、ということ。やりがちなのが、最初のログインのときだけ「この人は管理者だな」と判定して、あとはそれを信用し続ける作りだ。これが危ない。具体例を出そう。ある顧客のデータを見る画面で、見るデータの番号がアドレスに入っているとする。たとえば「顧客の二十三番のデータ」みたいに。このとき、ログイン済みだからと安心して、番号のチェックを省くと何が起きるか。利用者が、その番号を別の顧客の番号に書き換えただけで、他人のデータが見えてしまう。これがIDOR、日本語で言うと「番号を直接いじって他人のものを覗く」攻撃だ。

なぜ起きるか。一回認証を通したら、そのあとの一つひとつの操作については、「この人にこのデータを触る権利があるか」を確かめていなかったからだ。本人確認は済んでいる。でも、いま要求されているこの操作について、認可していない。だから対策はシンプルで、データに触る操作のたびに毎回、「いま操作しようとしている人は、このデータの持ち主か」を照らし合わせる。一回通したから信用、をやめる。リクエストのたびに認可を取り直す。

で、実装ではどうするか。三つに絞る。まず、パスワードはbcryptかargon2でハッシュ化して保存する。ハッシュと聞いてSHAやMD5を呼ばない。ソルトと計算コストが最初から入った専用の道具を使う。次に、外部連携は「繋がった」で完了にせず、手に入れた入館証が想定した相手のものかをAPIで実物照合する。一致しなければ拒否、確認が取れなければ拒否、というフェイルクローズドに倒す。そして、認可は毎リクエストごとに、データの持ち主かどうかを確かめる。ログイン直後の一回だけで判定して、あとは信用し続ける作りにしない。この三つを守れば、認証と認可の取り違えから来る事故は、ほとんど防げる。


この章のまとめ(実装で守る一線)

認証(あなたは誰か)と認可(あなたは何をしていいか)は別の問いとして設計し、「ログインできた人は何でもできる」を作らない。パスワードはSHA/MD5でなくbcrypt/argon2でハッシュ化する(ソルトと計算コストが最初から入っているから)。OAuthは「繋がった」で終わりにせず、手に入れたトークンが想定した相手のものかをAPIで実物照合し、確認が取れなければ拒否側に倒す(fail-closed)。そして認可は毎リクエストごとにデータの持ち主かを確かめ、一度通したら信用、をやめる(IDOR・権限昇格を防ぐ)。