Cookie phiên là chìa khoá vào tài khoản của một người dùng — nếu kẻ tấn công lấy được nó, họ vào được mà không cần mật khẩu. Bốn bài trước đã chạm tới cookie từ nhiều hướng (XSS ăn cắp cookie, CSRF lợi dụng cookie tự gửi). Bài này đo trực tiếp các thuộc tính của cookie — HttpOnly, SameSite, Path — xem mỗi cái thật sự chặn được gì, bằng một trình duyệt Chromium thật thay vì đọc tài liệu.

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

Cac thuoc tinh cookie: HttpOnly, SameSite, Path - do bang trinh duyet that

Cách đo

Máy chủ đặt bốn cookie cùng lúc, mỗi cái mang thuộc tính khác nhau:

  • thuong — không có thuộc tính đặc biệt.
  • httponly — có HttpOnly.
  • cathuoc — có HttpOnlySameSite=Strict.
  • path_han — giới hạn Path=/khu-vuc-khac.

Rồi tôi đo ba câu hỏi bằng trình duyệt thật: JavaScript (document.cookie) đọc được cookie nào; máy chủ đọc được cookie nào; và cookie giới hạn Path xuất hiện ở đâu.

Bảng số liệu

Ba lần chạy, kết quả giống hệt:

Ai đọc / ở đâu Cookie thấy được
JavaScript (document.cookie) chỉ thuong
Máy chủ, path gốc / thuong, httponly, cathuoc
Máy chủ, path /khu-vuc-khac/ cả bốn, gồm path_han

Vì sao phải đo bằng trình duyệt thật

Điểm này quyết định độ tin của cả bài. Một phép thử phía máy chủ không thể phân biệt HttpOnly với cookie thường: máy chủ nhận cả hai y hệt trong header Cookie, bất kể thuộc tính. Sự khác biệt của HttpOnly chỉ tồn tại bên trong trình duyệt — nó quyết định document.cookie có trả về cookie đó hay không. Muốn đo nó, bắt buộc phải có một trình duyệt thật thực thi JavaScript và một document.cookie thật để đọc.

Đây là cùng nguyên tắc đã dùng suốt sê-ri: đo thứ trình duyệt làm, không đo thứ tài liệu nói. Tài liệu bảo "HttpOnly chặn JavaScript"; phép đo cho thấy chính xác document.cookie trả về ['thuong'] và không hơn — một sự thật kiểm chứng được, lặp lại ba lần.

Điều đáng nhớ

HttpOnly giấu cookie khỏi JavaScript, nhưng máy chủ luôn đọc được. Đây là dòng đầu và dòng hai đọc cùng nhau. JavaScript chỉ thấy thuong — hai cookie HttpOnly vô hình với document.cookie. Nhưng máy chủ vẫn nhận đủ chúng trong header Cookie của mọi yêu cầu. Nghĩa là HttpOnly không phải "cookie bí mật" — nó là lá chắn riêng chống một thứ: XSS ăn cắp cookie. Nếu kẻ tấn công chèn được JavaScript vào trang bạn (bài XSS), đoạn document.cookie của nó không lấy được cookie HttpOnly. Cookie phiên vì thế bắt buộc phải có HttpOnly — để một lỗ XSS không tự động thành chiếm phiên.

Cookie không có thuộc tính là mồi cho XSS. thuong hiện ra trong document.cookie, nên chỉ cần một lỗ XSS là kẻ tấn công đọc và gửi nó đi. Bài học ngược lại: đừng để dữ liệu nhạy cảm trong cookie mà JavaScript đọc được. Cookie cần JavaScript truy cập (như lựa chọn giao diện sáng/tối) thì không đặt HttpOnly — nhưng chúng cũng không được chứa gì bí mật.

Path giới hạn nơi cookie được gửi, nhưng không phải ranh giới bảo mật. path_han chỉ xuất hiện khi máy chủ nhận yêu cầu tới /khu-vuc-khac/, vắng mặt ở path gốc. Nghe như một cơ chế cô lập — cookie của khu quản trị chỉ gửi tới khu quản trị. Nhưng đây là hiểu lầm nguy hiểm: Path là công cụ tổ chức, không phải bảo mật. Mọi path trên cùng một tên miền chia sẻ một không gian bảo mật; một trang ở / bị XSS có thể mở iframe hoặc gửi yêu cầu tới /khu-vuc-khac/, và cookie path_han sẽ tự đính kèm. Path ngăn cookie đi nhầm chỗ, không ngăn kẻ tấn công tới đúng chỗ.

Điều này còn có mặt trái về vận hành, không chỉ bảo mật: path_han vắng mặt ở path gốc là một nguồn lỗi khó lần. Một cookie đặt với Path=/gio-hang sẽ "biến mất" khi người dùng ở trang chủ, và lập trình viên đi tìm một lỗi phiên không tồn tại. Vì Path không đem lại bảo mật gì, cái giá gỡ lỗi này gần như luôn không đáng — đó là lý do khuyến nghị mặc định là Path=/.

Vì sao

Mỗi thuộc tính kiểm soát một trục khác nhau của "cookie này đi đâu, ai chạm được":

  • HttpOnly kiểm soát ai trong trình duyệt đọc được: chặn JavaScript, cho phép chính trình duyệt gửi kèm request. Trục "script hay không script".
  • SameSite kiểm soát ngữ cảnh nào cookie được gửi: same-site hay cross-site (bài CSRF). Trục "trang nào khởi phát yêu cầu".
  • Secure kiểm soát giao thức nào: chỉ gửi qua HTTPS, không bao giờ qua HTTP. Trục "kênh có mã hoá không". (Tôi đo trên HTTP nội bộ nên không kiểm được trục này — xem cuối bài.)
  • PathDomain kiểm soát phạm vi URL: cookie gửi tới path/tên miền nào. Trục "địa chỉ" — và đây là trục không bảo mật.

Cách đọc đúng: các thuộc tính này không phải "bật để an toàn hơn" theo kiểu cộng dồn. Mỗi cái đóng một cửa cụ thể. HttpOnly đóng cửa XSS-ăn-cookie; SameSite đóng cửa CSRF; Secure đóng cửa nghe lén trên đường truyền. Biết cửa nào mới đặt đúng thuộc tính.

Bảng đo minh hoạ trục thứ nhất và thứ tư trong cùng một lần chạy: cathuoc mang cả HttpOnly lẫn SameSite=Strict, và nó vắng khỏi document.cookie (do HttpOnly) trong khi vẫn tới máy chủ ở path gốc (vì phép đo là same-site, nên SameSite không chặn). Hai thuộc tính trên cùng một cookie, mỗi cái làm việc của mình mà không cản việc của cái kia — đúng nghĩa "mỗi cái đóng một cửa".

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

Cấu hình đúng cho một cookie phiên gần như luôn là cả bốn:

Set-Cookie: sid=...; HttpOnly; Secure; SameSite=Lax; Path=/
  • HttpOnly để XSS không đọc được phiên.
  • Secure để phiên không rò qua HTTP (kể cả một request nhầm sang http://).
  • SameSite=Lax để chặn CSRF (bài trước) — hoặc Strict nếu không cần link từ ngoài vào.
  • Path=/Path không thêm bảo mật; đặt hẹp hơn chỉ gây lỗi khó hiểu khi cookie "biến mất" ở path khác.

Và một quy tắc bao trùm: đừng dựa vào Path/Domain để cô lập quyền. Muốn tách khu quản trị khỏi khu người dùng, hãy dùng phiên riêng, kiểm quyền phía máy chủ — không phải một thuộc tính cookie.

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

Tôi đo trên HTTP nội bộ Docker, nên không kiểm được Secure — thuộc tính đó chỉ có tác dụng khi phân biệt HTTP với HTTPS, và cả hai đầu của tôi đều HTTP. Cơ chế thì rõ: Secure khiến trình duyệt không đính cookie vào bất kỳ request http:// nào. Trên sản xuất, cookie phiên thiếu Secure là một lỗ rò thật khi có dù chỉ một liên kết http:// sót lại.

Tôi cũng không đo tiền tố __Host-__Secure- — hai quy ước tên bắt trình duyệt ép một số thuộc tính (ví dụ __Host- đòi Secure, Path=/, và không Domain). Chúng là cách chống lại việc một subdomain bị chiếm ghi đè cookie của tên miền chính, và xứng một bài riêng.

Thử ba mươi giây

Mở công cụ nhà phát triển, tab Application → Cookies, chọn cookie phiên của một site bạn dùng. Nhìn ba cột: HttpOnly, Secure, SameSite.

Nếu cột HttpOnly không có dấu tích, cookie phiên ấy đọc được bằng JavaScript — nghĩa là một lỗ XSS bất kỳ trên site đó là một vụ chiếm phiên, không cần thêm điều kiện gì. Đó là ô quan trọng nhất trong ba ô, và là ô dễ kiểm nhất.