1 điểm bởi GN⁺ 2024-09-13 | 1 bình luận | Chia sẻ qua WhatsApp
  • Trong quá trình rà soát backend của ứng dụng hẹn hò di động Feeld, đã phát hiện 8 lỗ hổng gồm lộ thông tin hồ sơ, đọc/sửa tin nhắn, truy cập tệp đính kèm trong chat; tất cả ngoại trừ lỗ hổng đầu tiên đều thuộc nhóm Broken Access Control trong OWASP Top 10
  • Người dùng thường trên giao diện app chỉ thấy thông tin hạn chế, nhưng nếu kiểm tra phản hồi bằng proxy thì có thể nhận được thông tin cấp premium như tuổi, khoảng cách, ảnh hồ sơ và streamUserId của người đã gửi “Like”
  • Nhiều lỗ hổng được xâu chuỗi bằng cách lấy các định danh như streamUserId, profileId, messageId, channelID từ các phản hồi API khác rồi đưa vào tham số request, từ đó mở rộng quyền truy cập tới tin nhắn, match, hồ sơ, lượt thích và cả việc gửi tin nhắn trong cuộc trò chuyện của người khác
  • Vấn đề được xác nhận trên mọi loại tệp đính kèm chat gồm ảnh thường, ảnh giới hạn 5–15 giây, video thường và video chỉ phát một lần; một số URL Cloudinary và Stream CDN có thể truy cập mà không cần xác thực
  • FORTBRIDGE đã tiết lộ vấn đề cho Feeld vào ngày 8/3/2024; sau nhiều lần Feeld đề nghị hoãn công bố, công ty phản hồi rằng đã triển khai thay đổi để giảm thiểu các hạng mục còn lại vào ngày 16/8/2024, và bài blog được đăng ngày 10/9/2024

Phạm vi lỗ hổng được xác nhận trên Feeld

  • Mục tiêu là ứng dụng hẹn hò di động Feeld, tương tự Tinder và Bumble, có bộ lọc theo khoảng cách, tuổi, giới tính, cặp đôi và vị trí
  • Người dùng premium còn có thể tìm kiếm theo loại kink, kịch bản threesome/group và kiểu quan hệ quan tâm
  • Cuộc rà soát bảo mật xác nhận 8 lỗ hổng
    • Lộ thông tin hồ sơ cho người dùng không premium
    • Đọc tin nhắn của người khác
    • Truy cập không cần xác thực vào ảnh/video đính kèm trong chat
    • Xóa/khôi phục/sửa tin nhắn của người khác
    • Cập nhật thông tin hồ sơ của người khác
    • Tạo “Like” từ hồ sơ người dùng bất kỳ
    • Gửi tin nhắn vào cuộc trò chuyện của người khác
    • Xem match của người khác
  • Ngoại trừ lỗ hổng đầu tiên, các vấn đề còn lại đều thuộc danh mục Broken Access Control trong OWASP Top 10

Thông tin hồ sơ bị lộ cho người dùng không premium

  • Khi người dùng thường xem những người đã thích mình trong menu Likes của app, họ chỉ thấy tên và ảnh bị làm mờ
  • Nhưng nếu chặn request và response bằng công cụ proxy như Burp, trong phản hồi lại có thông tin ở mức tương đương người dùng premium
    • Tuổi
    • Khoảng cách
    • Ảnh hồ sơ đầy đủ
    • streamUserId
  • Ảnh hồ sơ được lưu trên res.cloudinary.com và có thể truy cập mà không cần xác thực
  • streamUserId lấy được từ phản hồi sau đó có thể dùng cho lỗ hổng đọc tin nhắn của người khác

Vấn đề kiểm soát truy cập với tin nhắn và match

  • Để đọc tin nhắn của người khác, cần có streamUserId của nạn nhân, và giá trị này bị lộ trong nhiều request API
  • Luồng ví dụ là lấy streamUserId của người dùng mục tiêu từ phản hồi GraphQL DiscoverProfiles, rồi đưa giá trị đó vào điều kiện member của request kênh chat
  • Khi tìm "text" trong phản hồi, có thể thấy số lượng và nội dung các tin nhắn mà nạn nhân đã gửi/nhận
  • Với cùng cách tiếp cận, cũng có thể lấy messageId gắn với từng tin nhắn, và giá trị này được dùng để xóa/khôi phục/sửa tin nhắn
  • Nếu thay đổi tham số profileId yếu trong ChatListQuery, có thể xem match của người dùng khác
    • Thông tin có thể xem gồm imaginaryName, tuổi, ảnh, giới tính, sexuality, status và ngày sinh

Truy cập không xác thực vào tệp đính kèm trong chat

  • Tệp đính kèm được chia thành ảnh và video
    • Ảnh gồm ảnh thường có thể xem lại hoặc ảnh giới hạn 5–15 giây
    • Video gồm video thường có thể xem lại hoặc video chỉ phát một lần
  • Ảnh thường được tải lên api.cloudinary.com từ ứng dụng Feeld và phản hồi trả về photo_id
    • Sau đó ảnh được sao chép sang feeld.co để cung cấp cho người dùng đã xác thực
    • Đường dẫn có dạng cdn/chat-attachment/<receiver_profileId>/<photo_id> hoặc <sender_profileId>/<photo_id>
    • Phần profileId trong đường dẫn có thể bị rút còn một chuỗi tùy ý tối thiểu 1 ký tự mà vẫn trả ảnh cho người dùng đã xác thực
    • Đường dẫn có thêm /v1/ phía trước trả về URL ảnh gốc lưu trên Cloudinary, và URL đó có thể truy cập mà không cần xác thực
  • Ảnh giới hạn thời gian dùng thêm tham số như visibilityMilliseconds:15000 khi tải lên
    • Endpoint dành cho người nhận sẽ xóa ảnh sau 5–15 giây kể từ khi truy cập nên không thể truy cập tiếp
    • Endpoint dùng profileId của người tải lên vẫn tiếp tục trả ảnh cho người dùng đã xác thực ngay cả sau 5–15 giây
    • Đường dẫn /v1/ cũng trả về URL Cloudinary và URL này có thể truy cập không cần xác thực
  • Với video, URL được nhúng trong tin nhắn chat cho cả video thường lẫn video chỉ phát một lần
    • Video thường được tải lên us-east.stream-io-cdn.com
    • Video chỉ phát một lần dùng luồng upload qua chat.stream-io-api.com
    • Kẻ tấn công có thể lấy URL từ lỗ hổng đọc tin nhắn ở trên rồi thay u0026 bằng & để xem mà không cần xác thực
  • Với video chỉ phát một lần, kẻ tấn công vẫn có thể phát lại, trong khi app của người nhận sẽ hiện video expired sau khi xem một lần

Thao túng tin nhắn, thay đổi hồ sơ, giả mạo lượt thích

  • Tại endpoint chat.stream-io-api.com/messages/<messageId>, có thể dùng phương thức DELETEPUT để thao tác với tin nhắn của người khác
  • Tin nhắn đã xóa sẽ hiện là This message was deleted trong chat, nhưng nếu kẻ tấn công gọi lại cùng request DELETE thì có thể nhận lại nội dung gốc
  • Kẻ tấn công có thể sửa tin nhắn bằng messageId ngay cả khi không tham gia cuộc trò chuyện
    • Khi nạn nhân bấm thông báo, họ sẽ thấy tin nhắn đã bị sửa
    • Bên dưới tin nhắn có nhãn edited, nhưng không hiển thị ai là người sửa
    • Tên tài khoản không phải là duy nhất và cũng có thể chỉnh sửa
  • Nếu đổi tham số id yếu trong request GraphQL ProfileUpdate thành ID của nạn nhân, có thể cập nhật thông tin hồ sơ như tên, sexuality, tuổi, bio...
  • Trong request GraphQL ProfileLike, có thể đăng nhập bằng profile#1 nhưng khiến hệ thống ghi nhận như thể profile#2 đã gửi “Like” cho profile#3
    • Ví dụ được nêu là gửi Like từ một hồ sơ tùy ý tới chính hồ sơ của mình, rồi thấy lượt Like đó xuất hiện trong danh sách Likes của tài khoản premium

Gửi tin nhắn vào cuộc trò chuyện của người khác

  • Kẻ tấn công có thể gửi tin nhắn vào cuộc trò chuyện của người khác dù không phải thành viên cuộc chat
  • Giá trị cần thiết là channelID lấy được từ lỗ hổng đọc tin nhắn nêu trên
  • Chỉ cần gửi request POST tới đường dẫn channels/messaging/<channelID>/message là tin nhắn sẽ được thêm vào kênh đó
  • Nạn nhân sẽ nhận thông báo và có thể mở xem tin nhắn
  • Hệ thống hiển thị thông báo như thể tin nhắn đến từ tên của kẻ tấn công, nhưng kẻ tấn công có thể đổi tên hồ sơ và tên này không phải duy nhất

Mốc thời gian công bố

  • Ngày 8/3/2024, FORTBRIDGE công bố toàn bộ vấn đề cho Feeld
  • Cùng ngày, Feeld yêu cầu thông tin về các tài khoản được dùng để kiểm thử
  • Ngày 2/4/2024, FORTBRIDGE yêu cầu cập nhật, và Feeld đề nghị hoãn công bố vì đang điều tra
  • Ngày 28/5/2024, Feeld cho biết đã triển khai nhiều bản sửa và đề nghị trì hoãn tối đa 2 tuần để xác minh các phát hiện đã được xử lý hay chưa
  • Ngày 8/6/2024, tròn 3 tháng kể từ email công bố đầu tiên
  • Ngày 15/7/2024, Feeld phản hồi rằng một số vấn đề cần bản sửa phức tạp hơn
  • Ngày 4/8/2024, Feeld yêu cầu tiếp tục hoãn công bố cho đến khi xử lý xong các hạng mục còn lại
  • Ngày 16/8/2024, Feeld phản hồi rằng đã triển khai thay đổi để giảm thiểu các phát hiện còn lại
  • Ngày 8/9/2024, tròn 6 tháng kể từ lần công bố đầu tiên
  • Ngày 10/9/2024, bài blog được đăng
  • Nghiên cứu này được trình bày tại DEF CON 33 vào tháng 8/2025

1 bình luận

 
GN⁺ 2024-09-13
Ý kiến trên Hacker News
  • Có vẻ như kiểm tra quyền hạn chỉ được triển khai ở frontend, và không phải chỉ một hai endpoint mà dường như gần như trên toàn bộ hệ thống
    Về mặt khái niệm thì đây là lỗi khá dễ tránh, nhưng tôi đã thấy nó đủ nhiều đến mức khó mà chấp nhận đây chỉ là một sai lầm tương tự lặp lại
    Giải pháp kiểu “hãy kiểm tra mọi quyền ở backend” có cảm giác giống với câu “hãy thêm kiểm tra biên ở mọi nơi” cho lỗi tràn bộ đệm. Cả cộng đồng đều biết phải làm gì, nhưng để mọi người áp dụng nhất quán thì không hề dễ

    • Tôi không nghĩ hai chuyện đó là một. Kiểm tra tràn bộ đệm là chi tiết triển khai và ngôn ngữ rất cụ thể, và có thể xuất hiện ở bất kỳ đâu trong codebase
      Trong khi đó, kiểm tra quyền diễn ra ở những ranh giới xác định và liên quan đến cách thiết kế ứng dụng. Bất cứ khi nào có thể tác động đến cách một dự án được phát triển, tôi đều kiên quyết tách biệt rõ ràng việc phát triển backend API với mã client frontend. Theo kinh nghiệm của tôi, làm vậy sẽ dễ tránh và dễ kiểm thử các vấn đề kiểu này hơn nhiều, đồng thời cũng “miễn phí” có luôn API cho lập trình viên. Thành thật mà nói đó là lý do chính khiến tôi thích cách làm này
    • Nếu còn nhầm lẫn được chuyện này thì không nên đụng vào mã phía máy chủ
    • Trước đây tôi từng bắt được một web developer làm xác thực ở frontend bằng hộp thoại JavaScript bình thường. Mật khẩu được nhét thẳng vào JS rồi so sánh đơn giản
      Tôi phát hiện ra vì chủ tài khoản lamp liên hệ nói rằng dữ liệu của họ đột nhiên biến mất sạch. Xem log thì thấy Google Bot đã bấm hết mọi liên kết “Delete” trong màn hình quản trị nội bộ. Vì JavaScript là kiểu opt-in nên chuyện đó mới xảy ra được. Tôi gọi cho lập trình viên để giải thích họ đã làm gì, và từ ngày đó tôi mất đi rất nhiều niềm tin vào dân web
    • Tôi nghĩ chuyện này có thể xảy ra rất dễ nếu backend dùng “DB API tự động”. Ví dụ, tôi nghĩ ngay đến một số cấu hình GraphQL tự động
      Mỗi lần thấy là tôi đều đánh dấu lại, nhưng điều khá đáng lo là nhiều trường hợp hầu như không suy nghĩ gì về phạm vi của client API
    • Trong app di động thì tiếc là chuyện này khá phổ biến. Kiểu như “ai mà soi app di động kỹ chứ?”
      Tôi muốn đổ lỗi cho junior, no-code, hay AI code, nhưng bản thân tôi cũng lười chẳng kém nên cuối cùng chỉ biết lắc đầu cho qua
  • Đây là một lý do rất chính đáng để không điền thông tin cá nhân chính xác, ví dụ như ngày sinh
    Đặc biệt là app hẹn hò có vẻ hay đòi mấy thông tin này, nhưng tốt hơn là đừng làm vậy. Tốt hơn nên nhập một giá trị lệch khoảng một năm quanh ngày sinh thật
    App hẹn hò này tuy không quá nổi tiếng nhưng nhắm đến người dùng queer và những người có sở thích khác như BDSM, quan hệ nhóm. Ở nhiều nơi trên thế giới, những thông tin như vậy cực kỳ nhạy cảm, khỏi phải nói

  • Tuần này nó xuất hiện khá nhiều trên báo chí vì kiếm được nhiều tiền
    https://www.theguardian.com/technology/article/2024/sep/08/t...

    • Gần đây người ta đã chứng kiến quá nhiều chuyện cho thấy làm ra thứ tệ hại kiếm tiền hơn rất nhiều so với làm ra thứ tốt
    • Cần để The Guardian thấy chuyện này
  • Xét theo loại ứng dụng thì đây là mức thất bại gần như tắc trách đến mức hình sự

    • Tôi từng là gã contractor rẻ tiền đó. Sếp của tôi chẳng quan tâm gì ngoài deadline và những bug mà reviewer phía khách hàng có thể nhìn thấy
      Ở Mỹ và EU, nguy cơ ngồi tù, bảo hiểm dữ liệu, và chi phí bảo hiểm dữ liệu có lẽ là biện pháp răn đe duy nhất. Nếu ảnh của bạn không phải loại có thể đăng lên LinkedIn thì phải trả mức giá khiến mắt lồi ra mới đúng
      Tất nhiên, động lực không được thúc đẩy việc che giấu
    • Hóa ra không phải đùa. Những lỗ hổng kiểu này ngay cả 10 năm trước cũng đã là mức đáng xấu hổ rồi
  • Lĩnh vực hẹn hò trực tuyến thật sự là một đống hỗn độn. Chỉ có 2–3 công ty có dịch vụ có thể gọi là hữu ích, và những công ty đó thì độc ác, bất tài, hoặc cả hai
    Có lẽ bây giờ chúng ta cần thứ gì đó như dịch vụ hẹn hò liên hợp mã nguồn mở. Ít nhất là một thứ không bán dữ liệu, không làm lộ ảnh nude, và không khiến người ta bị đánh đập, cưỡng hiếp hay sát hại. Nói thì dễ hơn làm, nhưng vẫn là như vậy

    • Tôi đã ấp ủ ý tưởng đó nhiều năm rồi, nhưng không có đủ dopamine rảnh để xây nó song song với công việc chính
      ActivityPub cũng có cấu trúc có thể hỗ trợ điều này thông qua việc phát hành bản ghi Person. Đặc biệt nếu ưu tiên các nhu cầu phi đơn hôn, phi dị tính và phi chuẩn giới, thì còn rất nhiều không gian để đổi mới
      Tuy nhiên, app hẹn hò là lĩnh vực cực kỳ khó gia nhập. Để trở nên hữu ích thì cần quy mô người dùng tích lũy ở một khu vực cụ thể, và một khi kiếm tiền hóa thì ứng dụng tất yếu trở nên kém hữu ích hơn. Có lý do khiến okcupid sa sút sau khi thôi mang tính chất phi lợi nhuận
      Và còn cả vấn đề kiểm duyệt nữa
    • Tôi cảm thấy ảnh nude nên chỉ tồn tại ở dạng analog. Khi đó có thể kiểm soát việc phân phối gần như hoàn toàn và tuyệt đối
      Nếu bạn muốn biến dạng analog đó thành bản sao số thì đó là quyền cá nhân, nhưng cần hiểu rằng không có hệ thống nào đủ an toàn để cuối cùng ngăn được rò rỉ và phát tán, và sau này cũng sẽ không có
      Người trẻ đặc biệt thường không cân nhắc hậu quả và sự xấu hổ có thể xảy ra về lâu dài, và khả năng cao là sẽ xảy ra. Việc cung cấp tính năng như vậy chỉ là tự mời gọi hệ quả tiêu cực
  • Thật sự quá kinh khủng. Rõ ràng là họ không hề nghĩ gì đến bảo mật
    Tôi là game developer, và công ty này còn bỏ ít công sức để giữ an toàn cho người dùng hơn cả mức mà chúng tôi bỏ ra để giữ game công bằng. Họ đáng bị kiện cho tan nát

    • Không chỉ bảo mật, có vẻ họ chẳng suy nghĩ gì về bất cứ thứ gì
      Ngay cả trước khi nhận ra app đầy bug, tôi đã rất ngạc nhiên vì mục sở thích chẳng cung cấp chút ngữ cảnh nào. Ví dụ gần như ai cũng để Domination hoặc Submission là sở thích, nhưng hoàn toàn không có ngữ cảnh về việc họ muốn vai trò nào. Không hiểu chuyện đó sai từ gốc như thế nào trong bối cảnh đó có nghĩa là họ nhìn chung chẳng hiểu gì cả
    • Cần lưu ý rằng hồ sơ trên app hẹn hò về nguyên tắc là ai cũng truy cập được. Mở app lên là thấy hồ sơ. Không có thứ như ACL
      Tin nhắn và ảnh riêng tư lại là chuyện khác
  • Nói hơi khiêu khích một chút thì đây là vấn đề của GraphQL
    GraphQL cho phép frontend truy vấn dữ liệu. Nghe thì hay, nhưng từ góc nhìn backend thì nó rất thiếu minh bạch và thường được triển khai bằng các thư viện bên thứ ba hoàn toàn không hiểu gì về kiểm soát truy cập
    Nếu không triển khai kiểm soát truy cập ngay trong chính cơ sở dữ liệu, thì ở mã backend sẽ rất khó để phân tích truy vấn GraphQL nhằm xác định cần trả về hay giới hạn bản ghi nào. Làm ở cơ sở dữ liệu không phải là tệ nhất, và chắc chắn vẫn tốt hơn làm ở frontend
    Để triển khai kiểm soát truy cập đúng cách ở backend, bạn phải hiểu truy vấn, nắm được schema cơ sở dữ liệu, rồi xây dựng các model·class·hàm để xác định kiểu như “nếu user_id là XXX thì trong ngữ cảnh này có được xem ảnh này hay không”. Với GraphQL thì làm ở frontend dễ hơn nhiều, nên rõ ràng họ đã làm như vậy
    Không phải là nói cách triển khai GraphQL này là tốt, cũng không phải nói mọi vấn đề hoàn toàn chỉ do GraphQL. Ý là GraphQL cố gắng loại bỏ nhu cầu để backend phải hiểu truy vấn, nên làm các tình huống bảo mật phức tạp kiểu này trở nên khó hơn, và vì vậy khiến kiểu sai lầm này dễ xảy ra hơn
    [0] Ví dụ, một hình ảnh cụ thể có thể được truy cập công khai trên hồ sơ người dùng, nhưng chỉ hiển thị cho người đã match, hoặc chỉ trong ngữ cảnh trò chuyện (trừ chat nhóm), hoặc hoàn toàn không thể truy cập với người dùng đã bị chặn. Chỉ riêng một trường hợp này thôi cũng đã tạo ra hàng loạt edge case phức tạp

    • Khá dễ thôi. Hãy coi mỗi resolver lấy dữ liệu như một endpoint REST và bảo vệ nó, đồng thời dùng danh sách allowlist các truy vấn được phép thêm mục trong quá trình build CI
      Không cần đụng vào AST hay phải hiểu ngữ cảnh của phần còn lại trong truy vấn. Ở resolver lấy ảnh, chỉ cần trả lời câu hỏi “người dùng ABC có được xem ảnh của người dùng XYZ không?” là được. Nếu không hiệu quả thì có thể lấy trước một phần dữ liệu hoặc dùng dataloader
      Tuy nhiên, nếu đang dùng mấy thư viện ma thuật chuyển GraphQL thành SQL thì lại là chuyện khác
    • Với GraphQL, bạn phải định nghĩa quyền truy cập theo từng thuộc tính, hoặc biên dịch trước truy vấn rồi đưa vào allowlist. Ngoài ra thì dữ liệu sẽ bị rò rỉ hết
      https://hasura.io/docs/2.0/security/allow-list/
    • Một thư viện GraphQL bên thứ ba tử tế thì dưới hình thức nào đó cũng phải triển khai ACL. Có vẻ các thư viện phổ biến nhất cũng làm vậy [1] [2]
      Một ý tưởng đơn giản là triển khai ủy quyền trong data model. Tức là để GraphQL ủy quyền getlist cho resource model có thể thực hiện phân quyền dựa trên ngữ cảnh của request
      [1] https://www.apollographql.com/docs/apollo-server/security/au...
      [2] https://docs.graphene-python.org/projects/django/en/latest/a...
    • Khi dùng HotChocolate thì tôi không gặp vấn đề này. Có thể dễ dàng gán quy tắc phân quyền cho entity hoặc thuộc tính của entity và nó sẽ tự xử lý. Cũng áp dụng được cho mutation
  • Đây là một màn công bố có trách nhiệm và đầy cân nhắc đến mức đáng ngạc nhiên

    • Ảnh chụp màn hình menu “Discover profiles” và danh sách lượt thích có bao gồm hồ sơ thật không? Nếu có, thì dù đã che mặt đi vẫn khá là vô trách nhiệm
    • Hành động không khớp với lời nói
  • Cũng không quá ngạc nhiên. Tôi vẫn dùng nó nhưng sẽ mô tả nó là được làm kém cỏi chẳng khác gì app ngân hàng của tôi. Có khi còn tệ hơn, và hầu như chẳng hoạt động cho ra hồn
    Tôi không hiểu sao họ có thể làm ra thứ này

    • Hồi tôi dùng thì nó cũng tệ khủng khiếp. Nếu không phải là rò rỉ bộ nhớ kỳ quặc hay vấn đề riêng tư, thì UX cũng bị triển khai một cách cực kỳ tồi tệ
      Nhìn vào app này và Fetlife, có vẻ các cộng đồng đó gặp vấn đề lớn trong việc cứ bám lấy ứng dụng xuất hiện đầu tiên bất kể chất lượng ra sao
    • Lúc tôi dùng thì cộng đồng rất ổn, nhưng app thì chưa bao giờ được viết cho đàng hoàng
      Rồi cách đây không lâu họ làm một ngày chuyển đổi đồng loạt, phát hành app mới và server mới cho tất cả mọi người cùng lúc, và phần lớn thậm chí còn không đăng nhập được. Những ai đăng nhập được thì nếu là khách hàng trả phí cũng bị mất quyền lợi premium, lượt thích và đoạn chat biến mất, cùng nhiều vấn đề khác. Cuối cùng tôi cũng không đăng nhập được và bỏ app từ thời điểm đó
  • Thành thật mà nói tôi ngạc nhiên là các nhà nghiên cứu đã kiên nhẫn chờ công bố lâu như vậy
    Nếu cho một startup tồi tệ 6 tháng để vá một lỗ hổng quyền riêng tư nghiêm trọng thế này, thì họ sẽ tiếp tục lạm dụng đặc quyền được phép thu thập loại thông tin này ngay từ đầu. Tôi nghĩ chỉ nên cho 2 tháng rồi công bố. Họ phải học được rằng không thể đem thông tin riêng tư của người khác ra đánh cược