Firestoreでなぜセキュリティルールが必要か
JavaScriptでユーザやデータの検証をすれば十分では?
みなさん、Firestoreのセキュリティルールは書いていますか?書くのは結構面倒くさいですね。そもそも、JavaScript側で、あるユーザが他のユーザのデータを操作できないようにしてあるし、ユーザの入力値の検証もしているのに、どうしてセキュリティルールが必要なんでしたっけ?
Firestoreは、あなたが書いたJavaScriptからリクエストが送られたかかどうか気にしない
専門用語でステートレス通信というのですがそれは置いておいて、表題の通り、たとえFirebaseのHostingを利用してJavaScriptを配信していたとしても、そのJavaScriptがFirebaseにアクセスするとき、Firestoreはあなたが書いたJavascriptからリクエストが送られているか気にしません。
言い換えれば、あなたが書いたJavaScript以外からリクエストが送られてきてもFirestoreは全く意に介さずリクエストを処理します。
つまりは、悪意のあるユーザが書いたJavaScriptから送られたリクエストでも、Firestoreは受け入れてしまい、結果読み取られると思っていないデータが読み取られてしまったり、あなたが意図しない状態にDBがなってしまうことがあります。
リクエストに必要なのはDBの接続先情報
Firestoreにリクエストを送るのに必要なのはDBの接続先情報のみです。これさえ手に入ればFirestoreにアクセスし放題です。
// firebaseConfigにDBの接続先情報が含まれている
const firebaseConfig = {
apiKey: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
authDomain: "sample-abcd.firebaseapp.com",
projectId: "sample-abcd",
storageBucket: "sample-abcd.appspot.com",
messagingSenderId: "1234567890",
appId: "1:9999999999999:web:999999999999999999"
}
const app = initializeApp(firebaseConfig);
接続情報を隠したいですね。無理ですけど。
接続情報は公開されている
FireabseのHostingを利用している場合、/__/firebase/init.js
というパスでappIdなどが公開されています。悪意のあるなしに関わらず簡単に入手可能です。
https://${your_app_name}.web.app/__/firebase/init.js
ソースコードを見ればわかる
FirebaseのHostingを利用していない場合でも、JavaScriptのソースコード中に接続先情報は書かれているので、ソースコードを検索すれば接続情報を取得することができます。
暗号化すればOK?
接続先情報を暗号化する案も思い浮かぶかもしれませが、復号ロジックもユーザ側に送られてしまうので、無駄ですね。
ネットワーク通信をみればわかる
そもそもGoogle Chromeのデベロッパーツールを使用すればDBの接続先とappIDはすぐに取れてしまいます。
サービスが小さいから攻撃されない?
サービスが小さいし、無名だから攻撃の対象にはならない、と考えるかもしれません。重要な情報をDBに保存しているわけでもないので盗んでもしょうがないと。
こちらはFirestoreの事例ではありませんが、データを抜き取られた後データをすべて削除され、金銭を要求されています。サービスを公開して間もないころのことだったようです。
筆者の周りでも(Firestoreの事例ではないものの)サービス公開直後にハッキングされ、リソースを不正利用される事例がありました。
このことから、サービスが無名でも攻撃の対象になることは普通にあるということがわかります。むしろサービスの公開直後が一番狙われている気さえします。
JavaScriptと同じ入力値チェックをFirestoreのセキュリティルールに設定する
ここまででセキュリティルールの必要性がわかっていただけたかと思います。
セキュリティルールの設定のコツは、アプリの挙動をセキュリティルールにそのまま落とし込むことです。
他のユーザのデータを操作できないようにするなら、
allow create: if request.auth.uid == request.resource.data.author_uid;
allow update: if request.auth.uid == resource.data.author_uid;
allow delete: if request.auth.uid == resource.data.author_uid;
少なくともこのような設定は入ります。
また、例えばユーザ入力として"name"(氏名)を受け取る場合は、以下ように文字列であることを保証するだけでは不十分です。
allow create, update: request.resource.data.name is string
少なくとも長さは指定しましょう。
allow create, update: request.resource.data.name.size() <= 64
size()
のほかにも文字列が正規表現とマッチするか検証するmatches(regex)
関数も用意されているので積極的に活用してください。
// some_statusが"TODO"か"IN_PROGRESS"か"DONE"に制限される
allow create, update: request.resource.data.some_status.matches("^(TODO|IN_PROGRESS|DONE)$")
また、DBに登録されるのが意図したフィールドのみに制限する設定も入れる必要があります。
allow create: if (request.resource.data.keys().hasOnly(
['name', 'address', 'tel', 'email']))
Updateできる項目を制限することも可能です。
allow update: if (request.resource.data.diff(resource.data).affectedKeys()
.hasOnly(['name', 'address']))
他にも重要なセキュリティルールがありますが長くなるので公式ページにリンクを張るにとどめます。
同じことをセキュリティルールとJavaScript両方に書かなくてはいけなくて面倒ですね。今のところ自前でなにかツールを用意するしか解決策はありません。将来的によい解決策が出てくるとよいですが。
JavaScriptでの入力値チェックはユーザの利便性のため
JavaScriptでの入力値のチェックはセキュリティのためではなくユーザの利便性のためと心得ましょう。この原則はサーバサイドがFirestoreの場合だけでなく、すべてのwebアプリに共通します。
逆に、セキュリティルールがあればJavaScriptで入力値をチェックしなくてよい?
残念ながら、Firestore側のセキュリティルールだけでは不十分といえます。理由はセキュリティルールではじかれた場合、どのルールに違反しているか分からないためです。ユーザはどのフィールドがどのような理由で登録できなかったか分からず、ユーザ体験を大きく損ないます。
セキュリティルールに違反したリクエストを送るとエラーになるが、どのルールに違反したか記載されていない。
App Check
最後に、2021年5月に発表されたApp Checkという機能を紹介します。2021年10月現在、Firestoreには未対応ですが、この機能を使えば、あなたが作成したアプリからのリクエストのみFiresbaseで処理させることができます。
終わりに
セキュリティルールの必要性に言及している記事が少ないため、常識的な話かな?と思いつつ書いてみました。
自分の場合は幸いにもこの辺のセキュリティ周りは会社の新人研修のときに学ぶ機会があったのですが、そのような機会がない人はどこで学んでいるのでしょうか。
リンク
この記事を作成するにあたり参考にしました。
Discussion