Bài trước đo các thuộc tính bảo vệ cookie phiên khỏi bị đánh cắp. Bài này đo một cuộc tấn công lật ngược ý tưởng đó: kẻ tấn công không cần đánh cắp ID phiên của bạn — nó cho bạn mượn ID phiên của chính nó, chờ bạn đăng nhập bằng ID đó, rồi dùng lại. Đây là session fixation (cố định phiên), và nó khai thác đúng một sơ suất phổ biến trong cách ứng dụng cấp phiên.

Toàn bộ chạy trên ứng dụng tự dựng trong container, tự dọn. Không nhắm vào hệ thống của ai.

Session fixation: ba buoc tan cong, va cach chan bang mot dong

Cuộc tấn công, ba bước

  1. Kẻ tấn công lấy một ID phiên hợp lệ từ chính ứng dụng — chỉ cần mở một trang, server cấp cho nó một sid. Lúc này sid chưa gắn với ai.
  2. Kẻ tấn công ép nạn nhân dùng đúng sid đó — qua một link chứa sẵn session ID, một cookie chèn được, hay một tham số URL. Nạn nhân mở trang bằng sid của kẻ tấn công, rồi đăng nhập.
  3. Kẻ tấn công dùng lại sid cũ. Nếu server gắn trạng thái "đã đăng nhập" vào chính cái sid ấy, kẻ tấn công giờ đang cầm một phiên đã đăng nhập — của nạn nhân.

Điểm cốt lõi: kẻ tấn công không bao giờ cần biết mật khẩu, và không cần đánh cắp gì sau khi nạn nhân đăng nhập. Nó chỉ cần biết trước ID phiên mà nạn nhân sẽ dùng.

Số đo

Hai cấu hình, mỗi cái chạy đủ ba bước tấn công. v0 giữ nguyên ID phiên khi đăng nhập; v1 cấp ID mới. Ba lần chạy, kết quả giống hệt:

Cấu hình ID sau đăng nhập Kẻ tấn công dùng lại ID cũ
v0 giữ nguyên ID giữ nguyên TẤN CÔNG THÀNH CÔNG — vào được phiên nạn nhân
v1 cấp ID mới đổi khác chặn (ID cũ trả về "chưa đăng nhập")

Điều đáng nhớ

Lỗ hổng không nằm ở chỗ đoán được ID, mà ở chỗ ID không đổi khi quyền hạn đổi. Với v0, cái sid mà kẻ tấn công đưa cho nạn nhân trước khi đăng nhập vẫn là cái sid gắn trạng thái đăng nhập sau đó. Kẻ tấn công không phải phá gì — nó chỉ giữ lại một bản sao của ID mà nó tự chọn từ đầu. Đây là điều làm session fixation khác với đánh cắp phiên: kẻ tấn công có ID trước cả khi phiên có giá trị, nên mọi thứ chống đánh cắp (HttpOnly, HTTPS) đều vô dụng — không có gì bị đánh cắp cả.

Cách chặn là một dòng: cấp ID phiên mới ngay khi đăng nhập. Với v1, khoảnh khắc nạn nhân đăng nhập, server vứt sid cũ và cấp một sid hoàn toàn mới, ngẫu nhiên. Cái ID mà kẻ tấn công đang cầm lập tức trở thành rác — nó trỏ tới một phiên đã bị bỏ, chưa đăng nhập. Kẻ tấn công không có cách nào biết ID mới, vì nó chỉ được sinh ra sau khi nạn nhân nhập đúng mật khẩu, và đi tới trình duyệt nạn nhân, không tới nó.

Nguyên tắc rộng hơn: đổi ID phiên mỗi khi mức quyền thay đổi. Không chỉ lúc đăng nhập — cả lúc nâng quyền (người dùng thường thành admin), lúc đổi mật khẩu, lúc xác thực hai bước bước hai. Bất cứ khi nào một phiên có thêm quyền, ID cũ của nó phải chết. Quy tắc thực dụng: ID phiên trước và sau một ranh giới quyền hạn không bao giờ được trùng nhau.

Fixation khác đánh cắp phiên thế nào

Hai cuộc tấn công dễ lẫn, nhưng chúng ngược chiều nhau về thời gian, và điều đó quyết định cách phòng.

Đánh cắp phiên (session hijacking) xảy ra sau khi nạn nhân đăng nhập: kẻ tấn công lấy ID phiên đang có giá trị — qua XSS đọc cookie, qua nghe lén HTTP, qua log rò rỉ. Phòng nó bằng cách giữ ID bí mật: HttpOnly, Secure, không ghi ID vào log.

Cố định phiên (fixation) xảy ra trước khi nạn nhân đăng nhập: kẻ tấn công cung cấp ID, rồi chờ nó trở nên có giá trị. Giữ ID bí mật không cứu được, vì kẻ tấn công tự chọn ID — nó không cần đọc trộm gì. Phòng nó bằng cách làm cho ID hết hạn tại ranh giới quyền: đổi ID mới khi đăng nhập, để cái ID kẻ tấn công gieo chết đúng lúc nó sắp có giá trị.

Đây là lý do một cái không thay được cái kia. Bạn có thể có cookie HttpOnly; Secure hoàn hảo và vẫn dính fixation, vì fixation không đánh cắp cookie — nó lợi dụng việc cookie không đổi.

Vì sao

Session fixation tồn tại được vì hai quyết định thiết kế, mỗi cái riêng lẻ đều vô hại:

  • Ứng dụng chấp nhận một session ID do client đưa lên. Điều này bình thường — cookie phiên chính là client đưa ID lên mỗi request. Nhưng nếu ứng dụng chấp nhận một ID nó chưa từng cấp (hoặc một ID cũ), kẻ tấn công có thể gieo trước.
  • Trạng thái đăng nhập được gắn vào ID phiên hiện có thay vì một ID mới. Đây là chỗ hỏng thật. Đăng nhập là ranh giới quyền hạn quan trọng nhất; giữ nguyên ID qua ranh giới đó là để một danh tính cũ mang theo quyền mới.

Cách chặn tấn công đúng ngay tại mắt xích thứ hai: làm mới định danh tại ranh giới quyền. Nó không cần token, không cần mã hoá thêm — chỉ cần một sid mới. Đó là lý do các framework phiên trưởng thành làm việc này tự động trong hàm đăng nhập (session.regenerate(), request.session.cycle_key()), và lý do bạn nên kiểm rằng hàm đăng nhập của mình gọi nó.

Nghĩa là gì trong thực tế

  • Gọi hàm làm mới phiên trong mọi luồng đăng nhập. Nếu bạn tự viết quản lý phiên, đây là dòng quan trọng nhất; nếu dùng framework, hãy xác nhận nó được gọi (nhiều lỗ fixation đến từ việc tự viết login mà quên bước này).
  • Không bao giờ nhận session ID từ URL hay tham số. Chỉ nhận từ cookie. ID trong URL bị rò qua lịch sử duyệt, log máy chủ, header Referer — và là cách gieo fixation dễ nhất.
  • Huỷ phiên phía server khi đăng xuất, đừng chỉ xoá cookie. Nếu chỉ xoá cookie, ID cũ vẫn hợp lệ trên server và ai giữ được bản sao vẫn dùng được.
  • Kết hợp với các bài trước: HttpOnly (cookie không bị JS đọc) + SameSite (chống CSRF) + làm mới ID khi đăng nhập (chống fixation). Ba lớp chặn ba đường tấn công khác nhau vào cùng một cookie phiên.

Chỗ tôi không kết luận được

Ứng dụng của tôi cố ý chấp nhận một sid client đưa lên mà server chưa cấp — đó là cách mô phỏng lỗ fixation gọn nhất. Trong thực tế, cách gieo ID vào trình duyệt nạn nhân đa dạng hơn (cookie chèn qua subdomain bị chiếm, tham số URL, thẻ <meta>), và mỗi cách có điều kiện riêng. Tôi đo hậu quả của việc không làm mới ID, không đo mọi đường gieo.

Tôi cũng đo bằng HTTP trực tiếp với cookie quản lý thủ công, không qua trình duyệt — vì tấn công này thuần về vòng đời session ID, không cần JavaScript hay DOM. Một lần đo đầu của tôi dùng thư viện cookie tự động và thất bại vì nó khớp domain sai (app với app:5000), khiến cookie kẻ tấn công gieo và cookie phản hồi lẫn vào nhau — tấn công không tái hiện được và trông như "đã an toàn". Phải chuyển sang gửi header Cookie thủ công mới đo đúng. Đây là bài học lặp lại: một công cụ đo hỏng cho ra "an toàn" giả, y như một phép đo cho "chi phí bằng không".

Thử ba mươi giây

Tìm hàm xử lý đăng nhập trong mã của bạn. Ngay sau khi kiểm mật khẩu đúng, có dòng nào làm mới ID phiên không — regenerate, cycle_key, hay tạo session mới?

Nếu không có, ID phiên trước và sau khi đăng nhập là một. Nghĩa là bất kỳ ai gieo được cho nạn nhân một session ID trước khi họ đăng nhập đều vào được tài khoản của họ sau đó — không cần mật khẩu, không cần đánh cắp gì.