Firestoreのセキュリティルールについて

コキチーズ氏(以下、コキチーズ):今日はセキュリティルールについて話そうかなと思います。

自己紹介をこのスライドにもはさんでいます。

先ほどと少し内容が違いますが、GCPやFirebaseも好きで、あとはゲームが好きで、そういうことを書いてます。もう一度言いますけど、コキチーズです。TwitterやGitHubのアカウントはk2wankoで、SlackのIDもk2wankoなので、僕に連絡を取りたかったら、k2wankoでメンションしてくれたら、だいたい届きます。

最近、「めっちゃFirebaseが盛り上がってますね」と、Twitterやいろんなニュースを見て思っていて。「合計1億件以上の個人情報がFirebaseの脆弱性によって公開状態に」というGIGAZINEの記事がありました。

どういう記事かというと、セキュリティ会社の調査によって、Firebase利用企業の62パーセントが、データベースのセキュリティルールに問題があり、機密情報などが公開されていた(というもの)。この機密情報が、記事を読むかぎり、ユーザーのトークンやメールアドレスが含まれている状態だったそうです。

これは、セキュリティルールのミスなので……。セキュリティルールというのは、開発者が設定しなきゃいけないものなんですよね。この記事は、Firebase(の脆弱性)という書き方ですけど、ディベロッパーが正しく設定できていなかったというところがあります。

どういう状態のセキュリティルールだったのか、詳細はわからないです。ですが、おそらくこういう感じです。

.read、.writeがtrue、trueみたいな感じになっていて、全公開のようになっていたか、authで認証してるかどうかだけをチェックみたいな状態になっていたか、ということが想像できるわけですね。

そもそも、セキュリティルールとは何か?

これを踏まえて、「最強のセキュリティルールとは何か?」みたいなことを考えると、何もしない、falseにしておくと最強である、ということがわかります。

(会場笑)

つまり、クライアントで書き込みも読み出しもしなければ安全である、ということになるんです。これで完結するんですけど、そういうわけにはいかないので、ちゃんとやります。

「そもそも、セキュリティルールとは何か?」ということで、Firebaseでセキュリティルールが設定できるサービスは3つあります。

最初のRealtime Database、ちょっと前に追加されたCloud Storage、最近出てきたCloud Firestoreの3つがセキュリティルールを書けるサービスになっています。

Realtime Databaseのセキュリティルールは、たぶん、一番「よくわからん」みたいなものになっています。

そもそもRealtime Databaseとは何かという話を軽くします。

書き込んだ内容をリアルタイムに同期してくれるデータベースで、オフライン時でも書き込めるというのがいいです。あと、クライアントが直接データベースに書き込めるという良さもある。主に、ゲームやチャットを作成するのに向いているサービスということになっています。以降、Realtime DBと呼びます。

Realtime DBのルールは、JSONで書いていくのが基本です。

さっき見せたこんな感じのJSONで、この.read.writeというのが読み出しや書き込みです。この値が“true”になっている時、読み出しや書き込みができます。

条件式を値のところに書くことで、このユーザーは書き込みできて、このユーザーは書き込みできないといったことを、JSONで1つ1つ書けます。

カスケード形式といって、JSONの浅いネスト、上位層のほうで、".read": trueという状態に設定されていると、例えば下の階層のほうで".read": falseとしていても、下の設定は意味がないんです。このルールの意味を見出そうとすると、(この場合)k2wankoには何もアクセスさせたくないという書き方をしたいんだと思います。でも、こういう書き方をしても、それは意味がなくなります。

あと、$otherというものがあります。

これは.writeに書き込み権限があるという状態にしているんだけど、書き込みさせたいデータを制限したい時……例えば好き勝手なプロパティを追加させたりしたくない時は、.validateというものが存在するので、それを使うことができます。

あと、リスト形式のものを取得する時に何件取得するとか、クエリの制限もできます。例えば1万件取られると、それだけパフォーマンスが悪くなってしまうので、取得できるアイテムの数を制限したい場合は、こういう感じでセキュリティルールを書けば制限できます。

けっこういろいろ柔軟にできて、がんばれば大丈夫です。

JOSNの辛いところを解消するBolt

ただ、JSONで書くのは正直つらいというところもあります。そのつらいポイントとして、JSONはコメントが書けない、関数を定義できないというのが、いろいろあります。

あと、型がないとか、条件分岐とか、ハイライトがつかないとか、つらいポイントがいっぱいあるんですけど、そういうのを解消するために、FirebaseにはBoltというツールがあります。

これは何かというと、コンパイルするとRealtime DB上のセキュリティルールを書き出してくれる独自の言語です。

コメントを書けて、関数を定義できます。TypeScriptのような感じで型の定義もできるので、type user 〇〇と書いた後、このタイプはnameを持っていて、emailを持っていて、という設定もできます。

説明すると大変なんです。path /〇〇という感じで、パスのキーワードをもとに、ここでどういう処理をするかといったことを、プログラムアプリに 0:06:51記述することができます。コメントもできるし、ファンクションも定義できるし、かなり便利です。

あと、書き込み権限をcreate、update、deleteなどで分割して定義することもできます。これはセキュリティルール本来のものでも書けるんだけど、書くのがちょっと冗長になるところをスマートに書けるなど、いろいろと気遣いがあります。けっこう便利なので、Realtime DBを使う時はBoltを使うと、見通しのいいセキュリティルールが書けるようになると思うので、見てみたほうがいいと思います。

でも、これは万能というわけではありません。コンパイルした後のセキュリティルールは、生のJSONをある程度追って、自分の意図したとおりのセキュリティルールが書かれているかどうか、出力されたJSONをちゃんとチェックしたほうがいいと思います。結局、人の目でチェックしないと、自分は正しいと思って書いたけど、実際は正しくないみたいなことも起こりうると思います。

Cloud Storageのセキュリティルール

ちょっと長くなっちゃいましたけど、Realtime DBのほうは終わりで、次は、Cloud Storageです。

Cloud Storageは何かというと、画像や動画などの大きめのファイルのデータを保存するためのストレージです。これもSDKを通して、クライアントから直接ストレージに突っ込めます。

これはJSONじゃなくて、セキュリティルール用の独自言語が存在しています。

型定義はないんですけど、コメントや関数定義ができます。さっきのBoltみたいに、readはgetとlistに分割できるし、writeはcreate、update、deleteといった感じで分割できるので、記述しやすくなっています。

よくあるCloud Storageのセキュリティルールなんですけど、こういう感じでserviceというのを定義して、matchでbucket……変数を「ブラケット、bucket、ブラケット」という感じで書くと変数になって、isImageや、ファンクションを整理しています。

isImageのなかで何をしているのかというと、リソースサイズを見て、5メガバイトに制限するということや、コンテントタイプを見て、Imageしかアップロードさせないということも、こうしてセキュリティルールを書けば作れます。

あとは、createの時にメタデータにuidが入ってるかどうか、それがリクエストした人とちゃんと同じuidかどうか、updateする時も同じユーザーIDの人かどうか、deleteの時も、リクエストしてきたuidの人がちゃんと一致してるかどうかをチェックできます。

注意点として、GCSのアクセス制御は、裏側がGoogle Cloud Storageというものが使われています。これはCloud Storageのほうの設定で、例えば「このオブジェクトがパブリックのURLを付与する」というものがあるんですけど、それにチェックを入れると、そのパブリックURLからはアクセスできます。resource.data.visibilityがpublicになるんですね。

ここのreadがfalseになっていても、GCSのパブリックのURLが存在すると、そっちからは別にダウンロードできるよね、といったことになります。Google Cloud Storage側のACLなどの設定を、しっかりチェックしておく必要があります。

セキュリティルールと必ず1対1であるかというと、そういうわけではありません。各種設定に依存してくるので、Cloud StorageはそういうACLをちゃんとチェックしましょう。

Cloud Firestoreのセキュリティルール

次が、Cloud Firestoreですね。みんな「セキュリティルールをやるぞ!」と思った時、たぶんFirestoreが一番気になってるかなと思います。

Cloud Firestoreとは何か知らない人もいるとは思うので説明すると、Realtime DBの次世代のデータベースということになっていると思います。

ドキュメント指向で、基本的にはRealtime DBと同じことができます。なので、さっき説明したオフラインの書き込みもできるし、クライアントから直接データベースへの書き込みもできる。あと、変更を検知して、いろいろ処理するということもできます。

Realtime DBとの違いは、クエリとかがちょっと強化されていて、where文とかでもう少し賢いクエリを立てます。

さすがにMySQLみたいな、ガッツリSQLが書けるわけではないですけど、Realtime DBと比べればマシなクエリが書けます。

Slackのシャープステータスに入っている人を見るとわかると思うんですけど、 Realtime DBのステータスや、落ちたレポートとかが上がってきてると思います。Firestoreは安定感があるかなと(思います)。ただ、まだβで、USにしかリージョンがないので、本番で使えるかどうかはまだこれからかなという気がします。

Realtime DBとのセキュリティルール的な違いは、さっき説明したJSONではなく、Storageと同じように独自言語で書けることです。Storageとだいたい同じものだと思って大丈夫です。

さっき説明し忘れたんですけど、Realtime DBのセキュリティルールは、セキュリティルール内にIndexを書くこともあります。Firestoreにおいては、セキュリティルールとは分離して設定ができます。あと、サーバーの設定になるんですけど、IAMの設定もできます。

また、ワイルドカードの設定を除き、ルールがカスケード式ではないので、さっきのRealtime DBで説明した、上位層でreadをマッチして、下位層も全部許可される、みたいなことにはなりません。さっき説明したStorageと同じで、readはget、listに分割できるし、writeはcreate、update、deleteに分割できます。

注意するべきポイント

これはざっくりしたサンプルなんですけど、isPersonMessageというファンクションを定義して、それを下のallow writeのほうで使っています。resource.keys().hasAll(['name'])とか、いろいろ便利な関数があります。

hasAllというのは……このリソースはnameプロパティを含んでなければいけないということをチェックできて、hasOnlyというのは、name、email以外のものが含まれているかどうかをチェックできます。name、email以外のものが含まれていると、これは失敗しますね。

あと、Realtime DBと同じように、クエリの制限ももちろんできます。query.limit <= 10というようにすると、10件に制限できます。

listにしているというのが、ポイントですね。

これはgetという関数が定義されています。何かというと、セキュリティルールは自分がアクセスしようとしているところのリソースをとろうとする時、すでにあるデータのリソースはそのまま取れるんですけど、別のところに存在するマスタとかをチェックしたい場合は、こういうgetなどを使って、別のパスに保存されているデータを読み出す必要があり、パスとかで読み出せます。getを使えば、referee == request.auth.uidとかでチェックできるわけですね。

ただ、getを使う時に注意しないといけない点があります。文字がかなり小さくて申し訳ないです。getやexistsなど別のパスにあるデータを読み出そうとすると、readのオペレーションがかかるので、その分コストがかかります。あと、getやexistsは呼び出し回数に制限があります。いまのところ、確か10回までしか呼べないという制限があったと思います。

早めにセキュリティルールを設定するべき

次は、protobuf-rules-genという、また突拍子のないものが出てきましたが、これが何かを説明します。

protocol bufferというものがあります。protocol bufferはIDLを定義して、クライアントやサーバーなど、いろんなものとメッセージをするためのシリアライザーみたいなものです。

protobuf-rules-genはprotocol bufferから型定義を見て、それを検査してくれるセキュリティルールを生成するプラグインというものです。

さっき説明したんですけど、hasAllやhasOnlyというのは、結局手動で書かなければいけないんですけど、このあたりの設定を、protobuf-rules-genというものを使うと、protocol bufferの設定から簡単に生成することができます。

これがなんでうれしいかというのは、さっき説明した型定義ができないというところを、これで解消できるからです。たくさんプロパティがあるものだと、「それ全部、セキュリティルールを1個1個書いていくのか……」という思いが生まれちゃいます。(しかし)こういうものを活用していくと、簡単に書くことができます。

まとめとして、セキュリティルールは必ず設定しましょう。

開発のタイミングだと、「とりあえずセキュリティルールなしでもいいかな」みたいな感じで開発しちゃうと思うんですけど、デプロイする時や公開する時は、どうせセキュリティルールを設定しないと危ないことになるので、最初からセキュリティルールを書いておいたほうが楽です。セキュリティルールを簡単に、書きやすくするためのツールはいろいろあるので、そういうものを活用して、早めに早めにセキュリティルールは書いておいたほうがいいです。

説明を省いちゃったんですけど、Realtime DBとFirestoreにはシミュレータがあるので、それを使ってテストするのもいいんですけど、シミュレータはシミュレータで、本物ではありません。実際にE2Eテストとかを組み合わせて、セキュリティルールが十分正しいかどうかをテストしたほうがいいです。以上です。ありがとうございます。

(会場拍手)

※登壇者注:今は、ローカルエミュレーターがあるのでそれも使えます。 https://firebase.google.com/docs/firestore/security/test-rules-emulator