Xử lý cookie là một bãi mìn
(grayduck.mn)- Cookie HTTP là cơ chế cơ bản để duy trì trạng thái trên web, nhưng trình duyệt·máy chủ·thư viện chuẩn khác nhau về ký tự được phép và cách xử lý lỗi, nên có thể dẫn tới sự cố thực tế
- Họ RFC 6265 đặt ra điều kiện khác nhau giữa giá trị
Set-Cookiedo máy chủ gửi và giá trị mà trình duyệt chấp nhận, và các giá trị tạo bằngdocument.cookiecó thể xung đột với giả định của bộ phân tích phía máy chủ - Firefox, Chromium và Safari khác nhau trong cách xử lý khoảng trắng, dấu ngoặc kép, dấu phẩy, dấu gạch chéo ngược và Unicode; Safari còn có hành vi chỉ lưu phần đầu thay vì bỏ toàn bộ cookie khi gặp ký tự bị cấm
- Go có thể âm thầm bỏ qua cookie JSON mà trình duyệt chấp nhận, Python
SimpleCookiecó thể dừng tải các cookie phía sau một cookie mà nó không hiểu, còn PHP·Ruby·Rust cũng có phạm vi chấp nhận rất khác nhau - Chỉ một cookie Unicode cũng có thể gây lỗi 400/500 hoặc sự cố một phần trên các trang lớn như Facebook, Netflix, Okta, WhatsApp, AWS, Apple Support, nên đặc tả cookie và hành vi thư viện cần được căn chỉnh rõ ràng hơn
Cookie mà trình duyệt nhận nhưng Go không đọc được
- Cookie là dữ liệu do JavaScript qua
document.cookiehoặc máy chủ HTTP thiết lập, và sẽ tiếp tục được gửi kèm trong các yêu cầu HTTP phù hợp phạm vi cho đến khi hết hạn - Đoạn JavaScript ví dụ lưu nguyên chuỗi JSON làm giá trị cookie phiên
- Giá trị có dạng
{"ginger":"snap","peanutButter":"chocolate chip","snicker":"doodle"} - Khi đưa JSON vào cookie, nhiều nơi thường tuần tự hóa bằng base64, nhưng trình duyệt vẫn thiết lập giá trị này bình thường và gửi nó trong header
Cookie
- Giá trị có dạng
- Vấn đề phát sinh khi cookie này được chuyển tới mã dùng thư viện chuẩn Go
- Bộ phân tích của Go không thể diễn giải cookie đó
- Sự thất bại lan truyền theo chuỗi lên các lớp cao hơn trong stack
Hai tiêu chuẩn trong RFC không khớp nhau
- Cookie được định nghĩa qua RFC 2109, RFC 2965, RFC 6265, và hiện có bản dự thảo cập nhật
- RFC xử lý giá trị cookie khác nhau ở hai khu vực
- Section 4.1.1 loại trừ ký tự điều khiển, khoảng trắng, dấu ngoặc kép, dấu phẩy, dấu chấm phẩy, dấu gạch chéo ngược... khỏi giá trị mà máy chủ gửi bằng
Set-Cookie - Section 5.6 lại cho phép trình duyệt chấp nhận rộng hơn nhiều khi phân tích chuỗi
Set-Cookie, miễn là không có ký tự điều khiển
- Section 4.1.1 loại trừ ký tự điều khiển, khoảng trắng, dấu ngoặc kép, dấu phẩy, dấu chấm phẩy, dấu gạch chéo ngược... khỏi giá trị mà máy chủ gửi bằng
- Xung đột cốt lõi là giá trị máy chủ được phép gửi và giá trị trình duyệt phải chấp nhận không được căn chỉnh
- Nếu trình duyệt chỉ nhận cookie do chính máy chủ thiết lập thì ảnh hưởng sẽ nhỏ, nhưng
document.cookiecũng có thể tạo cookie - Tiêu chuẩn không nói rõ thư viện chuẩn xử lý header
Cookienên hào phóng như user agent hay nghiêm ngặt như máy chủ
- Nếu trình duyệt chỉ nhận cookie do chính máy chủ thiết lập thì ảnh hưởng sẽ nhỏ, nhưng
Khác biệt giữa các trình duyệt về giá trị cookie được chấp nhận
-
Firefox
- Kiểm tra giá trị cookie của Firefox cho phép một số ký tự mà RFC 6265 cấm
- Các ký tự được cho phép dù RFC khuyến nghị loại trừ gồm:
0x09horizontal tab0x20khoảng trắng0x22dấu ngoặc kép0x2Cdấu phẩy0x5Cdấu gạch chéo ngược
- Hành vi này được đưa vào trước đây để tương thích với Chrome và vẫn còn trong cả hai codebase
- Thiết lập
network.cookie.blockUnicodecó thể từ chối các giá trị từ0x80trở lên, và công việc liên quan được theo dõi tại bug 1797231 - Vấn đề cho phép
0x7Fđã được sửa trong Firefox 108 tại bug 1797235
-
Chromium
- Chromium chỉ từ chối ký tự điều khiển và dấu chấm phẩy trong giá trị cookie
- Nó nghiêm hơn Firefox một chút vì không nhận
0x09horizontal tab - Trái với RFC, nó vẫn chấp nhận và gửi lại khoảng trắng, dấu ngoặc kép, dấu phẩy, dấu gạch chéo ngược và ký tự Unicode
-
Safari / WebKit
- Mã lưu cookie của Safari nằm trong
CFNetworkmã nguồn đóng nên khó kiểm chứng trực tiếp - Khi thử thiết lập giá trị cookie từ
0x00đến0xFFbằng JavaScript, Safari cho phép các giá trị sau0x09horizontal tab0x20khoảng trắng0x22dấu ngoặc kép0x5Cdấu gạch chéo ngược
- Safari không cho phép
0x7Fdelete và các ký tự high ASCII / Unicode0x80-FF - RFC nói rằng khi gặp ký tự điều khiển thì phải bỏ qua toàn bộ cookie, nhưng Safari lại nhận phần giá trị trước điểm xuất hiện ký tự cấm
- Cũng quan sát thấy lỗi Safari xóa khoảng trắng quanh dấu phẩy khi đặt giá trị
-- , --
- Mã lưu cookie của Safari nằm trong
Khác biệt phân tích giữa ngôn ngữ và thư viện chuẩn
-
Go
- Mã cookie của Go hoạt động khá gần với câu chữ RFC áp cho giá trị mà máy chủ gửi trong
Set-Cookie - Nó cho phép khoảng trắng và dấu phẩy vốn khá phổ biến trong thực tế, nhưng không cho phép dấu ngoặc kép, dấu chấm phẩy và dấu gạch chéo ngược
- Nếu header
Cookieví dụ chứa cookie JSON, kết quảrequest.Cookies()của Go chỉ còncookie1=foovàcookie3=bar cookie2mà trình duyệt chấp nhận sẽ âm thầm biến mất mà không có ngoại lệ hay lỗi tường minh
- Mã cookie của Go hoạt động khá gần với câu chữ RFC áp cho giá trị mà máy chủ gửi trong
-
PHP
- PHP không có hàm phân tích cookie native nên khó khẳng định chính xác phạm vi cho phép, nhưng kết quả thử nghiệm cho thấy cách xử lý ký tự điều khiển không nhất quán
- Các giá trị như
0x00-0x09và0x0Dcarriage return vẫn hoạt động - Nếu dùng
0x10data link escape hoặc0x7Fdelete thì PHP trả về lỗi 400 Bad Request - Cookie Unicode cũng xuất hiện trong đầu ra thử nghiệm
-
Python
http.cookies.SimpleCookiecủa Python khi gặp cookie JSON sẽ âm thầm dừng tải các cookie phía sau- Với đầu vào ví dụ, đầu ra chỉ còn
cookie1=foo - Nếu một subdomain có thể đặt cookie lỗi cho domain gốc, chỉ một cookie như vậy cũng có thể làm hỏng toàn bộ xử lý cookie của site
- Việc xử lý ký tự điều khiển cũng không đều
- Một số ký tự điều khiển được tải với giá trị rỗng
- Nếu thêm
aatrước và sau giá trị thì cookie chứa ký tự điều khiển sẽ không được tải
-
Ruby
CGI::Cookie.parsecủa Ruby có vẻ cực kỳ hào phóng khi phân tích- Nó chấp nhận ký tự điều khiển, tab, dấu ngoặc kép, dấu phẩy, dấu gạch chéo ngược,
0x7Fvà ký tự Unicode, rồi áp dụng percent-encoding khi lấy ra từ cookie jar - Cách này có thể gần như tối ưu trong thế giới cookie, nhưng mã đặt cookie bằng
document.cookiecó thể không mong đợi giá trị phản chiếu đã được percent-encode
-
Rust
- Rust không cung cấp xử lý cookie mặc định, nên việc kiểm tra dựa trên crate
cookiephổ biến cookiecrate ở cấu hình mặc định có vẻ thuộc nhóm hào phóng nhất và dường như chấp nhận chuỗi UTF-8 được truyền vào
- Rust không cung cấp xử lý cookie mặc định, nên việc kiểm tra dựa trên crate
Ảnh hưởng bộc lộ trên các website thực tế
- Vấn đề này được phát hiện khi kiểm chứng thủ công việc cập nhật thư viện bên thứ ba trên một site thử nghiệm
- Đây là kiểu thay đổi khó bị bắt bởi kiểm thử tự động
- Nếu được triển khai nguyên trạng, khách truy cập sau đó có thể nhận cookie hỏng và bị mắc kẹt với lỗi không rõ nguyên nhân cho tới khi rollback bản cập nhật và xóa cookie
- Vấn đề này không chỉ giới hạn ở site nhỏ hay framework cụ thể nào
- Nếu đặt cookie Unicode cho domain từ console trình duyệt như sau, nhiều site lớn có thể bị hỏng
document.cookie="unicodeCookie=🍪; domain=.grayduck.mn; Path=/; SameSite=Lax"
- Các trường hợp được quan sát gồm
- Facebook: hiện trang lỗi và hình ảnh cũng bị vỡ
- Instagram và Threads: phát sinh lỗi 500 đơn giản
- Netflix: trả về lỗi
NSES-500và cả trang trợ giúp cũng bị hỏng - Okta: mọi trang đăng nhập đều trả về lỗi 400
- WhatsApp: hiển thị “whatsapp error”
- Amazon: phần lớn vẫn hoạt động nhưng một số tính năng hỏng ngẫu nhiên
- AWS: console đăng nhập trả về lỗi 400 và ngừng hoạt động
- Apple Support: không thể tải danh sách thiết bị
- Best Buy: điều hướng hoạt động nhưng tìm kiếm không chạy
- eBay: phần lớn đã được sửa nhưng một số chỗ vẫn trả lỗi 400
- Home Depot: dự kiến sẽ sửa
- Intuit: site duy nhất xác định được nguyên nhân lỗi
- Outlook: xuất hiện thêm một trường hợp lỗi 400
Khó khăn khi sửa giữa tiêu chuẩn và tính tương thích
- Việc sửa một vấn đề trong đặc tả nền tảng đã 30 năm tuổi là cực kỳ khó, và có thể không tồn tại lời giải tốt cho vấn đề này
- Cả Mozilla và Google đều đã xem xét và làm việc theo hướng chặn các cookie kiểu này ở phía trình duyệt
- Mozilla: bug 1797235, CVE-2023-5723, bug 1797231
- Google: bug 40061459
- Việc chặn một chiều rất phức tạp vì vấn đề tương thích
- Cookie không phải ASCII chiếm dưới 0.01% tổng số cookie nên không phổ biến
- Telemetry cho thấy chúng xuất hiện thường xuyên hơn nhiều ở các nước như Argentina, Mexico và Finland
- Mozilla đã triển khai thiết lập
network.cookie.blockUnicodecó thể bật nhanh, nhưng chưa kích hoạt vì vấn đề tương thích hành vi với Chromium
- Sửa ở phía máy chủ cũng có thể khả thi, nhưng phạm vi trải dài qua hàng triệu website cùng lỗi xử lý nội bộ của nhiều ngôn ngữ và framework
- Những nơi như Facebook hay Netflix có thể giảm thiểu được, nhưng người vận hành một site trung bình khó có đủ thời gian hoặc năng lực để xử lý
- Giải pháp gốc rễ là IETF HTTP Working Group phải căn chỉnh lại đặc tả cookie từ bên trong và quy định chặt chẽ cách hệ thống xử lý cookie phải hoạt động
- Việc cho phép hay không cho phép ký tự không phải ASCII phải giống nhau giữa phía máy chủ và user agent
- Các bước mà trình duyệt, ngôn ngữ và framework xử lý cookie cũng phải được mô tả tường minh như các tiêu chuẩn W3C hiện đại như Content Security Policy
- Việc chỉ một cookie lỗi có thể khiến xử lý các cookie khác cũng dừng theo là hành vi khó chấp nhận vì dễ dẫn tới nhiều sự cố bất ngờ
Quy trình xử lý cookie được đề xuất
- Bắt đầu từ
field-value, tách theo;và,để tạo danh sáchraw-cookie-pair, nhưng không coi dấu phẩy là từ đồng nghĩa của dấu chấm phẩy - Mỗi
raw-cookie-pairđược xử lý theo thứ tự sau- Nếu không có
=thì chuyển sang pair tiếp theo - Xóa khoảng trắng ở đầu và cuối
- Phần trước dấu
=đầu tiên được xem làcookie-name-octets, phần sau làcookie-value-octets - Nếu giá trị bắt đầu bằng dấu ngoặc kép thì bỏ một dấu ngoặc kép mở đầu, và nếu có dấu ngoặc kép kết thúc thì bỏ một dấu đó
- Nếu tên hoặc giá trị ở dạng máy chủ không thể chấp nhận thì bỏ qua pair đó
- Bộ đôi
[cookie-name-octets, cookie-value-octets]còn lại được xử lý theo cách do máy chủ định nghĩa
- Nếu không có
- Đề xuất thêm là máy chủ từ chối các bộ đôi có tên cookie không phải token, và từ chối giá trị cookie nếu chứa octet không nằm trong
cookie-octet
1 bình luận
Ý kiến trên Hacker News
Cookie đầy những cái bẫy kỳ quặc và hành vi bất tiện, nhưng 99,95% thời gian thì vẫn chạy ổn. Bãi mìn cookie mà tôi thích nhất là cookie shadowing, khi bạn đặt cookie cùng tên nhưng chỉ khác các thuộc tính chính như domain, path, thì nhiều cookie gần như giống nhau sẽ tồn tại đồng thời, và backend hay JS không có cách nào phân biệt cái nào là cái nào
Hãy vào https://example.com/somepath rồi nhập các dòng dưới đây trong console của trình duyệt
document.cookie = "foo=a";document.cookie = "foo=b; domain=.example.com";document.cookie = "foo=c; path=/somepath";document.cookieTrong trường hợp của tôi, kết quả là
'foo=c; foo=a; foo=b'Đúng là một sai lầm cực lớn
/somepath, việc nhận C, giá trị cụ thể nhất trong ba giá trị, trông khá hợp lý. Vì mọi giá trị đều được trả về theo thứ tự, bạn có thể biết cả giá trị theo từng path lẫn giá trị toàn cục, nên cảm giác đây là thỏa hiệp tốt nhấtDù vậy, tôi không thích setter
document.cookieđầy tính ma thuật đó, nhưng nó đã gần 30 năm tuổi rồi nên cũng đành chịuVấn đề này gần đây lại nổi lên khi jshttp/cookie siết chặt kiểm tra hợp lệ: https://github.com/jshttp/cookie/pull/167
Sau PR đó, phần kiểm tra hợp lệ lại được nới lỏng đôi chút, tương tự đoạn mã trình duyệt được nhắc trong bài
Thay đổi ban đầu bắt nguồn từ việc chúng tôi tìm thấy một lỗi trong mã của mình: tạo header cookie bằng cách nối chuỗi mà không mã hóa. Thỉnh thoảng giá trị có khoảng trắng làm hỏng request, và để tránh chuyện này, chúng tôi định khuyên các developer dùng
serialize()của jshttp/cookie, nhưng rồi nhận ra phần kiểm tra hợp lệ của hàm đó không đủ để bắt lỗi mà chúng tôi đã thấyKhi đề xuất bản sửa, một người khác phát hiện phần kiểm tra quá lỏng, đến mức có thể nhét JS vào trường tên của cookie, rồi khiến nơi khác diễn giải nó như giá trị. Nó trở thành một đường chèn mã khá đặc biệt
Bài viết có nhắc đến cách tiếp cận của Rust, nhưng khác với các ngôn ngữ khác, thư viện chuẩn của Rust không có chức năng xử lý cookie. Thực ra là đang nhìn vào hành vi của crate
cookiebên thứ ba, và nó cũng có tùy chọn percent-encoding giống Ruby: https://docs.rs/cookie/0.18.1/cookie/Trong giao thức HTTP dường như thực chất có khoảng mười nghìn giao thức khác nhau nhét bên trong. Trình duyệt và web server đã gắn thêm đủ loại tính năng, mỗi thứ lại có đặc tả và cả đặc tả trên thực tế, và tất cả gần như đều được truyền dưới chiếc ô chung gọi là HTTP
Client không thể chỉ định mình tương thích với phiên bản nào trong số mười nghìn thứ phi-đặc-tả đó, server cũng vậy. Lý do không thể nâng cấp đặc tả là các client còn lại sẽ không hiểu, và cũng không có tương thích ngược
Vì thế thứ còn lại là một mớ hỗn độn ngẫu nhiên mà không ai đồng thuận được và cũng không sửa được. Không có kế hoạch loại bỏ dần, nên ta phải tiếp tục mang theo những quyết định tệ hại trong quá khứ
Trong một dự án khoảng 10 năm trước, tôi triển khai session dựa trên cookie, và đã thật sự khổ sở khi debug vì sao xác thực chạy trên Safari nhưng không chạy trên Chrome. Tôi không nhớ chính xác là bên nào, nhưng một trình duyệt sẽ không đặt cookie nếu định dạng không đúng
Tôi cũng không làm gì đặc biệt kỳ lạ; theo trí nhớ thì hình như là khác biệt giữa
-và_Set-CookieTrước đây vì vấn đề này mà tôi từng không thể dùng
camelCasecho key cookieTìm kiếm cũng không thấy chính xác issue đó
Có vẻ từ ngay sau khi cookie được đưa vào, cách dùng hợp lý đã được coi là chỉ đặt token mờ để server nhận ra lần sau đây là cùng client, còn mọi thứ khác thì lưu hết phía server
Tôi không hiểu vì sao việc client về nguyên tắc có thể xử lý những giá trị mà server tuyệt đối không bao giờ gửi lại là một vấn đề. Chỉ cần đừng gửi những giá trị như vậy, và không cần lo các câu đố kiểu “nếu gửi cái đó thì chuyện gì sẽ xảy ra?”
Dù vậy, vì đây là nơi duy nhất để lưu token mờ nên vẫn phải dùng cho xác thực
Phân tích cú pháp header cookie là một mớ hỗn độn. “Tiêu chuẩn” không phản ánh được những hành vi tồn tại ngoài thực tế; mỗi backend server, thư viện, framework lại chấp nhận các định dạng khác nhau, còn trình duyệt thì làm một kiểu khác nữa.
Nếu bạn kiểm soát hoàn toàn cả frontend lẫn backend thì không phải vấn đề lớn, nhưng ngay khi phải tích hợp các thứ khác nhau, tình hình sẽ rất nhanh trở nên ngớ ngẩn.
Cookie trông như một mớ hỗn độn lớn và phức tạp, đồng thời gần như không thể thay đổi vì tương thích ngược. Trong trường hợp này, có lẽ nên tạo một cơ chế mới hoàn toàn tách biệt.
Ví dụ có thể đặc tả một cơ chế mới kiểu NewCookie và thiết kế lại từ đầu để nó hoạt động nhất quán. Có thể tích hợp sẵn các biện pháp bảo mật hiện đại, đặc tả chặt chẽ hơn và hỗ trợ Unicode đúng cách.
Set-Cookie2nhưng đã bị loại bỏ: https://stackoverflow.com/q/9462180/3474615Ít nhất là trong một số use case, và tất nhiên nó không tích hợp trực tiếp với header.
Vì cookie đã tồn tại nên chúng ta bị ràng buộc với cookie.
Tôi đã mất trọn một tháng để truy tìm vấn đề iOS Safari tùy tiện nuốt cookie trên các domain do khách hàng kiểm soát. Tôi chưa từng thấy trạng thái phiên biến mất kiểu này trên các domain như Google, Twitter hay Facebook.
Nói nghiêm túc hơn một chút, tốt nhất nên tránh từ cookie và đặt hẳn một cái tên khác. Từ cookie mang theo quá nhiều gánh nặng.
Bài viết bắt đầu từ việc tác giả đưa kết quả
JSON.stringifyvào cookie, nhưng điều đáng ngạc nhiên là không phải vì ai đó đã đưa dấu chấm phẩy vào JSON được stringify.Hầu hết rắc rối quanh cookie dường như phát sinh khi cố đưa input tùy ý của người dùng vào cookie. Không nên làm vậy. Nếu chỉ dùng chuỗi ASCII chữ-số có độ dài cố định như khi dùng cho token xác thực thì ổn.
Đồng ý rằng đây đúng là một bãi mìn.
Với tư cách lập trình viên, cách đi đường vòng là mã hóa giá trị bằng Base64 an toàn cho URL. Khi đó bạn nhận được giá trị byte thô và có thể dùng biểu diễn nội bộ tùy ý. Tuy nhiên, như bài viết cũng nói, bạn không thể kiểm soát 100%. Vì đó là user agent, và cũng nên như vậy.
Mong rằng nhiều user agent hơn sẽ chọn tuân thủ tiêu chuẩn thay vì “byte trên dây và cầu nguyện”. Các phản hồi 400 trong ảnh chụp màn hình là phản hồi đúng theo đặc tả. Có lẽ sẽ tốt hơn nếu header ngay từ đầu đã là UTF-8, hoặc ban đầu là ASCII rồi sau đó cho phép UTF-8. Tuy nhiên trường hợp đầu khó về mặt nhân quả, còn trường hợp sau vẫn có thể gây vấn đề vì biến những giá trị vốn bất hợp pháp thành hợp pháp.
base64urlkhông tương thích vớibase64cộng thêm URL encoding trong khoảng 3% trường hợp; lúc phát triển rất dễ bỏ sót, nhưng lên production chắc chắn sẽ nổ.=,/,+, nên cũng có thể dùng mã hóa Base64 tiêu chuẩn :)Bài viết chế giễu định luật Postel, nhưng nếu bên đặt cookie đã bảo thủ khi gửi thì ngay từ đầu đã không cần bài viết này.
Đôi khi quả mìn đó không chỉ là bug đơn giản mà còn trở thành lỗ hổng bảo mật lớn.
Nếu client gửi dữ liệu không đúng đặc tả thì đó là bug và cần được sửa. Việc server đoán ý định rồi chấp nhận tuyệt đối không nên trở thành điều hiển nhiên.