はじめに
皆様、こんにちは!
佐久間まゆちゃんのプロデューサーの@hiroki_tanakaです。
私は現在、RailsシステムのAPIリプレイス活動に携わっています。
その際にエンドポイントにIDを使用するかどうかやレスポンスにIDフィールドを含めるか含めないかといったID問題があったので、考えたこと・調べたことをまとめたいと思います。
結論
エンドポイントやレスポンスにIDを使用しないほうが良い。
理由
Railsにおいて、IDカラムは基本的にDBでAuto Incrementされて発行された連番のサロゲートキーに当たります。
そのため、サロゲートキーであるIDカラムが露出してしまうことで以下のような問題が発生します。
IDの値の大小によって、サービス規模が推測出来てしまう。
例えば、Get /api/users/:id
といったIDに紐づくユーザ情報を取得するAPIが存在した場合、ユーザは:id
に好きな値を入れることができます。
そのためGet /api/users/10000
まではユーザ情報が取得出来ていたが、Get /api/users/10001
以降の値では全くユーザ情報が返ってこなくなった場合、そのサービスのユーザ数は1万人前後ということが推測出来てしまいます。
エンドポイントだけでなく、レスポンスに含まれている場合でも推測可能です。
自社のサービス規模は企業にとって重要な情報の1つです。それが簡単に第三者にわかってしまう状況は害ことあっても利がありません。
(虚偽広告をするのは以ての外ですが、もし「会員登録者数10万人突破!」を宣伝で謳っていたのに、IDを調べてみたら実際は1万人前後しかいなければ確実に炎上してしまいます。)
また、IDの大小とは直接関係ないですがユーザIDのような個人情報に紐付いているIDが推測可能になってしまうのはセキュリティの観点でもNGです。
IDはインクリメントする値のため、スクレイピングやアタックが容易になってしまう。
例えば、書評サイトを運営していて/api/books/:id
をいうエンドポイントで様々な本の書評を取得している場合、下記のように:id
を連番にするだけで簡単にスクレイピングでき、情報を抜き取る事ができます。
Get /api/books/100
Get /api/books/101
Get /api/books/102
・
・
・
Get /api/books/999
urlが予測しやすくスクレイピングが容易な状態だと攻撃者や競合相手に情報を簡単に渡すリスクが非常に大きいだけでなく、攻撃によるセキュリティリスクも高いです。
上記の例だと仮に/api/books/100
で攻撃に成功した場合、他のurlにも攻撃を繰り返すには簡単なfor文やwhile文で実現することが出来てしまうので、リスクが大きいです。
処理によってはIDにintegerの最大値を使用した場合、次のデータを作成する際にAuto Incrementでエラーとなってしまう可能性がある。
例えば、Getで連携されたIDが存在しない場合はそのIDのデータを作成して、後続処理を続けるという処理があったとします。
(find_or_initialize_by
やfind_or_create_by
が使用されるイメージです。)
その際に、いたずら心を持ったユーザがMySQLのintegerの最大値である2147483647を使用してGet /api/hoge/2147483647
を行った場合、ID:2147483647のデータが作成されます。
IDはAuto Incrementで発番されるので、次にこのテーブルのデータを作成しようとした時にIDはintegerの最大値+1となってしまい、MySQLでエラーが発生します。
このように開発者の予期せぬ所でエラーの発生原因を作ってしまいます。
では、どうするべきなのか
下記のようにIDではなく、システム内でランダム文字列として生成するkeyやcodeをエンドポイントやレスポンスに使用します。
IDの露出を極力抑えることでシステムの安全性を高める事ができます。
Get /api/piyo/abcd12345
もしくは、セキュアなuuidを使用する形でも問題ありません。
require 'securerandom'
p SecureRandom.uuid
#=> ad54c1ed-0ac7-47c4-8a4a-7f63fbbf9e6e
# Get /api/piyo/ad54c1ed-0ac7-47c4-8a4a-7f63fbbf9e6e
おわりに
これまで何気なくエンドポイントにIDを使用し、レスポンスにIDカラムを含めていましたがこれを機に見直していきたいです。