- モバイルデーティングアプリ Feeld のバックエンド調査で、プロフィールの露出、メッセージの閲覧・変更、チャット添付ファイルへのアクセスなど8件の脆弱性が見つかり、1件目を除く残りは OWASP Top 10 の Broken Access Control に該当する
- 通常ユーザーはアプリ画面では限られた情報しか見られないが、プロキシでレスポンスを確認すると、「いいね」を送ったユーザーの年齢、距離、プロフィール写真、streamUserId などの プレミアム相当の情報 を受け取れていた
- 複数の脆弱性は streamUserId、profileId、messageId、channelID といった識別子を別の API レスポンスから取得し、リクエストパラメータに入れる形でつながっており、他人のメッセージ・マッチ・プロフィール・いいね・チャット送信にまでアクセス範囲が広がる
- チャット添付ファイルは、通常写真、5〜15秒制限の写真、通常動画、1回再生動画のすべてで問題が確認され、一部の Cloudinary・Stream CDN URL は 認証なしでアクセス 可能だった
- FORTBRIDGE は2024年3月8日に Feeld へ問題を開示し、Feeld は複数回にわたり公開延期を要請した後、2024年8月16日に残りの項目を緩和する変更を実装したと回答し、ブログは2024年9月10日に公開された
Feeldで確認された脆弱性の範囲
- 対象は Tinder、Bumble に似たモバイルデーティングアプリ Feeld で、距離・年齢・性別・カップル・位置情報に基づくフィルターを提供している
- プレミアムユーザーは kink の種類、threesome/group シナリオ、関心のある関係タイプでも検索できる
- セキュリティ調査で確認された脆弱性は8件
- 非プレミアムユーザーへのプロフィール情報の露出
- 他人のメッセージの読み取り
- チャット写真・動画添付ファイルへの認証なしアクセス
- 他人のメッセージの削除・復元・編集
- 他人のプロフィール情報の更新
- 任意ユーザープロフィールから「Like」を受け取る
- 他人のチャットにメッセージを送る
- 他人のマッチを見る
- 1件目を除く残りの問題は OWASP Top 10 の Broken Access Control カテゴリに該当する
非プレミアムユーザーに露出していたプロフィール情報
- 通常ユーザーがアプリの Likes メニューで自分を好きになったユーザーを見ると、名前とぼかされた写真程度しか表示されない
- Burp のようなプロキシツールでリクエストとレスポンスを傍受すると、レスポンス内にプレミアムユーザーと同水準の情報が含まれていた
- 年齢
- 距離
- 完全なプロフィール写真
- streamUserId
- プロフィール写真は
res.cloudinary.com に保存されており、認証なしでアクセス可能だった
- レスポンスから得た streamUserId は、その後、他人のメッセージを読む脆弱性に利用できる
メッセージとマッチのアクセス制御問題
- 他人のメッセージを読むには被害者の streamUserId が必要で、この値は複数の API リクエストで露出する
- 例としては、
DiscoverProfiles GraphQL リクエストのレスポンスから対象ユーザーの streamUserId を得た後、チャットチャネルリクエストの member 条件にその値を入れる流れ
- レスポンスで
"text" を検索すると、被害者がやり取りしたメッセージ数と内容を確認できた
- 同じアクセス方法で各メッセージに付く messageId も取得でき、この値はメッセージの削除・復元・編集に使われる
ChatListQuery の脆弱な profileId パラメータを変更すると、別のユーザーのマッチを見ることができた
- 確認可能な情報には imaginaryName、年齢、写真、性別、sexuality、status、生年月日が含まれる
チャット添付ファイルへの認証なしアクセス
- チャットで共有される添付ファイルは写真と動画に分かれる
- 写真は再生可能な通常写真、または5〜15秒制限の写真
- 動画は再生可能な通常動画、または1回再生動画
- 通常写真は Feeld アプリから
api.cloudinary.com にアップロードされ、レスポンスとして photo_id が返される
- その後、写真は
feeld.co にコピーされ、認証済みユーザーに提供される
cdn/chat-attachment/<receiver_profileId>/<photo_id> または <sender_profileId>/<photo_id> 形式のパスが使われる
- パスの profileId 部分は、最小1文字の任意文字列に短縮しても認証済みユーザーに写真が返される
- 先頭に
/v1/ を付けたパスは Cloudinary に保存された元写真の URL を返し、その URL は認証なしでアクセス可能だった
- 時間制限付き写真では、アップロード時に
visibilityMilliseconds:15000 のような追加パラメータが使われる
- 受信者向けエンドポイントでは、アクセス後5〜15秒後に写真が削除され、それ以上アクセスできなくなる
- アップローダーの profileId を使ったエンドポイントでは、5〜15秒後も認証済みユーザーに写真を返し続ける
/v1/ パスは Cloudinary URL を返し、その URL は認証なしでアクセス可能だった
- 動画は通常動画と1回再生動画のどちらでも、URL がチャットメッセージに含まれる
- 通常動画は
us-east.stream-io-cdn.com にアップロードされる
- 1回再生動画は
chat.stream-io-api.com 側のアップロードフローが使われる
- 攻撃者が前述のメッセージ読み取り脆弱性で URL を取得し、
u0026 を & に置き換えると、認証なしで視聴できた
- 1回再生動画は攻撃者には再再生可能だったが、受信者のアプリでは一度見た後に
video expired と表示される
メッセージ操作、プロフィール変更、いいねの偽装
chat.stream-io-api.com/messages/<messageId> エンドポイントで DELETE と PUT メソッドにより他人のメッセージを扱えた
- 削除されたメッセージはチャットで
This message was deleted と表示されるが、攻撃者が同じ DELETE リクエストを呼び出すと元のメッセージを取り戻せた
- 攻撃者はチャット参加者でなくても messageId を使ってメッセージを編集できた
- 被害者が通知をタップしたとき、編集後のメッセージを見ることになる
- メッセージの下には
edited 表示があるが、誰が編集したかは表示されない
- アカウント名は一意ではなく、編集可能
ProfileUpdate GraphQL リクエストの脆弱な id パラメータを被害者の ID に変えると、名前、sexuality、年齢、bio などの プロフィール情報 を更新できた
ProfileLike GraphQL リクエストでは、profile#1 としてログインした状態で、profile#2 が profile#3 に「Like」を送ったようにできた
- 例では、任意のプロフィールから自分のプロフィールへ Like を送った後、プレミアムアカウントの Likes 一覧にその Like が表示された
他人のチャットにメッセージを送る
- 攻撃者はチャット参加者でなくても、他人のチャットにメッセージを送ることができた
- 必要な値は、前述のメッセージ読み取り脆弱性で得た channelID
channels/messaging/<channelID>/message パスに POST リクエストを送ると、そのチャネルにメッセージが追加される
- 被害者は通知を受け取り、メッセージを確認できる
- システムは通知が攻撃者の名前から来たものとして表示するが、攻撃者はプロフィール名を変更でき、名前は一意ではない
公開タイムライン
- 2024年3月8日、FORTBRIDGE が Feeld にすべての問題を開示
- 同日、Feeld はテストに使用したアカウント情報を要求
- 2024年4月2日、FORTBRIDGE が更新を要求し、Feeld は調査中として公開保留を要請
- 2024年5月28日、Feeld は複数の修正をデプロイし、発見事項が解決されたか確認するため最大2週間の遅延を要請
- 2024年6月8日、最初の開示メールから3か月が経過
- 2024年7月15日、Feeld は一部の問題にはより複雑な修正が必要だと回答
- 2024年8月4日、Feeld は残りの項目が解決するまで公開を保留してほしいと要請
- 2024年8月16日、Feeld は残りの発見事項を緩和する変更を実装したと回答
- 2024年9月8日、最初の開示から6か月が経過
- 2024年9月10日、ブログが公開
- 2025年8月、この研究が DEF CON 33 で発表された
1件のコメント
Hacker News の意見
権限チェックをフロントエンドだけで実装しているように見え、1つ2つのエンドポイントではなく、ほぼ全体にわたってそうらしい。
概念的には避けやすいミスだが、似たようなミスは認めたくないほど頻繁に見てきた。
「すべての権限をバックエンドでチェックせよ」という解決策は、バッファオーバーフローにおける「すべての箇所に境界チェックを入れろ」に似ているように感じる。コミュニティ全体として何をすべきかは分かっているが、全員に一貫して適用させるのは簡単ではない。
一方、権限チェックは特定の境界で行われるもので、アプリケーションの設計方法に関わる。プロジェクトの開発方法に影響を及ぼせるときはいつも、バックエンド API 開発とフロントエンドのクライアントコードを明確に分離しようと主張してきた。経験上、この種の問題を避けやすく、テストもしやすくなり、開発者向け API も「タダで」手に入る。正直、それがこの方式を好む主な理由だ。
分かったきっかけは、lamp アカウントの所有者から、自分のデータが突然全部消えたと連絡があったこと。ログを見ると、Google Bot が内部管理画面の「Delete」リンクを全部クリックしていた。JavaScript はオプトインなので起きたことだった。開発者に電話して何をしたのか説明し、その日、Web 系の人たちへの信頼を大きく失った。
見るたびに指摘しているが、クライアント API の範囲についてあまりにも考えが足りない場合があり、かなり心配になる。
ジュニア、ノーコード、AI コードのせいにしたくなるが、自分も彼らと同じくらい怠け者なので、ただ首を振って流すことになる。
正確な個人情報を入れるべきではない、とても良い理由だ。例えば生年月日など。
特にデーティングアプリはこうした情報を求めるようだが、そうしない方がいい。実際の誕生日から1年程度ずらした値を入れる方がましだ。
このデーティングアプリはそれほど有名ではないが、BDSM やグループセックスのような別の嗜好を持つ人々やクィアの利用者を対象としている。世界の多くの地域で、こうした情報が非常にセンシティブであることは言うまでもない。
今週、稼ぎが良いという理由でメディアによく出ていた。
https://www.theguardian.com/technology/article/2024/sep/08/t...
アプリのカテゴリを考えると、刑事上の過失レベルの失敗だ。
米国と EU での懲役の脅し、データ関連の保険、そしてデータ保険の費用だけが抑止力になりそうだ。写真が LinkedIn に載せてもよいようなものでないなら、目玉が飛び出るような価格を払うべきだ。
もちろん、インセンティブが隠蔽を促してはいけない。
オンラインデーティング分野はめちゃくちゃだ。有用と言えるサービスを持つ会社は2〜3社しかなく、その会社たちは邪悪か無能か、あるいはその両方だ。
そろそろオープンソースの連合型デーティングサービスのようなものが必要なのかもしれない。少なくとも、データを売らず、ヌード写真を流出させず、殴られたり、レイプされたり、殺されたりする羽目にしない何かが必要だ。言うほど簡単ではないだろうが。
ActivityPub には Person レコードの公開を通じてこれを可能にする構造もある。特に非モノガミー、非異性愛、ジェンダー非同調のニーズを優先すれば、革新の余地はとてつもなく大きい。
ただしデーティングアプリは参入が本当に難しい分野だ。有用になるには特定地域で蓄積されたユーザー規模が必要で、収益化すると必然的にアプリは使いにくくなる。okcupid が非営利的な性格をやめてから壊れたのには理由がある。
そしてモデレーション問題もある。
アナログ形式をデジタルコピーにしたいなら、それは個人の権利だが、どんなシステムも最終的には流出と配布を防げるほど安全ではなく、今後もそうはなれないことを知っておくべきだ。
特に若い人たちは、長期的に起こり得て、起こる可能性の高い結果や羞恥を考慮しない。そうした機能を提供するのは、悪い結果を招き入れるだけだ。
本当にひどい。セキュリティについて何も考えていなかったのは明らかだ。
ゲーム開発者だが、この会社が利用者を安全に保つことよりも、私たちはゲームを公平に保つことにずっと多くの労力をかけている。訴訟で叩きのめされるべきだ。
アプリがバグだらけだと気づく前から、興味関心セクションが何の文脈も提供していないのを見てとても驚いた。例えば、ほとんどの人が Domination や Submission を関心事にしていたが、どの役割を望んでいるのかという文脈がまったくなかった。その界隈でこれが根本的にどれほど間違っているか分からないということは、全般的に何も分かっていないということだ。
メッセージと非公開写真は別の問題だ。
挑発的に言えば、これは GraphQLの問題である
GraphQLはフロントエンドにデータをクエリさせてくれる。素晴らしくはあるが、バックエンド側から見ると非常に不透明で、たいていアクセス制御をまったく知らないサードパーティライブラリで実装される
アクセス制御をデータベース自体に実装しないのであれば、バックエンドコードでGraphQLクエリを解釈し、どのレコードを返すべきか、あるいは制限すべきかを判断するのは非常に難しい。データベースでやるのが最悪というわけではなく、フロントエンドでやるよりは確実にましだ
バックエンドで適切なアクセス制御を実装するには、クエリを理解し、データベーススキーマを把握したうえで、「user_idがXXXなら、この文脈でこの画像を見られるのか/見られないのか」を判断するモデル、クラス、関数などを作る必要がある。GraphQLではフロントエンドで実装するほうがずっと簡単なので、彼らがそうしたのは明らかだ
GraphQLの実装が良かったと言っているわけでも、問題が全面的にGraphQLだけにあると言っているわけでもない。GraphQLはバックエンドがクエリを理解する必要をなくそうとするため、このような複雑なセキュリティ状況をより難しくし、その結果このミスを起こしやすくする、という意味だ
[0] 例えば特定の画像はユーザープロフィールでは公開アクセス可能だが、マッチした相手にだけ見える場合や、チャットの文脈でだけ見える場合(グループチャットを除く)、ブロックしたユーザーには常にアクセス不可の場合があり得る。この一例だけでも、複雑なエッジケースを大量に生み出し得る
ASTをいじったり、残りのクエリの文脈を理解したりする必要はない。写真を取得するresolverで「ユーザーABCはユーザーXYZの写真を見られるか?」に答えればよい。非効率なら一部のデータを事前取得するか、dataloaderを使えばよい
ただし、GraphQLをSQLに変換してくれる魔法のようなライブラリを使っているなら話は別だ
https://hasura.io/docs/2.0/security/allow-list/
簡単なアイデアとしては、データモデルに認可を実装する方法がある。GraphQLが、リクエストの文脈に応じて認可を実装できるリソースモデルに
getとlistを委譲するようにするのだ[1] https://www.apollographql.com/docs/apollo-server/security/au...
[2] https://docs.graphene-python.org/projects/django/en/latest/a...
驚くほど 責任感があり配慮された公開 だった
大して驚きはない。使ってはいるが、自分の銀行アプリと同じくらい無能に作られている、と表現するだろう。もしかするともっとひどいかもしれず、ほとんどまともに動かない
どうやってこんなものを作ったのか分からない
このアプリとFetlifeを見ると、これらのコミュニティには、品質に関係なく最初に出たアプリに居座り続ける大きな問題がある
その後しばらくして、新アプリと新サーバーを全員に一斉展開するフラッグデーを実施し、ほとんどの人はログインすらできなかった。ログインできた人でも有料顧客ならプレミアム特典を失い、「いいね」やチャットが消えるなどの問題が起きた。私は結局ログインできず、その時点でアプリを捨てた
研究者たちが公開をここまで長く我慢したことに、正直驚いている
こんな深刻な プライバシーの穴 を塞げとひどいスタートアップに6か月も与えれば、そもそもこうした情報を収集できる特権を悪用し続けるだろう。2か月だけ与えて公開すべきだと思う。人々の私的な情報でサイコロ遊びをしてはいけないことを学ぶべきだ
例: https://news.ycombinator.com/item?id=41517747