2
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Firebase App Checkでアプリを保護する 悪意のあるアプリからの攻撃もやってみた

Last updated at Posted at 2023-04-18

はじめに

Firebaseを利用してアプリを開発する場合、Firebae Authenticationでユーザー認証を行う事で、ユーザーを保護するという事を行うだろう。そしてCloud Firestoreなどではセキュリティルールにより、データを保護する事になる。

ただ、Firebaseではアプリの設定情報(firebaseConfig)は公開される事を前提にしており、そのためFirebaeのアプリの設定情報をそのままそっくりコピーすれば別のアプリであってもFirebaseの各リソースへのアクセスを要求する事はできる。

そこで今回はApp Checkを導入する事でアプリ(クライアント)の真正性を証明し、アプリを保護するという事をやってみたいと思う。
また、App Check未導入時にはどういう事が起きうるか?も「おまけ」で実際に実装してみて検証してみた。

※ちなみに、以下のように環境変数から値を設定するように実装していても、ビルドされてWeb上で表示される際には、この情報はJavaScriptのどかに存在し、隠蔽することにはならないので注意が必要。

const firebaseConfig = {
	apiKey: import.meta.env.VITE_FIREBASE_API_KEY,
	authDomain: import.meta.env.VITE_FIREBASE_AUTH_DOMAIN,
	projectId: import.meta.env.VITE_FIREBASE_PROJECT_ID,
	storageBucket: import.meta.env.VITE_FIREBASE_STORAGE_BUCKET,
	messagingSenderId: import.meta.env.VITE_FIREBASE_MESSAGING_SENDER_ID,
	appId: import.meta.env.VITE_FIREBASE_APP_ID,
	measurementId: import.meta.env.VITE_FIREBASE_MEASUREMENT_ID
};

以下は上記のコードをビルドした後のファイルの一部(開発途中なアプリなためマスキングしている)。
image.png

App Checkを導入する

導入にあたっては以下の手順で行う。

  1. reCAPTCHAへの登録
  2. FirebaseでApp Checkを設定する
  3. アプリ側でApp Checkの実装をする
  4. 実際にDeployしてみて、App Checkの状態を確認してみる

reCAPTCHAへの登録

reCAPTCHAのv3 Admin Consoleから新規にsiteKeyとsecretKeyを生成する。

AdminコンソールにアクセスするとデフォルトではEnterpriseになっているので、Switch to create a classic keyからフリーのreCAPTCHA登録に切り替える。
image.png

登録内容はFirebaseのHostingを利用するのであればそのドメインを登録すればいい。
image.png

登録後に払いだされるsiteKeyとsecretKeyを控えておく(siteKeyもマスクしているが、これはフロントエンドに存在するキーで本来的には公開して問題ない)。
image.png

FirebaseでApp Checkを設定する

続いて、FirebaseのコンソールからApp Checkの設定をしていく。以下の画面の登録からreCAPTCHAを選択して、上記のreCAPTCHAへの登録で取得したsecretKeyを設定する。
image.png
image.png
image.png

これでFirebaseの設定は完了になる(実際には、ローカル環境から本番のFirebaseリソースにアクセスする際には、ウェブアプリのデバッグ プロバイダで App Check を使用するに書かれている追加設定が必要になるが、それについては後程取り上げる)。

アプリ側でApp Checkの実装をする

実装は特に難しい事はなく、以下のように初期化するだけでいい。

...
import {
	initializeAppCheck,
	ReCaptchaV3Provider,
	getToken
} from 'firebase/app-check';

const firebaseConfig = {
	...
};

const app = initializeApp(firebaseConfig);
...
const appCheck = initializeAppCheck(app, {
	provider: new ReCaptchaV3Provider(
		import.meta.env.VITE_FIREBASE_RECAPTCHA_SITEKEY
	),
	isTokenAutoRefreshEnabled: true
});
getToken(appCheck)
	.then(() => {
		console.log('AppCheck:Success');
	})
	.catch((error) => {
		console.log(error.message);
	});
...

実際にDeployしてみて、App Checkの状態を確認してみる

以下はFirestoreにリクエストをした際のApp Checkのモニタリング結果。青はApp Checkを通った正規のリクエストで、黄色はApp Checkトークンがないリクエストを示している(以下はモニタリングの結果を確認するために、敢えてApp Checkトークンなしのリクエストを何回かしたもの)。
image.png

ここまでApp Checkの導入は終わり。以降でローカル環境でApp Checkありの開発を行うための方法を見ていく(Firebase Local Emulator Suiteを利用しているのであればこの対応は不要だが、ローカルの開発環境から本番のFirebaseのリソースにアクセスするには必要になる)。

ローカル環境からのリクエストでもApp Checkをクリアするようにする

まず、既存の実装に以下のようなコードを追記し、window.self.FIREBASE_APPCHECK_DEBUG_TOKENをtrueにする。

if (import.meta.env.DEV) window.self.FIREBASE_APPCHECK_DEBUG_TOKEN = true; // <- これを追加
const appCheck = initializeAppCheck(app, {
	provider: new ReCaptchaV3Provider(
		import.meta.env.VITE_FIREBASE_RECAPTCHA_SITEKEY
	),
	isTokenAutoRefreshEnabled: true
});

これを追記すると、chromeのconsole上にデバックトークンが表示される。
image.png

これをFirebaseコンソールのApp Checkから登録すればいい。
image.png
image.png

これでローカルの開発環境からFirebaseへのリクエストが問題なく通るようになる。

まとめ

今回は、App Checkを利用してアプリを保護するという事をやってみた。App Checkにより認証やセキュリティールールによるユーザーの保護だけでなく、アプリ(クライアント)の保護もできるので、本番のリリース前には対応するのがいいだろう。

おまけ

firebaseConfigをコピーした別アプリからユーザー情報を取得する(アプリになりすまされた場合に起こる事)

簡易的に以下のようなコードで悪意のあるアプリを再現してみた。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <link
      href="https://cdn.jsdelivr.net/npm/vuetify@3.1.13/dist/vuetify.min.css"
      rel="stylesheet"
    />
  </head>
  <body>
    <div id="app">
      <v-app>
        <v-app-bar density="compact">
          <v-app-bar-title>ダミーアプリ</v-app-bar-title>
        </v-app-bar>
        <v-main class="bg-grey-lighten-5">
          <v-container>
            <v-row>
              <v-col cols="12">
                <v-btn @click="login">Googleでログイン</v-btn>
              </v-col>
              <v-col cols="12">
                <v-btn @click="getUser">user情報をFirestoreから取得</v-btn>
              </v-col>
            </v-row>
          </v-container>
        </v-main>
      </v-app>
    </div>
  </body>
  <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/vuetify@3.1.13/dist/vuetify.min.js"></script>
  <script type="module">
    const { createApp } = Vue;
    const { createVuetify } = Vuetify;
    import { initializeApp } from "https://www.gstatic.com/firebasejs/9.15.0/firebase-app.js";
    import {
      getAuth,
      GoogleAuthProvider,
      signInWithPopup,
    } from "https://www.gstatic.com/firebasejs/9.15.0/firebase-auth.js";
    import {
      getFirestore,
      doc,
      getDoc,
    } from "https://www.gstatic.com/firebasejs/9.15.0/firebase-firestore.js";
    import {
      initializeAppCheck,
      ReCaptchaV3Provider,
    } from "https://www.gstatic.com/firebasejs/9.15.0/firebase-app-check.js";

    const firebaseConfig = {
      apiKey: "...",
      authDomain: "....firebaseapp.com",
      projectId: "...",
      storageBucket: "....appspot.com",
      messagingSenderId: "...",
      appId: "...",
      measurementId: "...",
    };

    const app = initializeApp(firebaseConfig);
    const auth = getAuth(app);
    const provider = new GoogleAuthProvider();
    const db = getFirestore(app);
    // 以下は「無効なリクエスト」を再現するためのコード
    // const appCheck = initializeAppCheck(app, {
    //    provider: new ReCaptchaV3Provider("dumydumydumydumydumydumydumy"),
    //    isTokenAutoRefreshEnabled: true,
    //  });

    createApp({
      data() {
        return {
          message: "Hello Vue!",
          uid: null,
        };
      },
      methods: {
        login() {
          signInWithPopup(auth, provider)
            .then((result) => {
              const credential =
                GoogleAuthProvider.credentialFromResult(result);
              const token = credential.accessToken;
              const user = result.user;

              this.uid = user.uid;
              console.log(this.uid);
            })
            .catch((error) => {
              ...
            });
        },
        async getUser() {
          const userDocSnap = await getDoc(doc(db, "users", this.uid));
          console.log(userDocSnap.data());
        },
      },
    })
      .use(createVuetify())
      .mount("#app");
  </script>
</html>

上記のコードをVS CodeのLive Serverで立ち上げてログイン後にFirestoreからデータを取得できるか?検証してみると、簡単にデータを取得できてしまう。
image.png

上記のリクエストをApp Checkのモニタリングで確認してみる。
App Checkのトークンがない古いSDKからのリクエストとしか表示されないのでApp Checkを有効にしていない場合、簡単にデータが取られてしまうし、悪意のある書き込みを正規の認証後に行える状態になってしまう(無効なリクエストはダミーのApp Checkトークンを設定してみた場合のモニタリング結果)。
image.png

上記のように他のアプリ(クライアント)からのリクエストを遮断するために、App Checkを適用する必要がある。
image.png

App Checkを適用した後、同じように悪意のあるアプリからリクエストを行ってみると、今度は以下のようにエラーになり、リクエストが遮断される事が確認できる。
image.png

モニタリング上は、先ほどと変わらずクライアント リクエストが古いとして表示される。
image.png

※別の話として、本番リリース後にはFirebase Authenticationの承認済みドメインからlocalhostを削除できなら削除すべきだろう。これにより、ローカルの環境からの認証ができなくなるので、今回作成したような悪意のあるアプリで認証できてしまうという事を防げる。

2
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?