Các nhà phát triển không hiểu CORS (2019)
(fosterelli.co)- 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:19421trê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:19421thực hiện REST API và đặt giá trị headerAccess-Control-Allow-Originthànhhttps://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
- Trên Stack Overflow có rất nhiều câu hỏi liên quan đến
Access-Control-Allow-Origin - Trong các ví dụ Express, cũng có những trang khuyến nghị mặc định không an toàn, nếu sao chép nguyên xi có thể khiến ứng dụng dễ bị tấn công
- Các nhà cung cấp khác cũng từng gặp cùng một lỗ hổng như Zoom
- Trên Stack Overflow có rất nhiều câu hỏi liên quan đến
- 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.uskhô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ớilocalhost:19421y 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ụ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
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
Ví dụ, POST với
Content-Typelàtext/jsonkhông thể gửi tới host bên thứ ba mà không có OPTIONS preflight, nhưng POST vớimultipart/form-datathì được phép và CORS không chặn. Và nếu endpoint không kiểm tra nghiêm ngặtContent-Typemà 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ácMộ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ònPUT/PATCH/DELETEvà POST cóContent-Typekhô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ủ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
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
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
multipart/form-datathì được, nhưng JavaScript của ứng dụng lại không đượcCORS 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
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ả
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
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
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
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.comlấy danh sách đăng ký củayoutube.com. Nhưng dùng CORS thì có thể cho phépexample.comtruy cậpyoutube.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 gianTô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
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.comkhô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ơnCuố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