Firebaseは便利なサービスですが、標準で公開されている(されることもある)情報で結構いろいろイタズラができちゃうのでruleとかしっかり設定しましょうね。という話は理解しているのですが、具体的にどのようなリスクがあるのか試しながら調べてみました。
firebase歴が浅いため勘違い・無知識などあれば、どんどんご指摘ください。
基本config情報は公開されます
そもそもFirebaseコンソールのアプリ設定画面で、CDNで公開されてるsnippetとして紹介されているくらいですので、Googleとしては「公開前提」という理解でまちがいないでしょう。
また、Firebase Hostingで自動生成されるindex.htmlにも[hosting_url]__/firebase/init.jsとしてconfig情報が参照できるようになっています。
ReactやVueでも公開されますし、.envを利用してもコンパイル先のjsで各種情報は閲覧可能です。
クライアントサイド言語において.envが有効なのはGithub等で設定情報を公開しない時くらいです。
credential(秘密鍵)がバレなければいいの?
ネットを見てるとconfig情報は公開前提なので別にバレても良くて、 credential(サービスアカウントの秘密鍵)がもれなきゃ万事OK というような記述も見かけますが、「ほんとに?」って疑問を持ったのでこの記事を書いています。
確かに秘密鍵が漏れると、firebase-admin等を利用してあらゆる操作ができてしまうので「秘密鍵が漏れたら終わり」であることは間違いありませんが、実際コードを書いてると「config情報でもいろいろできない?」と思ったので試してみたいと思います。
基本情報がわかれば何ができるか
firestore/storage, APIの制約設定等を行っていない状態だと結構いろいろなことができます。
- 非ログイン状態でのSDKを使ったあらゆる操作
- 非ログイン状態でのREST APIを使ったあらゆる操作
SDKはもちろん、REST API経由でもいろいろなことができます。
まあ、最低限元ruleにて、
allow read, write: if request.auth !== null;
としてるから大丈夫だし、念を入れて、
allow read, write: if request.auth.uid === document_id
とかもしてる。
となっても十分ではありません。
ログイン状態も再現できます
実はいくつかの方法でログイン状態でさえ再現できます。
勝手にユーザーを作ってそのユーザーのidTokenを得る
まず、よく使われるemail, password認証がOnになっている場合、Identity Toolkit APIを利用して勝手にユーザーを作成してtokenを取得することが可能です。
下記はPostmanでそのAPIを立ていているところです。
APIキーを渡すだけで新規にユーザーが作成され、そのユーザー用のidTokenさらにはrefreshTokenも返るのでずっとAPIを利用できる状態になります。
このtokenを利用すれば、ログイン状態のユーザーとしてfirebaseにアクセスできるので、少なくとも
allow read, write: if request.auth !== null;
では、ログイン状態であることを利用して他人のデータを操作される可能性がり、不十分なことがわかります。
自分のindexDB情報からaccessToken(idToken)を得る
じゃあ、
allow read, write: if request.auth.uid === document_id
なら大丈夫でしょ。ということについてはアプリのコンテンツに依存するかなと。この設定の場合、少なくとも他人のデータを閲覧・改ざんされるリスクはありませんが、ユーザー自身のデータを改善される恐れがあります。
例えば、ゲームアプリで「スコア」や「レベル」管理しているような場合、そうした情報は改善されると困ります。
あるユーザーが自分自身のtoken情報を知る方法としてブラウザのIndexedDBを見る方法が考えられます。
WebアプリでFirebaseのログイン機能を利用した場合、標準("local")ではブラウザのIndexedDBに認証情報が保存されます。
これをデベロッパーツール等で閲覧・コピーすれば容易に自分のデータを操作することができます。
下記の例では(もともと10pointしかなかった)ポイントを1000に書き換えています。
ピンク部分は公開情報。緑部分は非公開情報。
防ぐ方法ある?
個人を特定する最高解像度が、下記なりますが、下記のruleでも上記のようなことをは起こりえます。
allow read, write: if request.auth.uid === document_id
では防ぐ方法はあるのでしょうか?よく言われるのは下記のような方法でしょうか。
- ruleで個人特定以外のバリデーションを追加
- API化してSDKを経由させない
- APIを呼び出すときhttps.onCall()を利用させるようにする
ただ、これらも十分とは言えません(よね?)。
個人特定以外のバリデーションを加える
例えば下記のような感じで、100point以上はNGとするとか。でも、100point以下なら更新されてしまいます。
allow update: if request.resource.data.point < 100
API化してSDKを使わない
これも対策例としてあげられますが、例えば上記point更新APIを公開するとして認証はどうしたらいいのでしょうか?
APIにJWT認証をつけたところでtokenをクライアント側で保持しなければならない問題はあり、同じ問題が発生するような気が・・・。
tokenをユーザーから見える場所に保存しなければいいのですが、そのような運用はなかなか難しそう。
https.onCall()を利用する
https.onCall()でfunctionを公開することでhttps.onRequest()と違い、SDKからの呼び出しが可能であり、認証情報も利用できます!!が、それが場合により偽装できるというのがここまでの話です。
その他
writeをupdate, deleteとかに分けるって話とかありますが、ここまでの話の流れではあまり意味がありません。
他に良い方法があれば教えてください。
防ぐ方法
何をしてもダメな気がしてきましたが(特にWebアプリ)、基本的な指針となるのは「Firebaseリリースチェックリスト」かなと。
ただ、ここで書かれているのは、
- ホワイトリスト追加等、制限できるものはなるべく制限しろ!
- ruleを書けるものはruleを書け!
ということだけです。
ホワイトリスト登録の具体的例
まず、Webアプリの場合はホスト(ドメイン)環境を明確し(通常はHostingかと)、そこからのリクエストしか受付受け付けないようにするのが基本かなと思います。
制限は[APIとサービス]->[認証情報]等でキー単位で行うことができ、例えば、下記のように設定すると、
先程実行できたユーザー登録は指定ドメイン以外からはできなくなります。
当然ですが、SDKも影響を受けます(裏でREST API叩いているだけなので)。
Firebase -> Authenticationページにある「承認済みドメイン」はOAuthのリダイレクト先・元としての許可のよう。
キーの追加
制限等はキー単位で行われるため、当然ながらそのキーを利用する全てのクライアントに適用されます。
場合によりキーを分けたいときもありますが、Firebaseコンソールでアプリを追加してもキーは変わらず悩んでいたところ、CGPコンソール(上記と同じくAPIとサービス)にて新規のキーを発行できることがわかりました。
ここでキー別に別のポリシーを設定し、クライアント用途毎に使い分けるなどすればいいかなと。
その他のセキュリティー対策
rulesを正しく設定
割愛(ここではruleは正しく設定されてても、裏でだだ漏れなのを防止することに重点を置いてます)。
他サイトを参照ください。
collectionやdocumentの(Path)生成に必要な情報を極力隠す
ここまでの例ではconfig情報だけで結構いろいろやれる!!的に解説してきましたが、少なくともfirestore(やstorage)を操作する場合、下記のように、
https://firestore.googleapis.com/v1/projects/[project_id]/databases/(default)/documents/[collection_name]/[document_name]
下記の3つの環境依存情報が必要となり、
- project_name(config情報に含まれている)
- collection_name(未知)
- document_name(未知)
collection_nameとdocument_nameに関しては通常未知であるためここがバレなければSDKであれ、REST APIであれ、直ちにリクエストできるわけではありません。ただ、document_nameはcollection_nameがわかればlistで取得できるので、意外と重要なのはcollection_nameの設定かもしれません(あまり、usersとかmembersとかは付けないほうがいいかも)。
あと、useridなどdocument_nameになりそうな情報はfieldに含めないほうがいいかもしれません(だいたいreadは許可されてるので見える)。
collection名を外部から取得する方法があれば教えてください。上記も無意味になります。exportとかで出来ちゃったりするのでしょうか。。。調べが足らず。
まとめ
とりあえず以下のような結論に至った。
- Firebaseでは何も考えていないと公開情報だけで以外といろいろできる
- 対策はruleの設定に加え、APIキー(やそれを使うSDK)の制限が必要
- collection名、document名は推測されないようなものがベター
- 実務でまじめにFirebaseを使うにはGCPの知識も必須
考えてみればFirebseの課題というより、SPAというかクライアン、サーバ分離型のアプリの課題と基本は同じ。
間違い・勘違い、対応策など、どんどんご意見いただけると幸いです。随時更新します。
データ盗まれるより、まさかり投げられる方が良い。
個人依存メモ
私はアプリをReact Nativeで開発することがおおいのですが、ReactNativeだとconfig情報はどこまで取得できるのか?調べてみました(継続中)。
結論から言えば、root権限や脱獄iphone出ない限りWebのようにtoken情報等を取得することは難しそう。
永続化情報は、Android, iOSともにアプリのプライベートエリアに保存されているため、少なくとも他のアプリから見たり、直接見たりはできなさそう。