アプリのプライバシーポリシーは記憶ではなくマニフェストから書く

24件のストア掲載が雛形のままのポリシーを指していた。17本のアプリをpubspec・Info.plist・マージ後のAndroidマニフェストから作り直した記録。

何年ものあいだ、私たちのアプリの App Store・Google Play 掲載24件は、1つのプライバシーページを指していました。改めてよく読むと、それは韓国語の汎用テンプレートで、テンプレート自身の記入例の文字「(예시)」(=例)がそのまま残っていました。会員登録、決済、配送について書かれていましたが、どのアプリにもそんな機能はありません。逆に、全アプリが実際にしていること、つまり広告IDを使った広告配信と、8本のアプリでのプッシュ通知の登録には一言も触れていませんでした。

記憶で書いたポリシーは、こうやって実態からずれていきます。この記事では、アプリの中身からポリシーを作り直した方法と、ソースファイルには現れずAndroidのビルドが追加していたものを紹介します。

1. 事実を知っている3つのファイルを読む

17本のFlutterアプリそれぞれから、次の情報を抜き出しました。

ファイル わかること
pubspec.yaml データを収集・送信するSDK:google_mobile_ads、firebase_messaging、firebase_analytics、supabase_flutter、google_mlkit_*、image_picker など
ios/Runner/Info.plist すべての NS…UsageDescription。アプリが求めうるiOSの権限
android/app/src/main/AndroidManifest.xml 宣言されたAndroidの権限(不完全。2を参照)

全アプリを回すシェルのループで十分でした。

for a in */; do
  sdk=$(grep -hoE "^\s+(google_mobile_ads|firebase_messaging|firebase_analytics|supabase_flutter|image_picker|google_mlkit[a-z_]*|geolocator|mobile_scanner)\b" $a/pubspec.yaml | tr -d ' ' | sort -u | tr '\n' ,)
  ios=$(grep -hoE "NS[A-Za-z]+UsageDescription" $a/ios/Runner/Info.plist | sort -u | tr '\n' ,)
  echo "$a | $sdk | $ios"
done

17本の集計:

データ アプリ数
広告ID(AdMob)、iOSのトラッキング許可(ATT) 17本すべて
プッシュ通知トークン(FCM)+ Firebase Analytics 8
写真ライブラリ 4
カメラ 3
位置情報 2
連絡先 1(QRスキャナー:読み取った連絡先の保存)
マイク 1(電光掲示板:音声付きの画面録画)
ニックネーム・スコア・国・端末IDをランキング(Supabase)へ 1(ゲーム)

人相占いアプリの顔の解析は、Google ML Kit で端末内で行っています。コードからアップロード処理(multipartリクエスト、画像データのPOST)を探しても見つからず、画像は端末から出ていませんでした。これはポリシーに書く価値があります。「カメラ+顔」と並ぶと、何も書かなければ最悪の想定で読まれるからです。

2. 本当のAndroid権限はマージ後のマニフェストにある

ソースのマニフェストが、そのまま出荷されるわけではありません。ビルド時にGradleが、すべてのプラグインとライブラリのマニフェストをマージします。5本のアプリについて、ソースとマージ後のリリース用マニフェストを比べました。

アプリ ソースの権限数 マージ後
日本の宝くじ 6 14
人相占い 8 16
QRスキャナー 10 22
電光掲示板 11 19
電卓 6 13

マージレポート(build/app/outputs/logs/manifest-merger-release-report.txt)には、追加された権限ごとに追加元が書かれています。QRスキャナーの例:

CAMERA                     [:mobile_scanner]
INTERNET                   [:google_mobile_ads]
ACCESS_NETWORK_STATE       [:wifi_iot]
CHANGE_NETWORK_STATE       [:wifi_iot]
WRITE_SETTINGS             [:wifi_iot]
ACCESS_ADSERVICES_AD_ID    [com.google.android.gms:play-services-ads-api:25.3.0]
ACCESS_ADSERVICES_ATTRIBUTION  [com.google.android.gms:play-services-ads-api:25.3.0]
ACCESS_ADSERVICES_TOPICS   [com.google.android.gms:play-services-ads-api:25.3.0]
RECEIVE_BOOT_COMPLETED     [com.google.android.gms:play-services-ads-api:25.3.0]
WAKE_LOCK                  [com.google.android.gms:play-services-measurement-sdk-api:20.1.2]
FOREGROUND_SERVICE         [androidx.work:work-runtime:2.7.0]

調べた5本すべてに、広告SDKから ACCESS_ADSERVICES_* の3つ(広告ID・アトリビューション・トピックを扱うAndroidのプライバシーサンドボックスAPI)が追加されていました。これは広告ネットワークとのデータ共有であり、ポリシーにもGoogle Playの「データセーフティ」の回答にも反映しなければなりません。ソースのマニフェストにはどれも現れていませんでした。

Gradleの中身を見ずにビルドを確かめるなら、APKから直接読み出せます。

aapt dump permissions app-release.apk

3. 文章ではなくデータにする

17本分のポリシーを手で書く代わりに、アプリごとのデータ項目のリストを1つの設定ファイルにまとめ、ページはそこから生成するようにしました。

- id: true_scanner
  name: {en: True Scanner, ja: 本当のスキャナー, ko: 진짜 스캐너}
  data: [ads, camera, contacts, location]
- id: facecheck
  data: [ads, push, camera, photos, face]

項目ごとに、確認済みの文を言語ごとに1つ用意しました(例:「カメラ:撮影した画像は端末内でのみ処理し、サーバーへ送信しません」)。各アプリのページには、そのアプリの項目だけが並びます。共通ポリシーには全体の表を載せ、1つのURLで全アプリをカバーしました。24件のストア掲載が古い1つのアドレスを指しているので、そのアドレスも同じURLのまま新しい内容を返すようにしています。SDKを追加したら、1か所のリストを直せば全ページが作り直されます。

4. 説明できない権限は、それ自体が発見

比較から、どんなテンプレートでも拾えなかったことが2つ出てきました。

  • QRスキャナーは、自身のマニフェストで精密な位置情報とBluetoothを宣言していました。Androidでは周囲のWi-Fiをスキャンするのに位置情報の権限が必要なので、Wi-FiのQRコード機能と合っています。それでも、推測で済ませずコードで確かめる必要がありました。
  • 最初に生成したポリシーは、位置情報を「地域に合わせた表示のみ」と書いていました。カレンダーアプリには正しく、スキャナーには誤りです。両方に当てはまるよう書き直しました(「それを必要とする機能の利用時のみ…当方のサーバーへは送信しません」)。

リリースのたびに行うチェック

  • [ ] マージ後のマニフェスト(または aapt dump permissions)を前回のリリースと比べる
  • [ ] 新しい権限ごとに、マージレポートで追加元のプラグインを探し、必要な理由を書き残す
  • [ ] アプリのデータ項目を更新し、あわせてポリシーとGoogle Playのデータセーフティの回答も更新する
  • [ ] Info.plist の利用目的の文言を確認する(Appの審査でも読まれる)
  • [ ] ポリシーのページを作り直し、ストアに登録したURLが200を返すことを確かめる

プライバシーポリシーは、出荷されるバイナリの説明書です。正確さを保ついちばん確実な方法は、ビルドが使うのと同じファイルからポリシーを生成することです。

よくある質問

Flutterアプリの本当のAndroid権限はどのファイルを見ればわかる?

android/app/src/main/AndroidManifest.xml ではなく、ビルドが生成するマージ後のマニフェスト(build/app/intermediates/merged_manifest/release/…/AndroidManifest.xml)か、manifest-merger のレポートです。私たちのアプリでは、ソースの6〜11個に対し、マージ後は13〜22個の権限がありました。

増えた権限はどこから来る?

プラグインとSDKからです。マージレポートには追加元が書かれています。Google Mobile Ads SDK が AdServices 系の権限3つと RECEIVE_BOOT_COMPLETED を、Wi-Fi系プラグインが WRITE_SETTINGS と CHANGE_NETWORK_STATE を、スキャナー系プラグインが CAMERA を追加していました。

宣言された権限はすべてポリシーに書く必要がある?

すべてではありません。ポリシーが説明するのは、収集・共有するデータです。ただし、どの権限にも説明できる理由が必要で、説明できない権限こそ最初に見直す対象です。

  • #プライバシーポリシー
  • #Flutter
  • #AndroidManifest
  • #Info.plist
  • #AdMob
  • #Google Play

t.laboratory

実際に作って運用しているアプリ・サーバー・サービスで起きた問題と、その解決に使ったコードを記録する技術ブログです。

運営者情報 · アプリ

↑