2 điểm bởi GN⁺ 4 giờ trước | 1 bình luận | Chia sẻ qua WhatsApp
  • Lỗ hổng máy chủ web cục bộ của Zoom cho thấy ranh giới bảo mật có thể dễ dàng sụp đổ khi nhiều nhà phát triển web hiểu sai cách CORS hoạt động
  • Khi giao tiếp với máy chủ cục bộ localhost:19421, Zoom truyền mã trạng thái bằng kích thước hình ảnh thay vì AJAX, và điều này có thể được hiểu là một cách triển khai vòng tránh CORS
  • Chrome áp dụng header CORS cả với máy chủ web localhost, và giao tiếp frontend·backend trên các cổng localhost khác nhau cũng được trình duyệt hỗ trợ
  • Thiết kế an toàn hơn là để máy chủ cục bộ cung cấp REST API và đặt Access-Control-Allow-Origin để chỉ cho phép JavaScript của zoom.us truy cập
  • Có thể làm cho mã chạy được bằng cách lách chính sách cùng nguồn, nhưng khi đó các chức năng đặc quyền của máy chủ cục bộ có thể bị phơi bày cho mọi website trên Internet

Vòng tránh CORS do máy chủ web cục bộ của Zoom tạo ra

  • Khi làm việc trong lĩnh vực tư vấn full-stack và tiếp xúc với các nhà phát triển ở nhiều quy mô và ngành khác nhau, tác giả liên tục thấy vấn đề các nhà phát triển web không hiểu CORS
  • Trong lỗ hổng Zoom gần đây, nhà nghiên cứu bảo mật Jonathan Leitschuh phát hiện Zoom khởi chạy một máy chủ web http://localhost:19421 trên máy người dùng
    • Khi người dùng mở liên kết Zoom, website Zoom gửi yêu cầu tới máy chủ web localhost để chạy ứng dụng Zoom gốc
    • Thay vì yêu cầu AJAX thông thường, nó tải hình ảnh từ máy chủ web Zoom cục bộ và biểu diễn lỗi·mã trạng thái của máy chủ bằng các kích thước hình ảnh khác nhau
  • Cách hiểu rằng trình duyệt bỏ qua chính sách CORS của máy chủ localhost là sai, và Chrome tôn trọng header CORS của máy chủ web localhost
    • Ngay cả khi chạy frontend Create React App và backend API trên các cổng localhost khác nhau thì yêu cầu chéo nguồn vẫn phát sinh, và điều này được mọi trình duyệt hỗ trợ
  • Có vẻ Zoom đã dùng mẹo hình ảnh để vòng tránh CORS sau khi yêu cầu AJAX bị chặn
    • Kết quả là không chỉ website Zoom mà cả các website khác trên Internet cũng có thể kích hoạt hành vi của client gốc và truy cập phản hồi

Phương án thay thế an toàn và vấn đề UX còn lại

  • Cách triển khai an toàn là để máy chủ web trên localhost:19421 thực hiện REST API và đặt giá trị header Access-Control-Allow-Origin thành https://zoom.us
    • Làm như vậy sẽ chỉ cho phép JavaScript chạy trên domain zoom.us giao tiếp với máy chủ web localhost
    • zoom.us cũng có thể đặt header Content Security Policy để chặn render iframe nhằm ngăn cuộc họp Zoom tự động mở ở chế độ nền
  • Vấn đề rằng bất kỳ trang nào cũng có thể chuyển hướng trình duyệt đến liên kết họp zoom.us vẫn còn tồn tại
    • Tuy nhiên điều này gần với trải nghiệm người dùng mà Zoom lựa chọn hơn là một lỗ hổng phần mềm
    • Zoom đang phá vỡ kỳ vọng của người dùng rằng khi nhấp vào liên kết, camera và microphone sẽ không đột ngột mở cho người lạ
    • Nếu muốn tránh popup mặc định của trình duyệt vì lý do UX, họ cũng có thể hiển thị popup trong ứng dụng, và Google Meet sử dụng cách đó khá tốt
  • Việc chạy máy chủ web trên localhost tự thân đã là một nỗ lực rủi ro, và đặc biệt không nên cung cấp cho mọi website trên Internet các chức năng đặc quyền như cài đặt phần mềm
    • CORS là cơ chế để xử lý an toàn tình huống này, nên không nên tìm cách vòng tránh nó

Không chỉ Zoom mới nhầm lẫn về CORS

  • Không chắc Zoom có thực sự chọn cách này vì không hiểu CORS hay không
    • lerunicorn trên Reddit cho rằng Firefox có thể chặn XHR từ nguồn bảo mật sang nguồn không bảo mật
    • Tuy nhiên Firefox hỗ trợ điều này khi origin là localhost
    • Ứng dụng gốc có thể tạo chứng chỉ tự ký riêng, và cũng có thể dùng tiện ích mở rộng trình duyệt
    • Dù trong trường hợp nào thì đó cũng không phải là lý do chính đáng để bỏ qua lọc theo origin
  • Sự nhầm lẫn về CORS không phải chỉ là vấn đề của Zoom
  • Nhà phát triển muốn làm cho mã chạy được, nhưng nếu vòng tránh toàn bộ chính sách cùng nguồn thì như trường hợp Zoom, quyền cục bộ sẽ bị phơi bày cho các website bên ngoài
  • Sự nhầm lẫn về CORS xuất hiện ở cả nhà phát triển giàu kinh nghiệm lẫn người mới; chưa rõ là API CORS quá phức tạp hay việc đào tạo về CORS và CSP còn thiếu, nhưng cách hiện tại rõ ràng đang không hoạt động tốt

1 bình luận

 
Ý kiến Hacker News
  • Có vẻ ngay cả TFA cũng không thực sự hiểu đúng về CORS, hoặc đã giải thích sai khá nghiêm trọng
    Access-Control-Allow-Origin: https://zoom.us không đảm bảo rằng chỉ JavaScript từ tên miền zoom.us mới có thể giao tiếp với máy chủ localhost. JavaScript từ các website khác vẫn có thể gửi yêu cầu tới localhost:19421 y hệt như vậy. CORS không phải là cơ chế để hạn chế thứ gì đó, mà là cơ chế nới lỏng các hạn chế mặc định. Header này chỉ cho phép JavaScript chạy trên zoom.us đọc được phản hồi từ localhost:19421; bản thân yêu cầu thì vẫn sẽ được gửi đi, nên backend phải được thiết kế để không phát sinh tác dụng phụ

    • Không hiểu sao đây lại là bình luận được upvote nhiều nhất. OP đúng, còn phần giải thích phía trên thì sai
      Yêu cầu GET đúng là vẫn được gửi, nhưng vốn dĩ phải có tính idempotent, nên nếu máy chủ được triển khai đúng thì không thể gây ra tác dụng phụ; với GET, điều quan trọng là có đọc được phản hồi hay không. Ngược lại, với các yêu cầu không idempotent có thể gây tác dụng phụ, trong bối cảnh cross-origin thì trình duyệt sẽ gửi preflight OPTIONS trước thay vì gửi yêu cầu thật; nếu phản hồi OPTIONS không có các header phù hợp thì yêu cầu thật sẽ không được gửi đi
    • Tôi cũng không nghĩ có thể nói CORS hoạt động theo cách đó
      Hiểu lầm về CORS lan rộng đến mức tài liệu cũng thường mâu thuẫn lẫn nhau, nên khó mà kỳ vọng một bên không quen biết sẽ triển khai đúng. Khi một giao thức tạo ra mức độ nhầm lẫn rộng khắp như vậy, thì dù một phía có hoạt động đúng cũng không thể biết phía còn lại có như vậy không. Nếu mọi người đã sửa mã cho tới khi nó chạy được với các implementation khác, thì cũng mơ hồ luôn là bên mình sai hay bên kia sai
    • Theo cách tôi hiểu, mục đích cốt lõi của preflight OPTIONS là chặn các yêu cầu HTTP vốn không được phép, còn với các yêu cầu vốn được phép thì CORS không làm gì cả
      Ví dụ, POST với Content-Typetext/json không thể gửi tới host bên thứ ba mà không có OPTIONS preflight, nhưng POST với multipart/form-data thì được phép và CORS không chặn. Và nếu endpoint không kiểm tra nghiêm ngặt Content-Type mà cứ giả định là JSON, thì coi như bất kỳ website nào cũng có thể gửi POST mà không cần người dùng tương tác
    • Giả định kiểu “chỉ nói về các method an toàn” là một giả định khá lớn
      Một web developer tử tế thì không nên để GET/HEAD/OPTIONS làm thay đổi trạng thái, và việc tham gia cuộc họp chẳng hạn là thay đổi trạng thái. PUT/DELETE cũng phải idempotent. API POST dùng định dạng không phải JSON hoặc form phải kiểm tra header Content-Type; còn PUT/PATCH/DELETE và POST có Content-Type không phải dạng form sẽ kích hoạt preflight, nên CORS được kiểm tra trước khi yêu cầu thật đến máy chủ
    • Đoạn trong bài viết nói rằng “ứng dụng native có thể tạo chứng chỉ tự ký riêng” cũng có vấn đề
      Chỉ tạo chứng chỉ thôi thì không đủ; nó còn phải được cài làm chứng chỉ CA gốc trong kho tin cậy của mọi trình duyệt trên máy. Nếu khóa riêng của CA gốc không được bảo vệ đúng cách thì bất kỳ website nào cũng có thể thực hiện tấn công xen giữa, nên ít nhất phải có giới hạn tên (https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.10). Nhưng Chrome trước v112 vào năm 2023 lại không hỗ trợ điều này trên CA gốc (https://alexsci.com/blog/name-non-constraint/), nên phải thêm một CA trung gian và áp giới hạn ở đó. Dĩ nhiên, khóa CA gốc thì nên hủy bỏ
      Trước đây tôi từng thêm basic constraints cho một dự án dùng local root CA, nhưng lại đặt nhầm vào root CA và cũng không kiểm thử trên mọi trình duyệt
  • Ước gì nhiều người đọc tài liệu CORS của MDN hơn. Nó đã giúp tôi rất nhiều khi cố hiểu CORS, và nhìn phần bình luận ở đây tôi mới biết mọi người lại thấy nó khó đến mức này
    https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS

    • Chỉ riêng tài liệu đó thôi đã trả lời được hầu hết các trường hợp, không chỉ ví dụ origin đơn giản mà còn cả cách preflight hoạt động
  • Thứ khó hiểu không chỉ là CORS; nhiều developer cũng không thực sự hiểu mô hình đe dọa
    Nghe giải thích xong mà nhiều khi vẫn không hình dung được tại sao đó lại là vấn đề lớn. Đặc biệt là backend developer thường là người cấu hình CORS, nhưng vì CORS không phải cơ chế bảo vệ quyền truy cập nên từ góc nhìn backend nó có vẻ không quá quan trọng. Với attacker thì có cảm giác không lấy được gì, còn từ phía frontend thì nó dễ trông như một chướng ngại phiền phức. Bài này cho thấy các ví dụ cụ thể khá tốt

    • Ngay cả ở những dự án mà cùng một developer viết cả frontend lẫn backend, tôi vẫn từng thấy cấu hình CORS bị làm sai
      Với vai trò vận hành, tôi đã sửa lại cho đúng ở tầng load balancer, và ít nhất giờ ứng dụng cũng hoạt động. CORS khó hiểu thật, nhưng điều đáng buồn hơn là nhiều developer không chỉ không hiểu mô hình đe dọa mà CORS muốn ngăn chặn, mà còn không hiểu cả phát triển web nói chung, đặc biệt là giao thức HTTP
    • Mô hình đe dọa của CORS thật ra không quá khó. Kẻ tấn công dụ người dùng vào website của hắn, rồi khiến họ thực hiện một hành động nào đó trên website của bạn
    • CORS gây khó hiểu vì nó được chồng lên trên một mô hình quyền mặc định khá kỳ quặc. Kiểu như multipart/form-data thì được, nhưng JavaScript của ứng dụng lại không được
    • Nhìn từ góc độ attacker và defender thì mô hình đe dọa này không hẳn tự nhiên
      CORS là tùy chọn, và các thư viện hay công cụ khác hoàn toàn có thể cứ thế bỏ qua nó. Trên thực tế, CORS chỉ có ý nghĩa trong việc ngăn XSS và CSRF đối với người dùng thật đang đăng nhập; còn các kịch bản tấn công khác thì vô nghĩa vì đằng nào cũng dùng script hay chương trình giả mạo HTTP header. Bởi vậy người ta rốt cuộc thường bật hết mọi tùy chọn CORS, mà đó lại là trường hợp tệ nhất vì cho phép XSS và CSRF
    • CORS rất hữu ích trong việc ngăn người khác dễ dàng ăn cắp băng thông và tài nguyên hosting. Muốn ăn cắp thì họ phải tự dựng proxy, và như vậy sẽ dễ chặn hơn nhiều
  • Phần bình luận này thật sự có vẻ ở mức thông tin rất thấp, và ngược lại còn chứng minh đúng luận điểm của tác giả

    • Cũng có thể là khác biệt thế hệ
      Nếu bạn từng làm web trước khi CORS ra đời, bạn sẽ hiểu rằng ban đầu yêu cầu cross-domain vốn bị cấm, và CORS xuất hiện để nới lỏng cơ chế bảo mật đó. Vì vậy rất dễ chấp nhận rằng nếu muốn làm điều mình cần thì chỉ cần bật CORS là được.
      Ngược lại, những người học phát triển web sau thời CORS chỉ thấy luồng là thử gửi yêu cầu cross-origin, trình duyệt xác định là không được phép, thử CORS preflight, rồi nếu thất bại thì hiện lỗi CORS trong console. Nếu không biết cơ chế bên trong và không đọc tài liệu mà chỉ đoán, họ sẽ nghĩ CORS là nguyên nhân chặn yêu cầu và cố “tắt CORS”. Nhưng CORS không phải nguyên nhân của vấn đề mà là giải pháp.
      Những người có cùng hiểu lầm này lại tự tin lặp lại điều đó trong tutorial và thảo luận trực tuyến, khiến mọi thứ càng rối hơn
    • CORS không trực quan, nhưng nếu đọc tài liệu thì vẫn có thể hiểu được
      https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS
  • Khi đọc bình luận, tôi nhận ra không chỉ mình tôi như vậy. Lý do không ai thật sự hiểu CORS là vì nó quá phức tạp và có quá nhiều điểm xung đột
    Chuẩn và header cũng liên tục thay đổi, nên phần lớn lập trình viên chỉ chỉnh qua chỉnh lại đủ thứ cho tới khi nó chạy được rồi đem sản phẩm đi phát hành là xong. Ngay cả khi chạy được, lỗi và cảnh báo vẫn có thể còn trong console của lập trình viên, nhưng nếu bên ngoài trông vẫn ổn thì người ta thường để nguyên như thế

  • Muốn hiểu CORS thì trước hết phải hiểu same-origin policy
    Đặc biệt nếu câu hỏi “Tại sao lại cần cái này?” khiến bạn thấy khó, thì nên bắt đầu từ đây: https://developer.mozilla.org/en-US/docs/Web/Security/Defenses/Same-origin_policy
    Trước đây tôi từng dùng same-origin policy làm câu hỏi phỏng vấn, nhưng vì nhiều ứng viên không quen thuộc với nó nên câu hỏi đó không mang lại nhiều thông tin

    • Tôi nghĩ đây là câu hỏi khá tốt khi tuyển lập trình viên frontend
      Nếu đã phát triển ứng dụng web thì sớm muộn gì bạn cũng phải từng gặp same-origin policy. Nếu không biết, tôi thường sẽ hỏi thêm như họ đã giao tiếp với backend bằng cách nào. Với một số vai trò, việc họ từng gặp vấn đề CORS nhưng chỉ áp dụng cách lách nhanh nhất rồi quên đi, hay thật sự cố hiểu nó, cũng là một tín hiệu hữu ích.
      Với vai trò backend thì câu hỏi này kém phù hợp hơn. Không phải mọi lập trình viên backend đều từng làm việc sát với đội frontend, nơi thường xuyên gặp vấn đề CORS
  • Điều tôi nhớ về CORS là việc debug mất lâu hơn rất nhiều so với dự kiến, thông báo lỗi của trình duyệt thì nghèo nàn một cách có chủ ý, và lỗi CORS lúc đầu rất khó phân biệt với các kiểu lỗi khác

    • Lỗi CORS không phải là “thông báo lỗi được gửi tới trình duyệt”, mà là lỗi do chính trình duyệt tạo ra khi nó kết luận rằng không thể cho phép yêu cầu đó
      Tất nhiên, nếu máy chủ không hiểu yêu cầu CORS và trả về phản hồi kỳ lạ thì cuối cùng điều đó vẫn có thể bị diễn giải thành một lỗi CORS
  • Nhìn phần bình luận thấy khá thú vị nên xin bổ sung: same-origin policy bảo vệ để trình duyệt không làm rò rỉ thông tin sang các website không có quyền truy cập, còn CORS cho phép làm suy yếu lớp bảo vệ đó
    Ví dụ, same-origin policy ngăn example.com lấy danh sách đăng ký của youtube.com. Nhưng dùng CORS thì có thể cho phép example.com truy cập youtube.com/public/*.
    Một công dụng khác là ngăn việc backend API hoạt động dưới một frontend khác và dẫn tới đánh cắp dữ liệu. Chẳng hạn người dùng đã đăng nhập vào dịch vụ thật nhưng lại đang ở g00gle.com, và mọi yêu cầu đều có thể bị tấn công trung gian

    • Chính xác thì là ngược lại. Thứ ngăn các vấn đề bảo mật kiểu này là SOP, còn CORS là cơ chế nới lỏng SOP để cho phép các tương tác phức tạp hơn giữa các ứng dụng
  • Tôi cũng là một trong những người đó. CORS là chủ đề cứ phải học lại định kỳ, và tôi luôn quên nên nó không đọng lại trong đầu
    Có lẽ vì tôi là lập trình viên backend nên hầu như không gặp vấn đề CORS. Những thứ không dùng hằng ngày thì tôi khá dễ quên

    • Trải nghiệm lập trình viên với CORS và CSP thật kinh khủng. Vì trình duyệt không nói rõ vấn đề bắt nguồn từ đâu
      Trong một thế giới bình thường, thông báo lỗi hẳn phải có gợi ý như “response header” hay “meta tag”, nhưng có vẻ như các hãng trình duyệt lớn thuê những người chuyên viết thông báo bí hiểm. Cụm “requested resource” của Chrome có đỡ hơn, nhưng vẫn như mật mã.
      Thông báo tốt hơn nên là kiểu như tài nguyên từ https://bank.com không cho phép yêu cầu cross-origin vì thiếu CORS header, hoặc origin hiện tại không nằm trong danh sách cho phép của CORS. Nó cũng nên hiển thị yêu cầu preflight trong tab network cùng với liên kết MDN. Với CSP cũng nên nói rõ rằng không thể tải tài nguyên do CSP header của trang này, rồi liên kết tới request header của trang hoặc meta tag trong inspector thì sẽ tốt hơn
    • Vấn đề lớn nhất của CORS là phần lớn lỗi trông giống như vấn đề của frontend, đặc biệt là vấn đề của trình duyệt, nhưng chỗ thực sự phải sửa lại nằm ở backend
    • Tôi cũng có cảm giác tương tự. Những lần tôi phải đụng tới CORS đều là trong tình huống kiểu “phải lấy cái gì đó từ server này nhưng không thể thay đổi CORS hay CSP của server”, mà nói theo ngôn ngữ bảo mật thì nghĩa là “có hệ thống bảo mật ở đó và phải tìm cách lách qua”.
      Cuối cùng, đa số trường hợp đều dựa vào giả định rằng server chỉ bị truy cập bởi các yêu cầu trình duyệt không bị chỉnh sửa. Lỗ hổng của Zoom xuất hiện vì việc vượt qua CORS và CSP ở phía client quá dễ, và đúng là Zoom đã tệ, lười biếng và ngớ ngẩn, nhưng tôi cũng cảm thấy cộng đồng tiếp tục duy trì mô hình này phải chịu một phần trách nhiệm
  • Tôi hiểu cách same-origin policy ngăn trình duyệt chạy script độc hại để làm rò rỉ thông tin. Tôi cũng hiểu rằng với header Access-Control-Allow-Origin, server tuyên bố tin cậy thêm các origin khác để nới lỏng SOP.
    Tuy vậy, tôi vẫn chưa hiểu mục đích của header Access-Control-Allow-Headers. Nó dường như không cải thiện bảo mật trình duyệt, lại càng không phải bảo mật phía server. Tôi tự hỏi liệu các nhà thiết kế giao thức có thêm nó vào chỉ “cho đầy đủ” hay không. Liên quan: https://stackoverflow.com/questions/17992042