こんにちは。株式会社Flatt Security セキュリティエンジニアの梅内 (@Sz4rny) です。
本稿では、2022年9月に Cloud Storage for Firebase に新たに導入された Cross-service Rules という機能について、前提知識をおさらいしつつ、実例を交えながらその概要や利用方法、メリットなどを紹介します。
また、Flatt Securityのセキュリティ診断(脆弱性診断)ではFirebaseを用いたサーバーレスなアプリケーションも診断可能です。 「料金を自分で計算できる資料」を無料公開中ですので、ご興味のある方は是非ダウンロードしてみてください。
前提知識のおさらい
本章では、Cross-service Rules を利用するにあたって必要な前提知識について説明します。
Firebase とは
まず、Firebase とは、Google が提供する mBaaS (mobile Backend as a Service) です。
本来、Web アプリケーションやモバイルアプリケーションのバックエンドを用意する際は、アプリケーションロジックの実装だけではなく、インフラの選定や構築、データベースやストレージ用サーバの構築、認証機構の実装など、様々な対応が必要になります。mBaaS は、このようなアプリケーションの実装に必要なバックエンド環境をクラウドサービスとして提供する形態のことを指しています。mBaaS を活用することで、バックエンドの構築にかかる工数を最小限に抑えつつ、クライアントサイドで動作するアプリケーションの開発に工数を割くことができるようになります。
Firebase では、主に以下のサービスが提供されています。
- Authentication ... ユーザの認証と管理のための機能を提供するサービス
- Cloud Firestore ... ドキュメント指向型の NoSQL データベース
- Cloud Storage ... 静的ファイルの格納や配信に利用できるオブジェクトストレージ
- Hosting ... コンテンツのホスティングサービス (いわゆる CDN)
- Cloud Functions ... 特定のトリガーにもとづいて事前に定義された関数を実行するコンピューティングサービス
これらのサービスのうち、本稿で紹介する Cross-service Rules に関連するのは Cloud Storage と Cloud Firestore (以下、Firestore) の2つです。次節では、これらのサービスについてもう少し詳しく説明します。
Firestore の概要
Firestore は Firebase において利用できるドキュメント指向形の NoSQL データベースです。Firestore の特徴として、以下が挙げられます。
- データをコレクションとドキュメントという構造で管理する
- クライアントが直接データベースにアクセスし、データの CRUD 操作を行う
- セキュリティルールという仕組みを用いてアクセス制御を行う
まず、1つ目の「データをコレクションとドキュメントという構造で管理する」という特徴について説明します。
一般的なリレーショナル型データベース (RDB) では、データ単体を1行のレコード、データ全体を2次元のテーブルで管理します。一方で、Firestore をはじめとするドキュメント指向型データベースでは、データ単体は構造化データで表現できるドキュメント、データ全体はドキュメントを集めたコレクションという単位で管理します。
詳細な仕様は各プロダクトやサービスによってまちまちですが、Firestore は key-value 型のドキュメントとそれを集めたコレクションという概念を用いてデータを管理します。加えて、Firestore においては、ドキュメント自身が子要素としてコレクションを持つことができます (これをサブコレクションと呼びます)。以下に概要図を示します。
次に、2つ目の「クライアントが直接データベースにアクセスし、データの CRUD 操作を行う」という特徴について説明します。
一般的な Web アプリケーションのアーキテクチャにおいて、クライアントがバックエンドのデータベース上にアクセスしたり、その中のデータを更新したりしたい場合は、その前段にある API サーバ等にリクエストを送信します。一方で、Firestore では、クライアントが直接 Firestore にアクセスし、データを操作します。そのため、Firestore においては、一般的に API サーバが担っているリクエストの認証認可や作成・更新されるデータのバリデーション等の機能を Firestore 自身が有しています。
最後に、3つ目の「セキュリティルールという仕組みを用いてアクセス制御を行う」という特徴について説明します。
前述の通り、Firestore はクライアントから直接アクセスされるため、それ自身がセキュリティ上のチェック機構を有している必要があります。これを実現するのがセキュリティルールです。セキュリティルールは Firebase 独自の DSL (Domain Specific Language) として提供されている仕組みであり、利用することで以下に示す事項をチェックすることができます。
- アクセス元ユーザの認証情報
- アクセス元ユーザのロールや属性に基づいた権限の可否
- 作成・更新されようとしているデータのバリデーション
- アプリケーションの仕様に基づいた操作の正当性の可否
以下に、認証が完了しており、かつアクセス元ユーザに対応するドキュメントの role
フィールドが admin
である場合だけ secrets
コレクション内のドキュメントへのアクセスを許可するセキュリティルールの実装例を示します。
rules_version = '2'; service cloud.firestore { match /databases/{database}/documents { function isAdmin(uid) { return get(/databases/$(database)/documents/users/$(uid)).data.role == 'admin'; } match /secrets/{secredId} { allow read: if request.auth != null && request.auth.token.email_verified && isAdmin(request.auth.uid); } } }
なお、Flatt Security Blog では Firestore のより詳細な概要やセキュリティルールを用いた堅牢な Cloud Firestore 環境の構築に関するブログを過去にいくつか公開しています。より詳しくこれらのトピックについて知りたい方は、こちらも合わせてご覧ください。
Cloud Stroage の概要
Cloud Storage は Firebase において利用できるオブジェクトストレージで、静的ファイルの格納や配信に利用することができます。
Cloud Storage は GUI によるアクセスと API によるアクセスの双方に対応しています。以下に、JavaScript で Cloud Storage にファイルをアップロードする際の実装例を示します。
<!-- index.html --> <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <script defer src="https://www.gstatic.com/firebasejs/8.10.1/firebase-app.js"></script> <script defer src="https://www.gstatic.com/firebasejs/8.10.1/firebase-storage.js"></script> <script defer src="./script.js"></script> </head> <body> <div id="message"> <input type="file" id="file-form" /> <input type="text" id="name-form" /> <button id="upload-button">upload</button> <p id="url"></p> </div> </body> </html>
/* script.js */ const firebaseConfig = { /* 省略 */ }; const app = firebase.initializeApp(firebaseConfig); document.getElementById('upload-button').addEventListener('click', () => { const name = document.getElementById('name-form').value const file = document.getElementById('file-form').files[0]; app .storage() .ref(`files/${name}`) .put(file) .then(snapshot => snapshot.ref.getDownloadURL()) .then(url => document.getElementById('url').innerText = url) .catch(e => console.error(e)) });
なお、Firestore と同様に、Cloud Storage のアクセス制御もセキュリティルールを用いて定義することができます。以下に、アクセス元のユーザがログイン済みであり、かつ、アップロードされようとしているファイルが 10MB 以下の画像であることをチェックするセキュリティルールの例を示します。
rules_version = '2'; service firebase.storage { match /b/{bucket}/o { match /files/{filename} { allow get: true; allow create: if authed() && isValidFileTypeAndSize(); } } function authed() { return request.auth.uid != null && request.auth.token.email_verified; } function isValidFileTypeAndSize() { return request.resource.size < 10 * 1024 * 1024 && request.resource.contentType.matches('image/.*'); } }
Cross-service Rules が解決する課題
課題
前提として、Firestore のセキュリティルールでは Firestore 上に格納されたデータの内容やドキュメントの存在可否に基づいてアクセス制御を行うことができます。
たとえば、「引数 uid
に対応する users
コレクションに格納されたドキュメントの role
属性の値が admin
であるかどうか」は get
関数を用いた下記のコードにより判定することができます。
function isAdmin(uid) { return get(/databases/$(database)/documents/users/$(uid)).data.role == 'admin'; }
また、「引数 uid
に対応するドキュメントが users
コレクションに存在するかどうか」は exists
関数を用いた下記のコードにより判定することができます。
function existsUser(uid) { return exists(/databases/$(database)/documents/users/$(uid)); }
一方、Cross-service Rules 導入前まで Cloud Storage のセキュリティルールでは、上記に示したような Firestore 上のデータに基づいたアクセス制御を行うことができませんでした。そのため、Cloud Storage のセキュリティルールのみでは、下記に示すような要件を実現することができませんでした。
- Firestore にアプリケーションを利用するユーザのロールを格納しており、そのロールに基づいて Cloud Storage 上のファイルのアクセス可否を判断したい
- Firestore に特定のデータの公開設定の可否を示すデータを格納しており、そのデータに基づいて Cloud Storage 上のファイルのアクセス可否を判断したい
このような課題を解決するのが Cross-service Rules です。Cross-service Rules により、Cloud Storage 上のセキュリティルールで firestore.get
と firestore.exists
という2つの関数が利用できるようになりました。直感的に分かる通り、これらは Firestore のセキュリティルールにおける get
関数および exists
関数と同様の働きをするものです。
次節以降において、Cross-service Rules を用いた実装例を紹介します。
事例1. ロールに基づくアクセス制御
まず、下記の画像に示すとおり Firestore の users
コレクションにユーザの名前とロールが格納されたドキュメントが格納されているとします。
次に、下記の画像に示すとおり Cloud Storage に public
および private
という2つのフォルダを作成します。
これらのフォルダへのアクセスについては、以下に示す要件が定められているものとします。
private
フォルダ内のファイルへはロールがadmin
のユーザのみがアクセスできるpublic
フォルダ内のファイルへはログイン済みのユーザ全員がアクセスできる
この要件を満たす Cloud Storage のセキュリティルールの実装例を下記に示します。
特に注目すべき点は、authed
関数内で認証情報をチェックするとともに Firestore において UserID に対応するドキュメントが存在することを firestore.exists
関数を用いてチェックしている点、また、isAdmin
関数において firestore.get
関数を用いて対応するドキュメントの role
フィールドの値が admin
であることをチェックしている点です。
rules_version = '2'; service firebase.storage { match /b/{bucket}/o { match /public/{filename} { allow read, write: if authed(); } match /private/{filename} { allow read, write: if authed() && isAdmin(); } } function authed() { return request.auth.uid != null && request.auth.token.email_verified && firestore.exists(/databases/(default)/documents/users/$(request.auth.uid)); } function isAdmin() { return firestore.get(/databases/(default)/documents/users/$(request.auth.uid)).data.role == "admin"; } }
事例2. 公開設定に基づくアクセス制御
まず、下記の画像に示すとおり Firestore の configs
コレクション内のpublic
ドキュメントにおける filenames
フィールドに公開対象のファイルの名前が格納されているとします (例では dog.png
と cat.png
)。
次に、下記の画像に示すとおり Cloud Storage に files
フォルダを作成し、中にいくつかのファイルを格納します。
これらのファイルへのアクセスについては、前述の公開対象のファイル名が Firestore 上で指定されている場合にのみ許可されるものとします。この要件を満たす Cloud Storage のセキュリティルールの実装例を下記に示します。
特に注目すべき点は、アクセス対象のファイル名 (filename
) を公開対象とするという設定が Firestore に存在するかどうかを firestore.get
関数を用いてチェックしている点です。なお、セキュリティルールにおける data in array
という式は、data
が配列 array
の要素として存在すれば true
、存在しなければ false
と評価されます。
rules_version = '2'; service firebase.storage { match /b/{bucket}/o { match /files/{filename} { allow get: if filename in firestore.get(/databases/(default)/documents/configs/public).data.filenames; } } }
以下に、公開設定に基づくアクセス制御が正しく設定されていることを検証するための実装例を示します。
<!-- index.html --> <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <script defer src="https://www.gstatic.com/firebasejs/8.10.1/firebase-app.js"></script> <script defer src="https://www.gstatic.com/firebasejs/8.10.1/firebase-storage.js"></script> <script defer src="./script.js"></script> </head> <body> <p id="results"></p> </body> </html>
/* script.js */ const firebaseConfig = { /* 省略 */ }; const app = firebase.initializeApp(firebaseConfig); const filenames = ['dog.png', 'cat.png', 'snake.png']; for (const filename of filenames) { app .storage() .ref() .child(`files/${filename}`) .getDownloadURL() .then(url => { document.getElementById("results").innerText += `${filename} is located at ${url}\n` }) }
この index.html を開いてみると、Firestore の公開設定用のドキュメントに記載されている dog.png
と cat.png
は正しく取得できますが、snake.png
は storage/unauthorized
エラーが返却されます。この結果より、Firestore 上のデータを用いた Cloud Storage のアクセス制御が正しく実装できていると言えます。
おわりに
本稿では、2022年9月に Cloud Storage のセキュリティルールにおいてに新たに導入された Cross-service Rules という機能について、前提知識をおさらいしつつ、実例を交えながらその概要や利用方法、メリットなどを紹介しました。Cross-service Rules は以前から要望が多かった機能ということもあり、当該機能を用いることで、多くの組織でセキュリティルールによるアクセス制御の効率化が達成できるのではないでしょうか。
これまで Cloud Storage のセキュリティルールから Firestore にアクセスできないという制約のために Cloud Functions 等を用いて Cloud Storage のアクセス制御を行っていた方は、ぜひ Cross-service Rules で快適な Firebase 生活を送っていただけますと幸いです。
さて、Flatt Security では、一般的な Web アプリケーションだけでなく、本稿で紹介した Firebase のような mBaaS を含む幅広いセキュリティ診断サービスを提供しています。また、事後的な診断に限らず開発段階でのコンサルティングも対応可能です。
上記のデータが示すように、診断は幅広いご予算帯に応じて実施が可能です。ご興味のある方向けに下記バナーより料金に関する資料もダウンロード可能です。
また、Flatt Security はセキュリティに関する様々な発信を行っています。 最新情報を見逃さないよう、公式 Twitter のフォローをぜひお願いします!
では、ここまでお読みいただきありがとうございました。