Firebase Cloud Storageでセキュリティルールを突き破る方法
はじめに
Firebase Cloud Storageは便利なStorageです。
ですが、使い方を間違えるとセキュリティルールを貫通し、全てのファイルを誰でも閲覧になってしまう可能性があります。
よくある場合では、以下の実装をしてしまうだけで簡単にセキュリティルールを突き破れます
- 画像URLをFirestoreに保存する
- 画像がCDN等でキャッシュされる・する
本記事では
- どのような場合でセキュリティルールの意味がなくなってしまうのか
- アクセストークンが含まれるURLの無効化・更新方法
この二つを紹介します。
どのような場合でセキュリティルールの意味がなくなってしまうのか
まず初めにStorage内の画像を取得し表示を行う機能を実装する場合を考えましょう
Firebase Cloud Storageで画像のURLを取得する
Storageにある画像を表示する場合は、getDownloadURL()
というメソッドを使ってURLを取得します。以下は、html,jsでのサンプルです
const storage = firebase.storage();
const imageUrl = storageRef.child('images/stars.jpg').getDownloadURL()
<img src={imageUrl} />
この画像URL取得のタイミングでStorageに設定されたセキュリティルールを検証し、そのユーザーに正しい権限があるか確認が行われます。
例えば権限のあるAさんと権限のないBさんがいた場合、セキュリティルールが正しく機能し以下のような結果になります
ユーザー | 権限有無 | 取得 |
---|---|---|
Aさん | true | できる |
Bさん | false | できない |
取得したURLは以下のようになり、細部を見るとアクセストークンが付いていることが分かります
https://firebasestorage.googleapis.com/v0/b/xxxxx.appspot.com/o/xxxxxxxxxx?alt=media&token=xxxx-xxxx-xxxx-xxxx
この部分です token=xxxx-xxxx-xxxx-xxxx
ですがこのアクセストークンには弱点があります
アクセストークンの弱点
弱点とは、アクセストークンが含まれるURLにアクセスするユーザー自体には認証が行われないことです。
結果として権限のないBさんでもアクセストークン付きURLでは画像を取得し閲覧ができてしまいます
ユーザー | 権限有無 | 取得 | アクセストークン付きURL閲覧 |
---|---|---|---|
Aさん | true | できる | できる |
Bさん | false | できない | できる |
また、そのアクセストークンが無期限の生存期間で一度アクセストークン付きURLが露出してしまえば、誰でも無期限に閲覧できてしまう状態となります。
アクセストークン付きURLのダメな使い方
そのような場合が起きる実装として以下のようなものがあります
画像URLをFirestoreに保存する
フォロワーのみ公開している画像をアクセストークン付きURLで保存していれば
そのドキュメントをreadする権限さえあれば、Storageのセキュリティルールの制限を超えて画像を閲覧することができます
もしStorageの画像をStorageのセキュリティルールを通して閲覧したい場合は、FirestoreにはファイルのRefを保存し、閲覧を行うクライアント内で
getDownloadURL()
を行う必要があります
画像がCDN等でキャッシュされる・する
CDN等で権限の認証が行われない場合、どのユーザーでもURLさえ知っていれば
Storageのセキュリティルールでの制限を超えて画像を閲覧することができます
基本的にはアクセストークン付きURLに関してはキャッシュを行わない実装をするべきです
既にアクセストークン付きURLが露出していた場合の対処
既存の実装でアクセストークン付きURLが露出していた場合は、アクセストークンの更新を行うことで既存のアクセストークンからのアクセスはブロックされ、新たなアクセストークンがついている場合閲覧が可能になります
アクセストークン付きURLの無効化・更新方法
該当ファイルのmetadataに登録されているfirebaseStorageDownloadTokens
フィールドに新しいアクセストークンを設定することで、以前に発行されたアクセストークンを無効にすることが可能です。
次の実装例は特定のフォルダ以下のファイルにアクセストークンの更新を行う処理です
実装例
npm i uuid
npm i firebase-admin
import * as admin from "firebase-admin";
const { v4: uuidv4 } = require("uuid");
const serviceAccount = require("./firebase-admin-service-account.json");
admin.initializeApp({
credential: admin.credential.cert(serviceAccount),
});
const storage = admin.storage();
const bucketName = `${serviceAccount["project_id"]}.appspot.com`;
const bucket = storage.bucket(bucketName);
async function updateMetadataInDirectory(directory: string) {
const options = {
prefix: directory,
delimiter: "", // サブディレクトリも含めてリストアップするためにはdelimiterを空に設定
};
try {
// ディレクトリ以下のファイルをリストアップ
const [files] = await bucket.getFiles(options);
// // 各ファイルのメタデータを更新
for (let index = 0; index < files.length; index++) {
const file = files[index];
const customToken = uuidv4();
const newMetadata = {
metadata: {
firebaseStorageDownloadTokens: customToken,
},
};
await file.setMetadata(newMetadata);
console.log(files.length, index, `Metadata updated for file: ${file.name}`);
}
console.log("Metadata updated successfully.", files.length);
} catch (error) {
console.error("Error updating metadata:", error);
}
}
updateMetadataInDirectory("users");
処理内容
- 実行する環境を指定
Firebase Admin SDKを導入し、適切なサービスアカウントを選択します。
Admin SDKからFirebase Storageを初期化し、操作するバケットを設定します。コードではサービスアカウントに記述されたデフォルトのバケットが設定されるように実装されています - ディレクトリ内のファイル処理
指定されたディレクトリ内のすべてのファイルをリストアップします。この例ではusers
ディレクトリが対象です。 - アクセストークンの生成と設定
各ファイルに対して新しいuuidv4()
で生成される一意のトークンを生成し、firebaseStorageDownloadTokens
メタデータフィールドに設定します。これにより、既存のアクセストークンが失効し、新しいトークンに更新されます。
セキュリティ上の考慮事項
- 新しいトークンは、ファイルごとに異なるべきです。これにより、特定のファイルへの不正アクセスを防ぐことができます。
- 定期的にトークンを更新することで、セキュリティをさらに強化できます。
終わりに
getDownloadURL()
での処理内容を理解していない限り、比較的簡単にこのトラップを踏む可能性があります。
Cloud Storageは使い方さえ間違えなければ非常に良いサービスです。安全に使いましょう。
この記事は、コード・文章共にほぼChatGPTで書きました。
Discussion