การจัดการคุกกี้คือทุ่งกับระเบิด
(grayduck.mn)- คุกกี้ HTTP เป็นกลไกพื้นฐานสำหรับคงสถานะของเว็บ แต่ เบราว์เซอร์·เซิร์ฟเวอร์·ไลบรารีมาตรฐาน กลับตีความอักขระที่อนุญาตและการจัดการข้อผิดพลาดไม่ตรงกัน จนทำให้เกิดปัญหาจริงได้
- ตระกูล RFC 6265 กำหนดเงื่อนไขของค่าที่เซิร์ฟเวอร์ส่งผ่าน
Set-Cookieกับค่าที่เบราว์เซอร์ยอมรับต่างกัน และค่าที่สร้างผ่านdocument.cookieก็อาจชนกับสมมติฐานของตัวแยกวิเคราะห์ฝั่งเซิร์ฟเวอร์ - Firefox, Chromium และ Safari มีพฤติกรรมต่างกันในการจัดการช่องว่าง เครื่องหมายอัญประกาศ จุลภาค แบ็กสแลช และ Unicode โดย Safari แสดงพฤติกรรมที่เมื่อเจออักขระต้องห้ามจะไม่ทิ้งคุกกี้ทั้งก้อน แต่ เก็บไว้เฉพาะส่วนก่อนหน้า
- Go อาจเงียบ ๆ ทิ้งคุกกี้ JSON ที่เบราว์เซอร์ยอมรับได้, Python
SimpleCookieอาจหยุดโหลดหลังเจอคุกกี้ที่มันไม่เข้าใจ, และ PHP·Ruby·Rust ก็มีขอบเขตการยอมรับต่างกันไป - คุกกี้ Unicode เพียงตัวเดียวสามารถทำให้เว็บใหญ่ ๆ อย่าง Facebook, Netflix, Okta, WhatsApp, AWS และ Apple Support เกิดข้อผิดพลาด 400/500 หรือระบบล่มบางส่วนได้ จึงควรทำให้สเปกคุกกี้และพฤติกรรมของไลบรารีสอดคล้องกันให้ชัดเจนกว่านี้
คุกกี้ที่เบราว์เซอร์รับได้ แต่ Go อ่านไม่ได้
- คุกกี้คือข้อมูลที่ตั้งผ่าน JavaScript อย่าง
document.cookieหรือผ่าน HTTP server และจะถูกแนบไปกับคำขอ HTTP ที่อยู่ในขอบเขตจนกว่าจะหมดอายุ - ตัวอย่าง JavaScript เก็บสตริง JSON ตรง ๆ เป็นค่าคุกกี้ session
- ค่าจะอยู่ในรูป
{"ginger":"snap","peanutButter":"chocolate chip","snicker":"doodle"} - เวลาใส่ JSON ลงในคุกกี้มักมีการ serialize เป็น base64 แต่เบราว์เซอร์ตั้งค่านี้ได้โดยไม่มีปัญหาและส่งกลับในส่วนหัว
Cookie
- ค่าจะอยู่ในรูป
- ปัญหาเกิดขึ้นเมื่อคุกกี้นี้ถูกส่งเข้าโค้ดที่ใช้ Go standard library
- ตัวแยกวิเคราะห์ของ Go ไม่สามารถตีความคุกกี้นี้ได้
- ความล้มเหลวลามต่อขึ้นไปในชั้นบนของสแตก
เกณฑ์สองชุดใน RFC ที่ไม่ตรงกัน
- คุกกี้ถูกนิยามผ่าน RFC 2109, RFC 2965, RFC 6265 และปัจจุบันมี draft version ที่กำลังปรับปรุงอยู่
- RFC ปฏิบัติต่อค่าคุกกี้ต่างกันในสองบริบท
- Section 4.1.1 ตัดอักขระควบคุม ช่องว่าง เครื่องหมายอัญประกาศคู่ จุลภาค อัฒภาค และแบ็กสแลช ออกจากค่าที่เซิร์ฟเวอร์ส่งผ่าน
Set-Cookie - Section 5.6 กลับเปิดกว้างกว่ามากสำหรับสิ่งที่เบราว์เซอร์ควรยอมรับเมื่อแยกวิเคราะห์สตริง
Set-Cookieโดยยกเว้นเพียงอักขระควบคุม
- Section 4.1.1 ตัดอักขระควบคุม ช่องว่าง เครื่องหมายอัญประกาศคู่ จุลภาค อัฒภาค และแบ็กสแลช ออกจากค่าที่เซิร์ฟเวอร์ส่งผ่าน
- จุดขัดแย้งหลักคือ ค่าที่เซิร์ฟเวอร์ควรส่ง กับ ค่าที่เบราว์เซอร์ควรรับ ไม่ได้จัดให้สอดคล้องกัน
- ถ้าเบราว์เซอร์รับเฉพาะคุกกี้ที่เซิร์ฟเวอร์ตั้งเอง ผลกระทบอาจน้อย แต่
document.cookieก็สร้างคุกกี้ได้เช่นกัน - มาตรฐานไม่ได้ระบุชัดว่าไลบรารีมาตรฐานที่ประมวลผลส่วนหัว
Cookieควรผ่อนปรนแบบ user agent หรือเข้มงวดแบบเซิร์ฟเวอร์
- ถ้าเบราว์เซอร์รับเฉพาะคุกกี้ที่เซิร์ฟเวอร์ตั้งเอง ผลกระทบอาจน้อย แต่
ความต่างของการยอมรับค่าคุกกี้ในแต่ละเบราว์เซอร์
-
Firefox
- การตรวจสอบค่าคุกกี้ของ Firefox ยอมให้อักขระบางตัวที่ RFC 6265 ห้ามไว้
- อักขระที่ RFC แนะนำให้ตัดออกแต่ Firefox ยอมรับ ได้แก่
0x09horizontal tab0x20ช่องว่าง0x22เครื่องหมายอัญประกาศคู่0x2Cจุลภาค0x5Cแบ็กสแลช
- พฤติกรรมนี้มีเข้ามาแต่เดิมเพื่อให้เข้ากันได้กับ Chrome และยังคงอยู่ในทั้งสอง codebase
- การตั้งค่า
network.cookie.blockUnicodeสามารถปฏิเสธค่าตั้งแต่0x80ขึ้นไปได้ และมีการติดตามงานนี้ใน bug 1797231 - ปัญหาการยอมรับ
0x7Fถูกแก้ใน Firefox 108 ตาม bug 1797235
-
Chromium
- Chromium ปฏิเสธเฉพาะ อักขระควบคุม และอัฒภาคในค่าคุกกี้
- จึงเข้มงวดกว่า Firefox เล็กน้อย เพราะไม่รับ
0x09horizontal tab - แต่ต่างจาก RFC ตรงที่ยังรับและส่งต่อช่องว่าง เครื่องหมายอัญประกาศคู่ จุลภาค แบ็กสแลช และอักขระ Unicode ได้
-
Safari / WebKit
- โค้ดเก็บคุกกี้ของ Safari อยู่ภายใน
CFNetworkซึ่งเป็นซอฟต์แวร์ปิด จึงตรวจสอบโดยตรงได้ยาก - จากการทดสอบตั้งค่าคุกกี้ผ่าน JavaScript ด้วยค่าตั้งแต่
0x00ถึง0xFFพบว่า Safari ยอมรับค่าต่อไปนี้0x09horizontal tab0x20ช่องว่าง0x22เครื่องหมายอัญประกาศคู่0x5Cแบ็กสแลช
- Safari ไม่ยอมรับ
0x7Fdelete และอักขระ0x80-FFhigh ASCII / Unicode - RFC ระบุว่าเมื่อพบอักขระควบคุมควรทิ้งคุกกี้ทั้งก้อน แต่ Safari กลับยอมรับค่าจนถึงตำแหน่งก่อนหน้าที่เจออักขระต้องห้าม
- ยังพบ Safari bug ที่เมื่อตั้งค่า
-- , --จะลบช่องว่างรอบจุลภาคออก
- โค้ดเก็บคุกกี้ของ Safari อยู่ภายใน
ความต่างในการแยกวิเคราะห์ของภาษาและไลบรารีมาตรฐาน
-
Go
- โค้ดคุกกี้ของ Go มีพฤติกรรมค่อนข้างใกล้กับถ้อยคำ RFC สำหรับค่าที่เซิร์ฟเวอร์ส่งผ่าน
Set-Cookie - ในการใช้งานจริงมันยอมรับช่องว่างและจุลภาคที่พบได้บ่อย แต่ไม่ยอมรับเครื่องหมายอัญประกาศคู่ อัฒภาค และแบ็กสแลช
- หากส่วนหัว
Cookieตัวอย่างมีคุกกี้ JSON อยู่ด้วย ผลจากrequest.Cookies()ของ Go จะเหลือเพียงcookie1=fooและcookie3=bar cookie2ที่เบราว์เซอร์ยอมรับจะหายไปแบบเงียบ ๆ โดยไม่มี exception หรือ error ชัดเจน
- โค้ดคุกกี้ของ Go มีพฤติกรรมค่อนข้างใกล้กับถ้อยคำ RFC สำหรับค่าที่เซิร์ฟเวอร์ส่งผ่าน
-
PHP
- PHP ไม่มีฟังก์ชัน native สำหรับ parse คุกกี้ ทำให้ระบุขอบเขตการยอมรับที่แน่ชัดได้ยาก แต่ผลทดสอบพบว่าพฤติกรรมต่ออักขระควบคุมไม่สม่ำเสมอ
- ค่าพวก
0x00-0x09และ0x0Dcarriage return ใช้งานได้ - แต่ถ้าใช้
0x10data link escape หรือ0x7Fdelete, PHP จะส่งข้อผิดพลาด 400 Bad Request - คุกกี้ Unicode ก็ปรากฏในผลทดสอบด้วย
-
Python
http.cookies.SimpleCookieของ Python จะหยุดโหลดคุกกี้หลังเจอคุกกี้ JSON แบบเงียบ ๆ- ในตัวอย่าง อินพุตจะเหลือผลลัพธ์เพียง
cookie1=foo - หาก subdomain สามารถตั้งคุกกี้มีปัญหาให้โดเมนหลักได้ คุกกี้ตัวเดียวนี้ก็อาจทำลายการประมวลผลคุกกี้ของทั้งเว็บไซต์
- การจัดการอักขระควบคุมก็ไม่เป็นระเบียบเช่นกัน
- อักขระควบคุมบางตัวถูกโหลดเป็นค่าว่าง
- หากเติม
aaไว้หน้าและหลังค่า คุกกี้ที่มีอักขระควบคุมจะไม่ถูกโหลด
-
Ruby
CGI::Cookie.parseของ Ruby ดูจะผ่อนปรนมากในการ parse- มันรับอักขระควบคุม, tab, เครื่องหมายอัญประกาศคู่, จุลภาค, แบ็กสแลช,
0x7Fและอักขระ Unicode ได้ และจะใช้ percent-encoding เมื่อนำค่าออกจาก cookie jar - วิธีนี้อาจใกล้เคียงสิ่งที่ดีที่สุดในโลกของคุกกี้ แต่โค้ดที่ตั้งค่าผ่าน
document.cookieอาจไม่ได้คาดหวังค่าสะท้อนกลับที่ถูก percent-encode
-
Rust
- Rust ไม่มีฟังก์ชันจัดการคุกกี้พื้นฐานในตัว จึงอ้างอิงจาก
cookiecrate ที่นิยมใช้ cookiecrate ในการตั้งค่าเริ่มต้นดูจะอยู่ฝั่งที่ผ่อนปรนมากกว่า และดูเหมือนยอมรับสตริง UTF-8 ที่ส่งเข้ามาได้
- Rust ไม่มีฟังก์ชันจัดการคุกกี้พื้นฐานในตัว จึงอ้างอิงจาก
ผลกระทบที่เห็นได้จริงบนเว็บไซต์
- ปัญหานี้ถูกพบระหว่างการตรวจสอบอัปเดตไลบรารี third-party แบบ manual บนเว็บไซต์ทดสอบ
- เป็นการเปลี่ยนแปลงที่จับด้วย automated test ได้ยาก
- หากปล่อยขึ้น production ผู้เข้าชมหลังจากนั้นอาจได้รับคุกกี้เสียและติดอยู่กับข้อผิดพลาดที่อธิบายได้ยากจนกว่าจะ rollback การอัปเดตและลบคุกกี้
- ปัญหานี้ไม่ได้จำกัดอยู่แค่เว็บเล็ก ๆ หรือ framework เฉพาะทาง
- หากตั้งค่า Unicode cookie ให้โดเมนจาก browser console แบบนี้ ก็อาจทำให้หลายเว็บไซต์ใหญ่พังได้
document.cookie="unicodeCookie=🍪; domain=.grayduck.mn; Path=/; SameSite=Lax"
- กรณีที่สังเกตพบมีดังนี้
- Facebook: แสดงหน้าข้อผิดพลาดและรูปภาพก็พังด้วย
- Instagram และ Threads: เกิดข้อผิดพลาด 500 แบบตรง ๆ
- Netflix: ส่งคืนข้อผิดพลาด
NSES-500และแม้แต่หน้าช่วยเหลือก็พัง - Okta: ทุกหน้าเข้าสู่ระบบส่งคืนข้อผิดพลาด 400
- WhatsApp: แสดง “whatsapp error”
- Amazon: ส่วนใหญ่ยังใช้งานได้ แต่บางฟีเจอร์พังแบบสุ่ม
- AWS: คอนโซลล็อกอินส่งคืนข้อผิดพลาด 400 และใช้งานต่อไม่ได้
- Apple Support: โหลดรายการอุปกรณ์ไม่ได้
- Best Buy: เมนูนำทางใช้งานได้ แต่ฟังก์ชันค้นหาใช้ไม่ได้
- eBay: ส่วนใหญ่แก้แล้ว แต่บางส่วนยังส่ง 400 อยู่
- Home Depot: มีแผนจะแก้
- Intuit: เป็นเว็บไซต์เดียวที่ระบุสาเหตุของข้อผิดพลาดได้
- Outlook: พบอีกกรณีของข้อผิดพลาด 400
ความยากในการแก้ระหว่างมาตรฐานกับความเข้ากันได้
- การแก้ปัญหาในสเปกพื้นฐานที่มีอายุกว่า 30 ปีเป็นเรื่องยากมาก และปัญหานี้อาจไม่มีทางออกที่ดีนัก
- ทั้ง Mozilla และ Google ต่างพิจารณาและทำงานกับแนวทางบล็อกคุกกี้ลักษณะนี้จากฝั่งเบราว์เซอร์
- Mozilla: bug 1797235, CVE-2023-5723, bug 1797231
- Google: bug 40061459
- การบล็อกแบบฝ่ายเดียวซับซ้อนเพราะปัญหาด้าน compatibility
- คุกกี้ที่ไม่ใช่ ASCII พบได้น้อยมาก คิดเป็นต่ำกว่า 0.01% ของคุกกี้ทั้งหมด
- แต่ telemetry ชี้ว่าพบได้บ่อยกว่ามากในประเทศอย่าง Argentina, Mexico และ Finland
- Mozilla จึงทำการตั้งค่า
network.cookie.blockUnicodeที่เปิดใช้งานได้รวดเร็วไว้แล้ว แต่ยังไม่เปิดจริงเพราะปัญหาความเข้ากันได้กับพฤติกรรมของ Chromium
- การแก้ฝั่งเซิร์ฟเวอร์ก็อาจทำได้ แต่กินวงกว้างไปถึงเว็บไซต์นับล้านและการจัดการข้อผิดพลาดภายในภาษาและเฟรมเวิร์กจำนวนมาก
- เว็บอย่าง Facebook หรือ Netflix อาจพอบรรเทาได้ แต่ผู้ดูแลเว็บไซต์ทั่วไปอาจไม่มีเวลาหรือความสามารถในการแก้
- ทางออกระดับรากคือให้ IETF HTTP Working Group จัดแนวสเปกคุกกี้ให้สอดคล้องกันภายใน และกำหนดให้เข้มงวดว่าระบบจัดการคุกกี้ควรทำงานอย่างไร
- การอนุญาตหรือไม่อนุญาตอักขระ non-ASCII ควรเหมือนกันทั้งฝั่งเซิร์ฟเวอร์และ user agent
- ขั้นตอนที่เบราว์เซอร์ ภาษา และเฟรมเวิร์กใช้จัดการคุกกี้ก็ควรถูกระบุอย่างชัดเจนเหมือนมาตรฐาน W3C สมัยใหม่อย่าง Content Security Policy
- พฤติกรรมที่คุกกี้ผิดตัวเดียวทำให้การประมวลผลคุกกี้อื่นหยุดตามไปด้วย เป็นสิ่งที่ยอมรับได้ยากเพราะนำไปสู่ความขัดข้องที่คาดไม่ถึงหลายแบบ
ขั้นตอนการจัดการคุกกี้ที่เสนอ
- เริ่มจาก
field-valueแล้วแยกด้วย;และ,เพื่อสร้างรายการraw-cookie-pairโดยไม่ถือว่าจุลภาคเป็นคำพ้องของอัฒภาค raw-cookie-pairแต่ละรายการให้ประมวลผลตามลำดับนี้- ถ้าไม่มี
=ให้ข้ามไป pair ถัดไป - ตัดช่องว่างหัวท้ายออก
- ส่วนก่อน
=ตัวแรกให้ถือเป็นcookie-name-octetsและส่วนหลังให้ถือเป็นcookie-value-octets - ถ้าค่าเริ่มด้วยเครื่องหมายอัญประกาศคู่ ให้ลบอันเปิดออกหนึ่งตัว และถ้ามีอันปิดท้ายก็ให้ลบออกหนึ่งตัว
- ถ้าชื่อหรือค่ามีรูปแบบที่เซิร์ฟเวอร์รับไม่ได้ ให้ข้าม pair นั้น
- ทูเพิล
[cookie-name-octets, cookie-value-octets]ที่เหลือให้นำไปประมวลผลตามนิยามของเซิร์ฟเวอร์
- ถ้าไม่มี
- มีข้อเสนอเพิ่มเติมว่าเซิร์ฟเวอร์ควรปฏิเสธทูเพิลที่ชื่อคุกกี้ไม่ใช่ token และปฏิเสธค่าคุกกี้ที่มี octet นอกชุด
cookie-octet
1 ความคิดเห็น
ความคิดเห็นจาก Hacker News
คุกกี้เต็มไปด้วยกับดักแปลก ๆ และพฤติกรรมที่ชวนหงุดหงิด แต่ 99.95% ก็ทำงานได้ดี กับดักคุกกี้ที่ผมชอบที่สุดคือ cookie shadowing คือถ้าตั้งค่าคุกกี้ชื่อเดียวกันแต่ต่างกันแค่แอตทริบิวต์สำคัญอย่างโดเมนหรือพาธ ก็จะมีคุกกี้ที่แทบเหมือนกันหลายตัวเกิดขึ้นพร้อมกัน และทั้งแบ็กเอนด์หรือ JS ก็ไม่มีทางแยกได้ว่าอันไหนเป็นอันไหน
ลองไปที่ https://example.com/somepath แล้วพิมพ์สิ่งต่อไปนี้ในคอนโซลของเบราว์เซอร์ได้เลย
document.cookie = "foo=a";document.cookie = "foo=b; domain=.example.com";document.cookie = "foo=c; path=/somepath";document.cookieในกรณีของผม ผลลัพธ์คือ
'foo=c; foo=a; foo=b'เป็นความผิดพลาดครั้งใหญ่จริง ๆ
/somepathการได้รับค่า C ซึ่งเป็นค่าที่เจาะจงที่สุดในสามค่าก็ดูสมเหตุสมผลอยู่มาก ทุกค่าถูกส่งกลับมาตามลำดับ จึงรู้ได้ทั้งค่าตามพาธและค่าระดับโกลบอล ฟังดูเหมือนเป็นทางประนีประนอมที่ดีที่สุดแต่ตัว setter ของ
document.cookieที่เหมือนเวทมนตร์นี่ไม่ค่อยถูกใจเท่าไร ถึงอย่างนั้นมันก็เป็นของที่อายุเกือบ 30 ปีแล้ว คงช่วยไม่ได้ประเด็นนี้กลับมาอีกครั้งเมื่อไม่นานมานี้หลังจาก jshttp/cookie เพิ่มความเข้มงวดในการตรวจสอบ: https://github.com/jshttp/cookie/pull/167
หลัง PR นั้น การตรวจสอบก็ถูกผ่อนลงเล็กน้อยอีกครั้ง คล้ายกับโค้ดของเบราว์เซอร์ที่บทความกล่าวถึง
การเปลี่ยนแปลงเดิมเริ่มจากโค้ดของเรา ตอนที่เราพบบั๊กซึ่งสร้าง เฮดเดอร์คุกกี้ ด้วยการต่อสตริงโดยไม่เข้ารหัส บางครั้งค่ามีช่องว่างจนทำให้คำขอเสีย เราจึงตั้งใจจะแนะนำให้นักพัฒนาใช้
serialize()ของ jshttp/cookie เพื่อเลี่ยงปัญหานี้ แต่ก็พบว่าการตรวจสอบของฟังก์ชันนั้นยังไม่พอที่จะจับบั๊กที่เราเจอพอเสนอแพตช์ไป คนอื่นก็พบว่าการตรวจสอบหลวมเกินไป จนสามารถแทรก JS ลงใน ฟิลด์ชื่อ ของคุกกี้ แล้วให้อีกที่หนึ่งตีความมันเหมือนเป็น ค่า ได้ กลายเป็นเส้นทาง code injection ที่ค่อนข้างแปลก
บทความพูดถึงแนวทางของ Rust แต่ต่างจากภาษาอื่น ๆ คือไลบรารีมาตรฐานของ Rust ไม่มี ฟังก์ชันจัดการคุกกี้ รวมอยู่ด้วย ที่จริงแล้วเรากำลังดูพฤติกรรมของ crate
cookieจากภายนอก และมันก็มีตัวเลือกสำหรับ percent encoding แบบเดียวกับ Ruby ด้วย: https://docs.rs/cookie/0.18.1/cookie/ในโปรโตคอล HTTP ดูเหมือนมีโปรโตคอลต่าง ๆ ฝังอยู่จริง ๆ สักหมื่นตัว เบราว์เซอร์กับเว็บเซิร์ฟเวอร์เพิ่มฟีเจอร์สารพัดเข้าไป แต่ละอย่างมีทั้งสเปกและสเปกโดยพฤตินัย แล้วทั้งหมดก็ถูกส่งผ่านใต้ร่ม HTTP อเนกประสงค์อันเดียวกัน
ไคลเอนต์ไม่สามารถระบุได้ว่าเข้ากันได้กับเวอร์ชันไหนของบรรดาสิ่งนอกสเปกนับหมื่นนี้ และเซิร์ฟเวอร์ก็เช่นกัน เหตุผลที่อัปเกรดสเปกไม่ได้คือไคลเอนต์ที่เหลือไม่เข้าใจ และมันก็ไม่มีความเข้ากันได้ย้อนหลัง
เลยเหลือแต่ความโกลาหลแบบสุ่ม ๆ ที่ไม่มีใครตกลงกันได้และแก้ไขไม่ได้ ไม่มีการเลิกใช้ตามแผนด้วย จึงต้องลากการตัดสินใจแย่ ๆ ในอดีตต่อไป
ประมาณ 10 ปีก่อน ผมเคยทำโปรเจกต์ที่ใช้เซสชันบนคุกกี้ และต้องลำบากมากกับการดีบักว่าทำไมการยืนยันตัวตนใช้ได้ใน Safari แต่ใช้ไม่ได้ใน Chrome จำไม่ได้แน่ชัดว่าเป็นฝั่งไหน แต่เบราว์เซอร์หนึ่งจะไม่ตั้งค่าคุกกี้เลยถ้ารูปแบบไม่ถูกต้อง
ไม่ได้ทำอะไรแปลกเป็นพิเศษ และเท่าที่จำได้ น่าจะเป็นความต่างระหว่าง
-กับ_Set-Cookieก็ได้เมื่อก่อนเคยใช้
camelCaseในคีย์คุกกี้ไม่ได้เพราะปัญหานี้ค้นหาแล้วก็ยังหาอิชชูที่ตรงเป๊ะไม่ค่อยเจอ
ตั้งแต่ไม่นานหลังคุกกี้ถูกนำมาใช้ ดูเหมือนว่าการใช้งานที่สมเหตุสมผลคือใส่แค่ opaque token เพื่อให้เซิร์ฟเวอร์รู้ว่าครั้งถัดไปยังเป็นไคลเอนต์เดิม แล้วเก็บอย่างอื่นทั้งหมดไว้ฝั่งเซิร์ฟเวอร์
ผมไม่เข้าใจว่าทำไมการที่ไคลเอนต์โดยหลักการแล้วสามารถประมวลผลค่าที่เซิร์ฟเวอร์ไม่มีวันส่งมาให้ จึงเป็นปัญหา ก็แค่ไม่ส่งค่านั้นไป และไม่ต้องกังวลปริศนาแบบ “ถ้าส่งมันไปแล้วจะเกิดอะไรขึ้น?”
ถึงอย่างนั้นก็ยังต้องใช้มันสำหรับการยืนยันตัวตน เพราะเป็นที่เดียวสำหรับเก็บ opaque token
การพาร์สเฮดเดอร์คุกกี้ นั้นเละเทะมาก “มาตรฐาน” ไม่ได้สะท้อนพฤติกรรมที่มีอยู่จริงในภาคสนาม รูปแบบที่แบ็กเอนด์เซิร์ฟเวอร์·ไลบรารี·เฟรมเวิร์กแต่ละตัวยอมรับก็แตกต่างกัน และเบราว์เซอร์ก็ทำอีกแบบหนึ่ง
ถ้าคุณควบคุมทั้งฟรอนต์เอนด์และแบ็กเอนด์ได้ทั้งหมดก็อาจไม่ใช่ปัญหาใหญ่ แต่ทันทีที่ต้องเชื่อมสิ่งที่ต่างกันเข้าด้วยกัน สถานการณ์จะกลายเป็นเรื่องงี่เง่าอย่างรวดเร็ว
คุกกี้ดูเหมือนความยุ่งเหยิงขนาดใหญ่และซับซ้อน และในขณะเดียวกันก็แทบเปลี่ยนแปลงไม่ได้เพราะต้องรักษาความเข้ากันได้ย้อนหลัง ในกรณีแบบนี้ ผมคิดว่าการสร้างกลไกใหม่ที่แยกออกมาต่างหากโดยสิ้นเชิงน่าจะถูกต้องกว่า
เช่น สามารถกำหนดสเปกกลไกใหม่อย่าง NewCookie แล้วออกแบบใหม่ตั้งแต่ต้นให้ทำงานสอดคล้องกันได้ ใส่มาตรการความปลอดภัยสมัยใหม่ไว้ในตัว รวมถึงสเปกที่เข้มงวดขึ้นและการรองรับ Unicode ที่ถูกต้องด้วย
Set-Cookie2ที่ถูกเลิกใช้ไปแล้ว: https://stackoverflow.com/q/9462180/3474615อย่างน้อยก็ในบาง use case และแน่นอนว่าไม่ได้ผสานกับเฮดเดอร์โดยตรง
เพราะคุกกี้มีอยู่แล้ว เราจึงถูกผูกไว้กับคุกกี้
ผมใช้เวลาหนึ่งเดือนเต็มไล่ตามปัญหาที่ iOS Safari กินคุกกี้ของโดเมนที่ลูกค้าควบคุมเองแบบสุ่ม ๆ ไม่เคยเห็นสถานะเซสชันหายไปแบบนี้ในโดเมนอย่าง Google, Twitter, Facebook
ถ้าพูดจริงจังกว่านั้น ควรเลี่ยง คำว่า cookie และตั้งชื่อเป็นอย่างอื่นไปเลย คำว่า cookie มีภาระพ่วงติดมามากเกินไป
ผู้เขียนเริ่มจากการใส่ผลลัพธ์ของ
JSON.stringifyลงในคุกกี้ แต่กลับน่าแปลกที่ไม่ใช่เพราะมีคนใส่ เซมิโคลอน ไว้ใน JSON ที่ถูกแปลงเป็นสตริงปัญหาส่วนใหญ่รอบคุกกี้ดูเหมือนจะเกิดตอนพยายามใส่อินพุตผู้ใช้แบบอิสระลงในคุกกี้ ไม่ควรทำแบบนั้น ถ้าใช้แค่ สตริง ASCII ตัวอักษรและตัวเลขความยาวคงที่ เหมือนที่ใช้กับโทเคนยืนยันตัวตน ก็จะไม่เป็นไร
เห็นด้วยว่ามันเป็นทุ่งกับระเบิดพอสมควร
วิธีเลี่ยงในฐานะนักพัฒนาคือเข้ารหัสค่าด้วย Base64 แบบปลอดภัยสำหรับ URL แล้วจะได้ค่าไบต์ดิบ และสามารถใช้ representation ภายในตามต้องการได้ อย่างไรก็ตาม ตามที่บทความบอก ก็ไม่ได้ควบคุมได้ 100% อยู่ดี เพราะมันเป็น user agent และก็ควรเป็นเช่นนั้น
อยากให้ user agent มากขึ้นเลือก ปฏิบัติตามมาตรฐาน แทน “ไบต์บนสายกับการสวดภาวนา” คำตอบ 400 ในภาพหน้าจอเป็นคำตอบที่ถูกต้องตามสเปก ถ้าเฮดเดอร์เป็น UTF-8 ตั้งแต่แรก หรือเริ่มจาก ASCII แล้วภายหลังค่อยอนุญาต UTF-8 ก็คงจะดีกว่า อย่างไรก็ตาม อย่างแรกทำได้ยากในเชิงเหตุและผล ส่วนอย่างหลังก็ยังอาจก่อปัญหาได้ เพราะทำให้ค่าที่เดิมผิดกฎหมายกลายเป็นถูกกฎหมาย
base64urlไม่เข้ากันกับการเอา URL encoding มาบวกกับbase64ในประมาณ 3% ของกรณี ซึ่งตอนพัฒนามักพลาดได้ง่าย แต่ใน production จะต้องระเบิดแน่นอน=,/,+ได้ ดังนั้นใช้ การเข้ารหัส Base64 มาตรฐานก็ได้ :)บทความล้อเลียน กฎของ Postel แต่ถ้าฝั่งที่ตั้งคุกกี้ส่งอย่างระมัดระวังตั้งแต่แรก บทความแบบนี้ก็คงไม่จำเป็น
บางครั้งกับระเบิดนั้นไม่ได้เป็นแค่บั๊กธรรมดา แต่กลายเป็น ช่องโหว่ความปลอดภัย ขนาดใหญ่ด้วย
ถ้าไคลเอนต์ส่งข้อมูลที่ไม่ตรงตามสเปก นั่นคือบั๊กและควรแก้ไข เซิร์ฟเวอร์ไม่ควรต้องเดาเจตนาแล้วรับไว้จนกลายเป็นเรื่องปกติเด็ดขาด