MySQL uuid()活用記 - Auto Increment PKをURLに露出してはいけない理由
Auto Increment PKをURLに露出してはいけない理由
よくあるパターン
ほとんどのWebサービスで、こういうURLを見たことがあるはずです。
/users/1
/boards/152
/orders/30421
DBテーブルのauto_increment値をそのままURLに使うパターン。開発する時はこれが一番楽です。PK値一つで検索すれば終わりですから。
でもセキュリティレポートが来ると、ほぼ毎回この部分が指摘されます。
何が問題なのか
連番がURLに露出すると、こんな問題が発生します。
mb_no=1なら「このサイトの最初の会員だな」という情報が漏れるorder_id=30421なら「このサイトの注文件数は約3万件だな」ということも分かる- URLの数字を変えながら他人のデータへのアクセスを試みることが可能(IDOR脆弱性)
もちろんサーバー側で権限チェックを適切に行えば、実際にデータが漏洩することはありません。でもセキュリティ監査では「不必要な情報露出」として指摘の対象になります。
ではUUIDを使おう…でも
MySQLにはuuid()関数があります。これで生成すると550e8400-e29b-41d4-a716-446655440000のような値が出てきます。
SELECT uuid();
-- 550e8400-e29b-41d4-a716-446655440000
じゃあPKを全部UUIDに変えればいいのでは?と思うかもしれませんが、現実はそう簡単ではありません。
UUIDをPKにすると起きる問題:
- インデックス性能低下:36バイトの文字列 vs 4バイトの整数。比較演算自体が遅くなります
- クラスタードインデックスの問題:InnoDBではPKがクラスタードインデックスですが、UUIDはランダムなのでINSERTのたびにページ分割が発生します
- ストレージの浪費:JOINが多いテーブルほどFKまで全部UUIDになるので、容量の無駄が大きくなります
- デバッグの不便さ:
WHERE id = 1vsWHERE id = '550e8400-e29b-41d4-a716-446655440000'…言うまでもないですね
PKを全部UUIDに変えるのは非効率的すぎます。
現実的な妥協案
私が使っている方式はこうです。
PKはauto_incrementのまま残して、URLに露出する部分だけ別途UUIDカラムを追加する。
CREATE TABLE members (
mb_no INT AUTO_INCREMENT PRIMARY KEY,
mb_uuid CHAR(36) DEFAULT (uuid()),
mb_name VARCHAR(50),
-- ...
INDEX idx_uuid (mb_uuid)
);
内部的にJOINや検索をする時はmb_noを使い、外部に露出するURLではmb_uuidを使います。
-- 内部クエリ
SELECT * FROM members WHERE mb_no = 1;
-- URLからのアクセス
SELECT * FROM members WHERE mb_uuid = '550e8400-...';
これならパフォーマンスもセキュリティも両立できます。
必ずしもMySQLのuuid()でなくても良い
UUID生成を必ずMySQLで行う必要はありません。アプリケーションレベルで生成しても構いません。
PHPならuniqid()関数がありますし:
$unique_id = uniqid('', true);
// 例:5e6f7a8b9c0d1.12345678
より安全にしたいならrandom_bytes()で生成もできます:
$uuid = bin2hex(random_bytes(16));
最近はほとんどの言語でUUIDライブラリが提供されているので、状況に合わせて使えば良いです。
まとめ
結論はシンプルです。
- auto_incrementのPKは内部でのみ使う
- URLに露出する識別子は別途UUIDカラムで分離する
- PKを全部UUIDに変えるのは非効率的なのでやめておく
セキュリティレポートで毎回指摘されるストレスも、これでスッキリ解決できます。最初からこう設計しておけば良いのですが…すでに稼働中のサービスに適用するとなると、またマイグレーションの問題が出てきます。
結局「そのうちやろう」リストにまた一つ追加されるのです。