はじめに
どうも、大学院生エンジニアのつかちゃんと申します。
普段業務でFirebase Storageを扱っているのですがサービスが拡大し、機密性の高い画像を扱う機会が増えてきました。 バイブコーディング時代の今、セキュリティが必須になっていることも言うまでもありません。
またそこで、Firebase Storageの標準的な仕様の中に、セキュリティ的に「ここは気をつけないと危ない!」というポイントを見つけたので、その共有と対策(署名付きURL)についてお話しします。
この記事に向いている人
- Firebase Storageを使っている人
- Firebase Storageを使用してみたい人
- セキュリティレベルをもう一段階上げたい人
よくあるセキュリティ対策(基本編)
Firebase Storageの基本のセキュリティの高め方として以下が挙げられます
- Firebase Storageルールの記述
- サーバーサイドで画像を扱う
Firebase Storageを使う際、まず最初に行う基本的なセキュリティ対策は以下の2つです。
1. セキュリティルール(Security Rules)の設定
公式ドキュメントにある通り、認証済みユーザーのみに読み書きを許可する設定です。
- read, writeを認証の有無で許可する(できればcreate, read, update, delete)
service firebase.storage {
match /b/{bucket}/o {
match /someFolder/{fileName} {
// ログインしているユーザーのみ許可
allow read, write: if request.auth != null;
}
}
}
2. クライアントから直接アクセスさせない(サーバー経由)
画像を一度サーバーで受け取り、認証やバリデーションを行ってから保存する方法です。これならルールに関係なく、サーバー側で厳密に制御できます。
しかし、これだけでは防げない「落とし穴」がある
上記の対策をしていても、実は「URLを知っていれば誰でもアクセスできてしまう」というリスクが潜んでいます。
コンソールに潜む「アクセストークン」
Firebase Storageのコンソールを見てみましょう!
Firebase Consoleでアップロードされたファイルのメタデータを見ると、アクセストークンという項目があります。 これはいわゆる「ダウンロード用URL」の一部で、長期的に有効期限がないなトークンです。
万が一このURLが流出してしまうと、セキュリティルールに関係なく、誰でも・いつまでも画像が見られる状態になってしまいます。 「じゃあ、トークンを削除すればいいのでは?」と思いますよね。
恐怖!蘇る「ゾンビトークン」🧟
実は、Firebase経由で作成されたバケットは、デフォルトで「公開アクセス」を許容する設定になっていることが多いです。そのため、コンソール上でトークンを取り消しても、またすぐに新しいトークンが自動生成されてしまうことがあります。 私はこれをゾンビトークンと呼んでいます🧟。
原因としてFirebaseからバケットを作成すると以下のように公開アクセスが「オブジェクトのACLに準拠」という形式になります。おそらくこの設定がゾンビトークンの原因だと考えられます。
対策:バケットを「完全非公開」にして「署名付きURL」を使う
手順1. Google Cloudコンソールからバケットを作成
この問題を解決するベストプラクティスが、Google Cloud Storage (GCS) 側で非公開バケットを作成し、署名付きURLでアクセスする方法です。
- Firebase Storageからでなく、Google Cloud (Google Cloud Storage)
からバケットを作成 - 「このバケットに対する公開アクセス禁止を適応する」にチェック(アクセス方法あり)
これで、ゾンビトークンが勝手に作られることはなくなり、インターネットからの直接アクセスは完全に遮断されます。
手順2. 「署名付きURL」を使ってアクセスする
こちらはGoogle Cloudの公式ドキュメントでもベストプラクティスとして紹介されています。
バケットを非公開にすると、普通の方法では画像のアップロードや表示ができなくなります。 そこで使うのが署名付きURLです。
署名付きURLとは? 「5分間だけアップロードを許可する」「1分間だけ表示を許可する」といった、期間限定の通行手形付きURLのことです。
実装フロー
- クライアントがサーバーに「画像を上げたい(or 見たい)」とリクエスト
- サーバー(Firebase Admin SDK)が、権限を確認した上で「署名付きURL」を発行
- クライアントは、そのURLに対して直接ファイルをアップロード(or 表示)
実装コード例
サーバー側 (TypeScript / Cloud Functionsなど)
サーバー側では秘密鍵(Service Account)を使ってURLに署名を行います。
import * as admin from "firebase-admin";
import { defineSecret } from "firebase-functions/params";
// バケット名は環境変数やシークレットマネージャーで管理
const PRIVATE_STORAGE_BUCKET = defineSecret("PRIVATE_STORAGE_BUCKET");
// 有効期限を設定(短ければ短いほどセキュア)
const SIGNED_URL_TTL_MS = 1 * 60 * 1000; // 1分間だけ有効
export const getUploadUrl = async (req, res) => {
// ここでユーザーの認証チェックを行う
// if (!req.user) throw new Error("Unauthorized");
const filePath = `images/private/${req.body.filename}`;
// バケット情報を取得
const bucket = admin.storage().bucket(PRIVATE_STORAGE_BUCKET.value());
const file = bucket.file(filePath);
// 署名付きURLの生成
const [url] = await file.getSignedUrl({
version: "v4",
action: "write", // アップロード用なら 'write'、閲覧用なら 'read'
expires: Date.now() + SIGNED_URL_TTL_MS,
contentType: req.body.contentType, // ファイルタイプを固定するとより安全
});
return { url };
};
フロント側
サーバーから受け取ったURLに対して、直接 fetch でアップロードします。
const uploadImage = async (targetFile: File) => {
// 1. サーバーから署名付きURLを取得
const { url } = await getSignedUrlFromServer(targetFile.name, targetFile.type);
// 2. 取得したURLに対して直接PUT送信
const response = await fetch(url, {
method: "PUT",
headers: {
"Content-Type": targetFile.type,
},
body: targetFile,
});
if (response.ok) {
console.log("アップロード成功!");
}
};
まとめ
まとめるとこの記事のポイントは以下になります。
- Firebase Storageのデフォルト設定では、長期有効なトークン(ゾンビトークン)が発生する可能性
- 重要な画像を扱う場合は、Google Cloud側で「公開アクセス禁止」を設定したバケットを使うのが安全
- 画像の読み書きには「署名付きURL」を使うことで、期限付きのセキュアなアクセス権限を付与できる
現在はAIによるコーディング(バイブコーディング)も流行っていますが、セキュリティの根幹部分は人間がしっかり理解して設計する必要があります。 より堅牢なアプリケーションを目指して、ぜひ署名付きURLを活用してみてください!
こちらの記事が参考になれば幸いです。最後まで読んでいただきありがとうございました!!
