[Security 101] Cross-Site Request Forgery - CSRF Attack

Anh Hoang
SotaTek
Published in
14 min readJan 11, 2019

LỜI NÓI ĐẦU

Ban đầu, khi tham gia vào lĩnh vực lập trình ứng dụng nói chung, và lập trình Website nói riêng, tôi, và cũng như bao người, sẽ bỏ quên đi việc đảm bảo tính bảo mật và chỉ quan tâm tới việc dựng lên một Website hoàn chỉnh. Hai chữ "hoàn chỉnh" ở đây thật khó để định nghĩa khi vào một ngày đẹp trời, có thể bạn sẽ nhận được một loại báo cáo của người dùng về việc tài khoản của họ bị tấn công hoặc nếu trong lĩnh vực tài chính, ví tiền điện tử của User bị bay màu trong chớp mắt. Thế mới thấy, "hoàn chỉnh" bao gồm cả khi bạn phải quan tâm tới mọi vấn đề có thể xảy đến và đảm bảo bạn đã chuẩn bị tốt nhất cho ứng dụng của mình có thể chống lại các tác nhân gây hại đó.

Khi tham gia tìm hiểu về vấn đề bảo mật tấn công mạng, đặc biệt là quá trình làm việc tại SotaTek với những yêu cầu rất cao về mặt bảo mật ứng dụng, tôi cảm thấy thực sự bị lôi cuốn và muốn được chia sẻ những kiến thức mình góp nhặt được cho tất cả mọi người. Series "Security 101" xin được bắt đầu với hình thức tấn công phổ biến nhất, được cho vào quy chuẩn triển khai ứng dụng cần phải có: Cross-Site Request Forgery - CSRF.

CSRF LÀ GÌ?

CSRF (Cross-site Request Forgery) là một kiểu tấn công xảy ra khi một trang Website, Email, Blog, tin nhắn, hoặc các ứng dụng độc hại thực hiện một hành động gây hại trên một trang Website chính thống khi người dùng đã được xác thực. Nguyên nhân là do các Request từ phía trình duyệt sẽ gửi kèm một cách tự động các thông tin chứng thực liên quan tới trang Web, như Session Cookie của người dùng, địa chỉ IP, … Đồng thời, nếu người dùng đã được chứng thực trên Website, Website sẽ không thể nào phân biệt được giữa Request giả mạo với Request hợp pháp được gửi bởi nạn nhân.

Như vậy, kẻ tấn công sẽ sử dụng các thủ thuật tác động tới trình duyệt, tạo ra các HTTP Request mà không cần tới sự xác nhận từ phía người dùng. Đơn cử khi trình duyệt render một thuộc tính HTML như <img> hoặc <object>, nó sẽ tự động gửi một Request HTTP dưới phương thức GET tới URL được cung cấp bởi thuộc tính đó, kể cả khi URL đó là một URL ngoại vi. Thường thì các liên kết này sẽ là vô hại, nhưng chúng cũng có thể bị lợi dụng bởi những kẻ có mục đích xấu.

Hãy lưu ý rằng, bất cứ khi nào trình duyệt gửi một HTTP Request, nó sẽ tự động đính kèm các Cookie được lưu cho Domain đó. Nó có thể đi kèm tất cả các dữ liệu xác thực (Session Identification Cookie). Ví dụ, khi bạn đăng nhập vào đường dẫn www.bank.abc, với mỗi Request mà trình duyệt bạn gửi đi tới Server của bank.abc, sẽ đính kèm các Cookie xác thực người dùng (Authentication Cookie) liên quan tới Website này.

Tấn công CSRF sẽ tận dụng điều này từ phía hành vi của trình duyệt để khiến trình duyệt tạo một Request đính kèm với Cookie của bạn thay cho kẻ tấn công. Vậy hãy thử nghĩ xem, kẻ tấn công sẽ muốn tạo ra các loại Request nào nào? Ví dụ với ngân hàng, kẻ tấn công có thể muốn tạo một Request để chuyển tiền từ tài khoản của bạn vào ví của hắn. Request đó có thể có định dạng như sau:

http://www.bank.abc/transferFunds?amount=1000&to_account=12345678

Nếu như ứng dụng ngân hàng là một ứng dụng kém bảo mật, chỉ dựa vào việc xác thực Cookie, hoặc Session ID lưu trên Cookie để xác thực yêu cầu, thì quá trình tấn công sẽ thành công, và hắn sẽ làm giàu thêm 1000$ trên chính đồng tiền của bạn. Tất cả những gì hắn cần làm là khiến trình duyệt khởi tạo Request tới đường dẫn đó, và như ta đã thấy, hắn sẽ có rất nhiều cách để thực hiện. như thẻ <img src> hoặc <script src>. Chúng ta có thể mô phỏng quá trình bằng hình ảnh sau:

Mô phỏng tấn công CSRF dựa trên các thẻ đặc biệt của HTML

Và thật không may, quy ước Same-Origin (Same-Origin Policy) không giúp ích được gì để chống lại phương thức tấn công này. Vì điều quan trọng nhất để khiến quá trình tấn công này hoạt động chỉ là việc gửi đi Request chứ không đọc các phản hồi trả lại.

HTTP GET VÀ NỀN TẢNG XÂY DỰNG PHƯƠNG THỨC AN TOÀN

Vấn đề trong việc thiết kế, xây dựng các ứng dụng Website đó là cho phép thực thi các Request có dạng như: "transferFunds?amount=1000?to_account=12345678", vi phạm nguyên tắc rằng “Các Request dạng GET phải là một Request an toàn (Safe Request)”. Một cách ngắn gọn, Request an toàn là một Request không gây ra Side Effect lên phía Server. Đơn cử việc kiểm tra tài khoản Bank có thể là một Safe Request — chỉ đọc giá trị chứ không thay đổi nó. Nhưng chuyển tiền từ tài khoản ngân hàng sẽ chắc chắn không phải là một Safe Request: số dư tài khoản sẽ bị thay đổi như là kết quả của việc thực thi Request đó.

Theo mô tả kỹ thuật của W3C về HTTP chỉ ra: HTTP GET (và HEAD) chỉ nên được sử dụng cho các hành động an toàn. Nếu ta vi phạm quy luật này, không chỉ ta sẽ giúp kẻ tấn công dễ dàng thực hiện CSRF lên Website như ta đã nêu ở trên, mà còn có thể gây ra các vấn đề “không trực tiếp liên quan tới bảo mật” khác lên hệ thống. Một ví dụ điển hình đó là Google Web Accelerator.

Google Web Accelerator là một ứng dụng giúp tăng tốc độ truy cập Website. Nếu người dùng cài đặt, khi một ai đó truy cập vào Website, ứng dụng sẽ tự động lấy tất cả các URL liên kết trên trang đó một cách âm thầm. Bằng cách đó, khi người dùng đã xem xong trang đó và muốn chuyển hướng sang một trang khác, các nội dung của trang tiếp theo đã được tải trước vào trình duyệt. Vấn đề là ở chỗ: một số Website sẽ phá vỡ nguyên tắc W3C, khởi tạo các đường dẫn gây ra các hiệu ứng không mong muốn. Và Google Web Accelerator sẽ gọi các đường dẫn này một cách tự động.

Ví dụ, giả sử ngân hàng của bạn tạo ra một đường dẫn dùng tới trang Checking mà người dùng sẽ truy cập để mở một tài khoản tiết kiệm. Nếu bạn sử dụng Google Web Accelerator, khi bạn tới giao diện kiểm tra số dư, ứng dụng sẽ tự động Request đường dẫn kể trên để lưu vào Cache, vô tình đã tạo ra Side Effect khiến bạn mở thêm một tài khoản tiết kiệm kể cả khi bạn có muốn hay không.

Chúng ta nên sử dụng phương thức HTTP POST (hoặc PUT hay DELETE) để thực hiện các hành vi gây ra Side Effect. Nhưng bản thân nó cũng không thể ngăn chặn CSRF. Hãy cùng nhau tìm hiểu lý do tại sao và nhân cơ hội này để gỡ rối một số quan điểm sai lầm trong việc xây dựng hệ bảo mật CSRF.

PHÒNG THỦ CSRF KHÔNG HIỆU QUẢ

  1. Dựa vào phương thức POST

Trong việc bảo mật, CSRF có lẽ là lỗ hổng ứng dụng Web bị hiểu lầm nhiều nhất. Rất nhiều người thử ngăn chặn CSRF bằng cách tạo ra các Shortcut cuối cùng vẫn khiến ứng dụng bị tổn thương hoặc đôi khi thậm chí còn tệ hơn so với lúc đầu. Một số người sẽ chọn giải pháp lọc ra thẻ <script> trong Form Input từ phía người dùng, hay một số người khác tin rằng việc sử dụng POST và loại bỏ GET sẽ có thể chống lại phương thức tấn công này. Đó chắc chắn là một khởi đầu tốt, nhưng chỉ là sự khởi đầu. Khai thác lỗ hổng trên phương thức POST chỉ là có phần hơi khó khăn hơn so với phương thức GET.

Nếu như kẻ tấn công có thêm một số phương thức hỗ trợ từ các Website đồng phạm, hắn có thể tạo ra một Form Submit tự động gửi tới các trang Web dễ bị tấn công bởi CSRF.

Một lần nữa, kẻ tấn công sẽ lừa bạn truy cập vào các trang này, và trình duyệt sẽ tự động gửi một POST Request tới ngân hàng đi kèm với Cookie của bạn.

2. Kiểm tra Referer Header

Referer Header là một trường HTTP Header xác định địa chỉ của trang Web liên kết với tài nguyên được yêu cầu.

Bên cạnh việc nghĩ rằng sử dụng phương thức POST có thể cứu nguy, một cách thức sai lầm khác trong việc chống lại phương thức tấn công này đó là kiểm tra tính đúng đắn của nguồn Request thông qua Referer Header.

Trình duyệt sẽ thêm Referer vào các Request gửi đi để cho Server biết được người dùng này tới từ đâu. Ví dụ, khi ta vào đường dẫn www.genk.vn/tin-tuc, và bạn nhấn vào đường dẫn trên Website đó để chuyển hướng sang www.bank.abc thông qua quảng cáo trên một bài viết nào đó, trình duyệt sẽ tự động thêm vào một Referer Header như sau vào Request:

Referer: http://www.genk.vn/tin-tuc

Một số lập trình viên sẽ sử dụng tính năng này để chống lại CSRF. Họ sẽ kiểm tra xem giá trị Referer này có đến từ cùng một Domain hay không. Nếu không, chặn cứng ngay các Request này. Đây hẳn là một cách thức rất tao nhã để giải quyết các vấn đề về CSRF, ngoại trừ 2 sự thật rằng: Referer Header không phải lúc nào cũng được gửi với tất cả Request, và chúng có thể bị giả mạo.

Bất cứ khi nào người dùng nhập URL bằng tay lên thanh địa chỉ, hoặc khi họ click vào một đường dẫn trên Email, hoặc từ các ứng dụng di động, Request đó sẽ không chứa Referer Header. Thực sự không thể thay đổi được điều này, vì các hình thức kể trên không thể tìm được nội dung nào có nghĩa để gán nó vào Referer. Kể cả khi bạn có một trình duyệt đang mở trang Web www.genk.vn và ta nhập bằng tay www.bank.abc, trình duyệt của ta sẽ không thêm www.genk.vn vào trong Referer Header đâu. Vì vậy, nếu ta lên kế hoạch kiểm tra Referer Header cho mỗi Request, ta sẽ có 2 sự lựa chọn. Bạn có thể từ chối toàn bộ các Request không có Referer, điều đó có nghĩa người dùng sẽ không nhập thủ công URL dẫn tới Website của chúng ta được, và cũng chẳng thể truy cập Website từ Email hoặc ứng dụng được nữa; hoặc bạn có thể cho phép mọi Request không chứa Referer, tức bạn đã “cấp quyền” tấn công CSRF từ Email, hoặc ứng dụng.

Bạn có thể chọn giải pháp tạo ra một “Landing Page” — một trang chính chào mừng mà người dùng sẽ truy cập vào nó trước khi đi sâu hơn vào trang Web của bạn, và nó cho phép truy cập từ tất cả Request có hoặc không có Referer. Chỉ những Request tới trang không được đánh dấu là một Landing Page đặc biệt mới cần kiểm tra Referer. Nó có thể là một phương pháp không thực sự sạch sẽ, nhưng ít nhất nó dễ để triển khai. Tuy nhiên, phương pháp này không thể giúp chúng ta trong trường hợp kẻ tấn công có thể thay đổi giá trị Referer Header.

Ở một số trình duyệt cũ cho phép XMLHttpRequest để định nghĩa các giá trị Header, kể cả Referer Header. Trong trường hợp này, kẻ tấn công chỉ cần có một số hiểu biết về Referer, và cuộc tấn công bắt đầu. Các phiên bản cũ của Flash còn cho phép kẻ tấn công giả mạo Referer. Khi các vấn đề ngày đã được khắc phục trong các phiên bản mới của ứng dụng, nó vẫn là một “Bad Practice” nếu chỉ dựa vào giá trị Referer.

3. Viết lại URL (Rewrite URL)

Từ khi CSRF xảy ra là do ứng dụng lưu Token định danh vào trong Cookie, và được gửi đi mỗi khi truyền một Request, chúng ta có thể chống lại CSRF bằng cách hạn chế Cookie định danh — Cookieless Authentication. Cookieless Authentication sử dụng URL Rewrite (viết lại URL) để đặt Token định danh vào trong URL chứ không phải Cookie nữa, ví thử như:

http://www.bank.abc/home?userID=12345678

Không giống với Referer, nó thực sự là một cách tốt để chống lại CSRF. Giả định rằng Token là một chuỗi ký tự ngẫu nhiên vô cùng mạnh mẽ, rất khó cho kẻ tấn công có thể đoán ra chính xác nó (như vậy theo chiều ngược lại, nếu ta không sử dụng một kỹ thuật mã hóa mạnh, kẻ tấn công sẽ dễ dàng hơn trong việc truy ra Token chính xác. Vì vậy, hãy dùng các kỹ thuật mã hóa mạnh mẽ được cập nhật liên tục để đảm bảo tính năng bảo mật).

Không may, khi việc viết lại URL có thể giúp ta chống lại CSRF, trong trường hợp này thì vấn đề lại tệ hơn chúng ta nghĩ. Sẽ là vô cùng nguy hiểm khi truyền đi Token định danh xuyên suốt URL. Không chỉ bạn sẽ chịu chung một vấn đề về việc kẻ-đúng-giữa có thể đánh hơi thấy Token của bạn, mà bạn còn mở rộng cánh cửa để kẻ tấn công lợi dụng lỗ hổng tấn công Session.

PHÒNG THỦ CSRF HIỆU QUẢ

  1. Chia sẻ mã bí mật

Ta đã nói nhiều về cách phòng thủ không hiệu quả rồi, chuyển sang những cách làm thực tiến hơn nào. Một cách tốt nhất để phòng thủ CSRF là triển khai mã bí mật cho mỗi Session của User. Cách làm này sẽ hơi phức tạp đôi chút, nhưng lại cực kỳ hiệu quả. Và chúng ta có thể yên tâm khi có rất nhiều thư viện hỗ trợ cách làm này trên các ngôn ngữ lập trình phổ biến.

Đây là cách thức mà phương pháp này hoạt động: khi người dùng truy cập lần đầu tiên, bên phía Server sẽ tạo ra một chuỗi ký tự ngẫu nhiên rất mạnh với nhiều ký tự đặc biệt không theo quy luật (ta có thể gọi là nonce — viết tắt của Number-used-once). Máy chủ sẽ liên kết nonce đó với người dùng bằng cách thêm nó vào dữ liệu Session. Từ đây, mỗi khi Server gửi phản hồi về phía User, nó sẽ truyền đi nonce ẩn bên dưới. Ta có thể hiểu quá trình đó theo hình:

Mô phỏng cách thức chia sẻ mã bí mật

Một điều giá trị trong phương pháp này đó là dữ liệu nonce sẽ đươc thêm ẩn trong Form Input một cách tự động khi người dùng gửi Request lên Server, vì vậy ta sẽ không phải làm thêm các công việc liên quan trên code phía Client. Khi máy chủ nhận được Request, nó sẽ kiểm tra các dữ liệu ẩn này. Nếu thiếu nó, nó sẽ hiểu rằng Request này là giả mạo. Nếu nó tìm thấy Secret nhưng lại không khớp với dữ liệu được lưu cho User. nó cũng coi đó là một Request giả mạo. Nó chỉ cho phép Request thực thi nếu nó tìm thấy nonce và khớp với dữ liệu được gán cho User. Hình dưới sẽ cho bạn biết luồng tấn công CSRF bị chặn khi sử dụng phương pháp này:

Mô phỏng lộ trình từ chối truy cập CSRF nhờ phương pháp chia sẻ mã bí mật

Ví dụ thực tế, tưởng tượng rằng bạn muốn gửi một lá thư cho tôi. Tôi lo rằng có ai đó có thể gửi một lá thư nặc danh chính địa chỉ gửi của bạn, và thậm chí có thể giả mạo cả chữ ký của bạn cho tôi mà tôi chẳng thể nào nhận ra được. Vì vậy, trong lần đầu bạn viết thư cho tôi, bạn cho vào lá thư một tấm thiệp bất kỳ và trong bức thư bạn lưu ý rằng nếu tôi và bạn có gửi thư phản hồi thì hãy gửi kèm tấm thiệp đó. Và vì vậy, mỗi lần tôi và bạn mở bức thư ra và thấy có đính kèm tấm thiệp, chúng ta có thể chắc chắn người gửi bức thư chính xác là bạn mình.

Tất nhiên, kẻ xấu có thể đánh cắp bức thư của ta trong thùng thư hoặc luồn ra sau nhìn bức thư khi ta mở phong bì. Tức là, phương pháp này không hoàn toàn có thể giúp ta chống lại tấn công CSRF. Nhưng hắn sẽ phải ngồi đoán chuỗi ký tự đúng (hoặc một tấm thiệp chính xác), và nếu chúng ta dùng cách thức mã hóa ký tự mạnh mẽ thì hắn sẽ phải dành cả thanh xuân để tìm chuỗi đúng đó.

2. Truyền hai lần Cookie

Vấn đề của phương pháp chia sẻ mã bí mật là phía Server sẽ phải lưu chuỗi bí mật đó với mỗi Session của người dùng. Nó có thể không phải là vấn đề lớn khi chỉ phải lưu một ít Byte dữ liệu cho nonce, nhưng đó chỉ khi đây là dữ liệu duy nhất bạn lưu cho Session. Còn nếu bạn lưu rất nhiều dữ liệu phức tạp và liên tục vào Session hay bạn giới hạn khả năng mở rộng lưu trữ của Session thì sao? Hẳn đó chính là vấn đề của chúng ta. Trong trường hợp đó, ta có thể nghĩ đến phương pháp Double-submiited Cookie — Truyền 2 lần Cookie.

Cách thức này hoạt động tương tự như việc gửi mã bí mật, nhưng thay vì lưu trữ những mã bí mật riêng lẻ, Session sẽ tự định nghĩa chính nó và được đối xử như chính mã bí mật. Khi Server gửi phản hồi về phía User, nó sẽ định danh Session và ghi nó vào trong Cookie như bình thường, nhưng cũng sẽ bí mật lưu nó vào một trường Form ẩn. Khi người dùng gửi dữ liệu từ Form trả về phía Server, cả Cookie và dữ liệu ẩn kể trên sẽ được gửi đi theo Request. Server chỉ đơn giản kiểm tra xem hai giá trị có đồng nhất hay không mà thôi.

Đây là cách phòng ngự tốt, vì sẽ không có cách nào để kẻ tấn công có thể đoán ra định danh Session đúng của User — và kể cả có đoán ra, kẻ tấn công sẽ không thể gửi Request thay User mà hắn phải tự gửi Request. Và phương pháp này đảm bảo rằng phía Server không cần phải lưu trữ bất kỳ nội dung gì, kể cả là vài Byte cho nonce.

Có thể là một vấn đề xảy ra khi chúng ta sẽ truyền đi định danh liên tục ở bên trong Body của HTTP Request. Nhưng chúng ta có thể hạn chế rủi ro đó bằng cách mã hóa ký tự định danh đó và bên phía Server ta sẽ dịch nó ra trước khi kiểm tra tính đúng đắn.

KẾT LUẬN

Hiện nay, tấn công CSRF đã được hạn chế rất nhiều so với khi nó bùng nổ vào đầu năm 2017. Tuy nhiên, đây vẫn được coi là một quy chuẩn thiết kế Website và chúng ta phải luôn ở trong trạng thái đề phòng vì đây là một kiểu tấn công cơ bản, đơn giản nhưng hiệu quả thì cực kỳ cao, gây thiệt hại rất lớn cho người triển khai ứng dụng.

--

--