Chapter 03無料公開

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

su-
su-
2020.10.03に更新

セキュリティールール

前章でも述べたとおり、クライアントとFirestoreの間のやり取りはSDKやREST APIを介して直接行われます。
そのため、Firestoreにアクセスするユーザーが、

  • どのデータを取得することができるか
  • どこにデータを追加できるか
  • どのデータの情報を書き換えることができるか
  • どのデータを削除することができるか

といった操作(オペレーション)に対して適切に権限を設定し、開発者の意図しない操作が行われないようにデータベースを保護する必要があります。
この保護するために記述するプログラムのことをFirestoreのセキュリティールールと呼びます。

このセキュリティールールが設定されていると、通常クライアント側からSDKやREST APIを介したアクセスに対し、セキュリティールールの検証が適応され、検証の結果許可されればリクエストされたオペレーションが実行され、拒否されればエラーを返してオペレーションの実行が阻止されます。

もしこのセキュリティールールがない、或いは適切に設定されていないと誰でも自由にあらゆるデータを取得したり、書き込んでしまうことが可能になってしまいます。
もし悪意のあるユーザーが自由にデータを読み取ったり、改竄することができてしまうと...、サービスの運用が不可能になるだけでなく場合によってはもっと深刻な事態に陥る可能性もあります。

そのため、Firestoreではセキュリティルールの設定が非常に重要になってきます。
適切に設定ができていれば先程のように、悪意のあるユーザーからのアクセスを拒否することが可能になります。

「クライアントでその操作をしないから安全?」

例えばあなたがモバイルアプリケーションを作成していて、あるデータ(Data Aとします)の作成(create)と取得(get)を行うとします。
アプリケーション上ではこの作成と取得の動作のみ実装されているため、アプリケーション上では更新や削除といった操作を行うことは出来ないようにも見えます。

しかし、「アプリ上で操作ができない==セキュリティールールで対策しなくて良い」、というのは大きな誤りです。
というのも、FirestoreにはREST API経由でアクセスすることができることと、FirebaseプロジェクトのAPI Keyは見つけようと思えば見つけられること、Firestoreの認証でトークンを得ることも場合によっては難しくないこともあり、悪意ある人物が実行しようと思えば、あなたのアプリケーションの外側でデータベースにアクセスを試みることが可能になります。

その際にもし更新や削除の操作をセキュリティールールで何も対策していなかったら、或いはセキュリティールールの不備により操作が行えるようになっていたら...ゾッとしますね。

Admin SDK

基本的にはFirestoreへのアクセスに対してはセキュリティールールが適応されますが、Admin SDKを使い管理者権限を持ってFirestoreにアクセスする場合はセキュリティールールが無視されます。
最たる例としては、Cloud Functionsで記述する関数内でデータを取得する場合が挙げられます。
以下のコードでは該当するドキュメントに対していかなるセキュリティールールが設定されていても、Admin SDKを使用しているため、データの取得が可能になります。

import * as admin from 'firebase-admin'
import * as functions from 'firebase-functions'

// admin SDKのセットアップをする
admin.initializeApp()

const postCreated = functions.firestore
  .collection('posts')
  .document('{postID}')
  .onCreate(async (snapshot, context) => {
    // admin SDKを使い、イベントトリガー対象外のドキュメントを取得する
    // この場合はセキュリティールールの有無に関わらず取得が成功する
    const userID = snapshot.data().authorID;
    const userSnapshot = await admin.firestore()
      .collection('users')
      .doc(userID)
      .get()
  });

ですのでCloud FunctionsでAdmin SDKを用いてデータベースの操作をする場合には、関数が指定したトリガーによって実行されても問題がないかを確認しておきましょう。
特にHTTPsトリガーやHTTPS callable functionsを用いる場合には事前にAPI callしたユーザーの認証の有無や、ユーザーからリクエストされた内容が妥当であるかの検証をする必要があります。

別の例としては、予めサービスにシードデータを一括でプログラムの実行(スクリプト)によって追加したい場合にも、Admin SDKを利用してセキュリティールールの影響を受けずに一挙にデータを追加する、といった使い方も挙げられます。

Admin SDKに関しての説明はここでは割愛しますが、以下のドキュメントが参考になるかと思います。

セキュリティールールの形式

セキュリティールールは、JavaScriptに似たような記述で書くことができるプログラムになっています。基本的にはfirestore.rulesというファイル名で管理されます。

セキュリティールールは、matchステートメントと、allow式をベースに記述していくことになります。

以下はとても小さな簡単な例になります。

service cloud.firestore {
  match /databases/{database}/documents {
    match /users/{userID} {
      allow read, write: if request.auth.uid == userID;
    }
  }
}

この例では、/users/コレクションのドキュメントへの読み書きに対して、ユーザーの認証情報であるauth.uidと、ドキュメントのiduserIDが一致しているときのみ、オペレーションを許可するようになっています。つまり読み書きを行うドキュメントの持ち主がオペレーションを行うユーザーと一致していれば許可される、と解釈することができます。

つまり、matchステートメントは、どのドキュメントやコレクションに対してルールを適応するのかの範囲を定めallow式では、特定のオペレーションに対してどのようにアクセスを制御するかの条件式を定めるものになります。

AuthenticationとFirestoreはセットで使用する

セキュリティールールを用いてデータを保護する上で、どのユーザーからのアクセスなのか、どのユーザーのデータなのかを識別し管理するためにはFirebaseの機能の1つである「Authentication」と組み合わせる必要がでてきます。
このAuthenticationを用いてあげることで、ユーザーがクライアントを介してFirestoreにアクセスする際には認証情報が渡されるようになります。
これにより、

  • ユーザーが認証済かどうか
  • どのユーザーであるか(uidがわかる)

といった検証を、セキュリティールール上で行うことができるようになります。
認証していない(サービスにログインしていない)ユーザーからのアクセスを制限したり、特定のユーザーからのオペレーションに対して個別にルールを設定することが可能になります。

いわばAuthenticationはサービスのセキュリティーを担保する上での土台の役割を果たすと言えるでしょう。基本的にはFirestoreを使う場合はAuthenticationも併せて使用することを強く推奨します。

もし一切認証が不要でread onlyとしてFirestoreを使う場合

もしユーザーからのデータの書き込みはなく、サービス側で用意したデータのみを、認証関係なく全てのユーザーに提供するのであればAuthenticationは不要で、allow read: trueを書くだけで済みますが、そういったケースは稀かと思います。

ですが、そういうケースでも最低限、Authenticationの匿名認証の仕組みを利用し、サービスを使うユーザーに認証情報を付与した上で、サービスを使ってくれるユーザーにのみデータを提供する方が良いかなと思います。

開発環境と本番環境

サービス開発を行う場合は基本的には開発時に接続する環境(Development)と本番のサービス稼働時に接続する環境(Production)は分けて開発すると思います。
Firebaseの場合も、開発と本番でFirebaseプロジェクトを分けることを強く推奨します

なぜ本書でこの話を?と思う方もいらっしゃると思いますが、Firestoreのセキュリティールールを構築していく上でも、開発と本番環境のプロジェクトが分離されていることがとても望ましいからです。
もし開発と本番の環境が分離しておらず、1つのFirebaseプロジェクトで管理しようとすると、セキュリティールール上でも以下の問題が発生します。

  • ユーザーからのオペレーションのリクエストが開発サービスからなのか本番サービスからなのか判別が困難
  • もし開発と本番でコレクション名をusers-dev,users-prodのように分けてしまうと同じようなルールを2箇所書かないといけなくなる。将来的に渡ってどちらかに記述漏れが発生する恐れがある

どの環境からのリクエストなのか判別が難しく、記述漏れが発生しうる...避けがたい状況ですよね。
ですので必ず開発と本番のFirebase Projectは別で用意しましょう。

開発初期に開発環境用と本番環境用のFirebaseプロジェクトを準備した場合

開発初期に開発と本番のそれぞれのプロジェクトを作成した場合、おそらくしばらくの間は本番環境のFirestoreにデータを書き込んだり、データを取得したりすることはほぼないでしょう。
だからといってセキュリティールールをうっかり開けたままにしてしまうのはよろしくありません。
なので本番環境のセキュリティールールに関しては時が来るまで以下のように記述して、いかなるアクセスも受け付けないようにしておきましょう。

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /{document=**} {
      allow read, write: if false;
    }
  }
}

ここ最近では、Firebaseプロジェクトを作成し、Firestoreをセットアップする場合には、以下のようなセキュリティールールが設定される事があります。

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {

    // This rule allows anyone on the internet to view, edit, and delete
    // all data in your Firestore database. It is useful for getting
    // started, but it is configured to expire after 30 days because it
    // leaves your app open to attackers. At that time, all client
    // requests to your Firestore database will be denied.
    //
    // Make sure to write security rules for your app before that time, or else
    // your app will lose access to your Firestore database
    match /{document=**} {
      allow read, write: if request.time < timestamp.date(2020, 9, 15);
    }
  }
}

これは指定された日付をすぎると、全てのリクエストが遮断される、というルールになります。
昔はFirestoreのセットアップ時に、「テストモード」と「ロックモード」を選択できるようになっていて、「テストモード」を選択したときに、allow read, write: if true;と書かれたセキュリティールールがセットアップされるようになっていました。
当然ながらこれだとあらゆる脅威に晒されてしまいますし、そのままプロジェクトが放置されてしまうと格好の的になってしまうこともあり、最近ではテストモードを選択しても上記のように時限式のルールに変更されました。


次章ではセキュリティールールの書き方について詳しく述べていくことにします。