RFC 9839 และยูนิโค้ดที่มีปัญหา
(tbray.org)- RFC 9839 นิยาม อักขระยูนิโค้ดที่เป็นปัญหา ซึ่งอาจอยู่ในฟิลด์ข้อความระหว่างการพัฒนาซอฟต์แวร์ไว้อย่างชัดเจน
- RFC นี้กล่าวถึงปัญหาจากการที่ ภาษาและไลบรารีที่แตกต่างกัน จัดการกับอักขระเหล่านี้ได้ไม่สอดคล้องกัน
- ใน 9839 มีการเสนอ ชุดย่อยสามแบบ ที่มีปัญหาน้อยกว่า เพื่อให้เลือกใช้งานได้ตามต้องการ
- เมื่อเทียบกับ PRECIS framework เดิมแล้ว นำไปใช้ได้ง่ายและเรียบง่ายกว่า
- มีการเผยแพร่ ไลบรารีภาษา Go สำหรับ RFC 9839 ควบคู่กันไป เพื่อช่วยให้ใช้งานจริงได้สะดวกขึ้น
ภูมิหลังและภาพรวมของ RFC 9839
- ยูนิโค้ดถูกใช้เป็นมาตรฐานในการจัดการข้อมูลข้อความแทบทุกประเภท
- แต่ในการออกแบบโครงสร้างข้อมูลหรือโปรโตคอลจริง หากอนุญาตอักขระยูนิโค้ดทั้งหมด อาจทำให้เกิดปัญหาได้
- Paul Hoffman และผู้เขียนได้ส่ง individual draft ไปยัง IETF เพื่อเสนอเกณฑ์ที่ชัดเจนต่อปัญหายูนิโค้ดที่เกิดซ้ำอยู่เรื่อย ๆ
- หลังจากการหารือเป็นเวลา 2 ปี จึงได้รับการรับรองเป็นมาตรฐานอย่างเป็นทางการและประกาศเป็น RFC 9839
- เอกสารนี้อธิบายอย่างละเอียดถึง ประเภทของอักขระที่เป็นปัญหา เหตุใดจึงเป็นปัญหา (ทั้งในเชิงเทคนิคและเชิงมาตรฐาน) และชุดย่อยสามแบบที่ผู้ใช้สามารถเลือกใช้ได้
เนื้อหาหลักของ RFC 9839
- เป็นเอกสารที่ควรอ้างอิงโดยจำเป็นเมื่อต้องออกแบบ ฟิลด์ข้อความ ในซอฟต์แวร์และสภาพแวดล้อมเครือข่าย
- RFC 9839 มีความยาว 10 หน้า ซึ่งถือว่าค่อนข้างกระชับเมื่อเทียบกับเอกสารมาตรฐานของ IETF
- อธิบายไว้ให้เข้าใจง่าย โดยมุ่งไปที่นักพัฒนาซอฟต์แวร์และวิศวกรเครือข่ายเป็นหลัก
ตัวอย่างอักขระยูนิโค้ดที่เป็นปัญหา
- ตัวอย่างเช่น ในฟิลด์
usernameของ JSON อาจมีสตริงดังนี้{ "username": "\u0000\u0089\uDEAD\uD9BF\uDFFF" } - ปัญหาของแต่ละ code point
U+0000: อักขระ NULL ที่ไม่มีความหมายและอาจรบกวนการทำงานของภาษาโปรแกรมบางภาษาU+0089: โค้ดควบคุม C1 (CHARACTER TABULATION WITH JUSTIFICATION) ซึ่งมีพฤติกรรมซับซ้อนและไม่สม่ำเสมอU+DEAD: surrogate ที่ไม่ได้จับคู่ เป็นปัญหาที่เกิดจากข้อจำกัดของ UTF-16 ทำให้เกิดข้อมูลที่ไม่พึงประสงค์\uD9BF\uDFFF(U+7FFFFจริง) : noncharacter ซึ่งตามมาตรฐานห้ามใช้ในการแลกเปลี่ยนข้อมูล
- code point ลักษณะนี้ทำให้เกิด การจัดการที่ไม่สอดคล้องกัน ภายในโครงสร้างข้อมูลและโปรโตคอล และอาจก่อให้เกิดข้อผิดพลาดที่คาดไม่ถึง
- RFC 9839 จึงนิยามอักขระปัญหาเหล่านี้อย่างเป็นทางการ และ ระบุประเภทที่ควรถูกตัดออกอย่างชัดเจน
การออกแบบของ JSON และข้อจำกัด
- ไม่ใช่ความรับผิดของ Doug Crockford ผู้สร้าง JSON
- JSON ถูกออกแบบขึ้นในช่วงที่ยูนิโค้ดยังไม่เติบโตเต็มที่ จึงไม่สามารถจำกัดชุดอักขระได้อย่างเข้มงวด
- ตอนนี้ไม่สามารถเปลี่ยนมาตรฐานได้แล้ว จึงจำเป็นต้องใช้วิธี ตัดอักขระที่เป็นปัญหาออกตามประสบการณ์ที่พบจริง
ความแตกต่างจาก PRECIS framework ของ IETF
- ก่อน RFC 9839 ในปี 2025 ทาง IETF ก็มีมาตรฐานหลากหลายฉบับ เช่น RFC 8264 (PRECIS Framework)
- framework นี้อธิบายวิธีการปรับแต่ง การประยุกต์ใช้ และการเปรียบเทียบสตริงสากลไว้อย่างละเอียด
- มีความยาว 43 หน้า และ ครอบคลุมทั้งคำอธิบายพื้นหลังและแนวทางแก้ปัญหา
- PRECIS พึ่งพาเวอร์ชันของยูนิโค้ดอย่างมาก และมีข้อเสียคือซับซ้อนและนำไปใช้ได้ยาก
- RFC 9839 กระชับและเน้นการใช้งานจริงมากกว่า ทำให้ นำไปใช้ได้รวดเร็ว เมื่อต้องนิยามโปรโตคอลใหม่
ชุดย่อยของ RFC 9839 และตัวอย่างการใช้งาน
- 9839 นำเสนอชุดย่อยที่ใช้งานได้จริง 3 แบบ ได้แก่ scalars, XML และ assignables
- แต่ละชุดย่อยมีขอบเขตของอักขระที่เป็นปัญหาซึ่งจะถูกตัดออกแตกต่างกันเล็กน้อย
- ต่อไปนี้คือสรุปตารางว่าฟอร์แมตข้อมูลหลัก ๆ และชุดย่อยของ RFC 9839 จัดการกับอักขระที่เป็นปัญหาอย่างไร
- ฟอร์แมตบางชนิด เช่น CBOR, TOML, XML, YAML จะตัด surrogate หรืออักขระควบคุมออกบางส่วน
- I-JSON ตัด surrogate และ noncharacter ออก
- JSON ทั่วไป, Protobufs ไม่ได้ตัดออก
- XML, YAML เนื่องจากคุณสมบัติของ charset จึงตัด noncharacter/โค้ดควบคุมออกได้เพียงบางส่วน
- หมายเหตุ: XML และ YAML ไม่ได้ตัด noncharacter ที่อยู่นอก Basic Multilingual Pane ออก
ไลบรารี RFC 9839 สำหรับภาษา Go
- มีการเผยแพร่ไลบรารี Go ขนาดเล็กที่รองรับ การตรวจสอบอักขระ สำหรับชุดย่อยทั้งสามของ RFC 9839
- มีการทดสอบอย่างเพียงพอแล้ว แต่การปรับแต่งประสิทธิภาพยังอยู่ระหว่างดำเนินการ
- ยินดีรับการทดสอบและข้อเสนอแนะจากการใช้งานจริง
ความสำคัญของ RFC 9839 และกระบวนการทำงาน
- RFC 9839 ผ่านการรับฟังความคิดเห็นจากผู้เขียนร่วมหลายครั้ง และมีการแก้ไข draft มากกว่า 15 รอบก่อนประกาศอย่างเป็นทางการ
- ด้วยการอภิปรายและการมีส่วนร่วมจากผู้เชี่ยวชาญในชุมชนจำนวนมาก จึงพัฒนาเป็น เอกสารที่สมบูรณ์กว่าร่างแรกอย่างมาก
- มีการระบุผู้มีส่วนร่วมไว้ในส่วน “Acknowledgements”
ประสบการณ์จากการส่ง RFC แบบ individual submission
- RFC 9839 ดำเนินการในรูปแบบ individual submission
- เมื่อเทียบกับวิธีดั้งเดิมผ่าน Working Group แล้ว มี ภาระทั้งด้านความพยายามและขั้นตอน มากกว่า
- เมื่อเทียบกับประสบการณ์ในการเข้าร่วม Working Group วิธีดั้งเดิมมีประสิทธิภาพมากกว่าและน่าแนะนำกว่า
1 ความคิดเห็น
ความคิดเห็นจาก Hacker News
ผมคิดว่ามันชัดเจนว่ามีอักขระบางตัวที่ก่อปัญหา แต่ก็รู้สึกว่าสถานการณ์ที่แย่ที่สุดคือการที่ผู้ออกแบบโครงสร้างข้อมูลหรือโปรโตคอลมีแนวโน้มจะไม่ยอมให้อักขระทุกชนิดผ่านได้ตามอำเภอใจ แม้กระทั่งตัวที่ escape อย่างถูกต้องแล้วก็ตาม ตัวอย่างเช่น ผมมองว่าการตรวจสอบความถูกต้องของชื่อผู้ใช้ควรทำในอีกเลเยอร์หนึ่ง เช่น ตรวจว่าชื่อผู้ใช้ยาวน้อยกว่า 60 ตัวอักษร, ห้ามอีโมจิหรืออักษร zalgo, ห้าม null byte ฯลฯ แล้วให้ API ส่ง error ที่เหมาะสมกลับมา ผมไม่อยากให้มันล้มเหลวเพราะเรื่องพวกนี้ตั้งแต่ขั้นตอน parse JSON แทนที่จะตรวจสอบล่วงหน้า แน่นอนว่าสำหรับชื่อผู้ใช้ มีคลาสของอักขระบางอย่างที่ไม่เหมาะสมอย่างชัดเจน แต่ถ้าคุณกำลังส่งไฟล์ข้อความที่มีการใช้ tab character หรืออย่างอื่นจริง ๆ ผมก็คาดหวังว่าสิ่งที่จัดการได้ในชนิด
utf8"string" ของภาษาผมก็ควรถูกเข้ารหัสได้ โดยเฉพาะ null byte มีกรณีใช้งานเยอะ และใน JSON ก็เห็นอยู่บ่อย ๆ แต่ถ้าจำเป็นต้องใช้เฉพาะชุด Unicode "ปกติ" ที่จำกัดไว้ ผมก็คิดว่าการมีมาตรฐานกลางย่อมดีกว่าปล่อยให้แต่ละคนสร้างมาตรฐานย่อยของตัวเอง สรุปคือไอเดียโดยรวมดูดี แต่เหตุผลที่ยกมาในโพสต์บล็อกยังไม่น่าเชื่อถือเท่าไรพูดจริงจังนะ ณ ปี 2025 ผมคิดว่าวิธีแทนสตริงที่พอจะปกป้องได้จริงใน wire protocol ระดับล่างมีอยู่แค่ตัวเลือกต่อไปนี้
เอาจริง ๆ ผมอยากให้ในไฟล์ plain text ไม่มีการใช้ตัวอักษร C0 (ยกเว้นตัวขึ้นบรรทัดใหม่ และ HT แบบจำใจ) และตัวอักษร C1 ผมเข้าใจนะว่ามีคนอยากเก็บอะไรอย่าง ANSI color markup แต่ในกรณีนั้นมันไม่ใช่ plain text จริง ๆ มันคือฟอร์แมต markup แบบหนึ่งสำหรับข้อความ คล้าย Markdown แต่ต่างกันตรงที่ใช้อักขระเข้ารหัสในช่วง C0 แค่เพราะข้อมูลนั้นดูสวยเวลาใช้คำสั่ง
catไม่ได้แปลว่ามันเป็น plain text ผมก็ยอมรับว่ามีฟอร์แมต markup ที่เข้ารหัสเป็น plain text อยู่เยอะเพราะเหตุผลด้าน interoperabilityผมว่าความเห็นที่บอกว่าการเริ่มแบนชุดอักขระแบบตามอำเภอใจในโครงสร้างข้อมูลและโปรโตคอลเป็นสิ่งที่แย่ที่สุดนั้นค่อนข้างห่างไกลจากความเป็นจริง สิ่งที่แย่จริง ๆ คือการที่ข้อบกพร่องของซอฟต์แวร์อย่าง parser ทำให้เกิดการเจาะระบบด้านความปลอดภัย
ผมสงสัยว่ามีระบบไหนบ้างที่อนุญาต UTF-8 ในชื่อผู้ใช้ ตัวระบุทุกชนิดที่ถูกจัดการหรือประเมินผลทางโปรแกรมได้ (เช่น ชื่อผู้ใช้สำหรับล็อกอิน, รหัสผ่าน ฯลฯ) ควรต้องเป็น ASCII เท่านั้น ไม่ใช่ ISO-8859-1 แต่เป็น ASCII ล้วน Unicode ไม่เหมาะกับงานแบบนี้ ถึงจะไม่เป็นไรถ้าใช้ตอนแสดงชื่อผู้ใช้ แต่ถ้าเป็นตัวระบุสำหรับการล็อกอินเข้าระบบ การเข้ารหัสที่ไม่ใช่ ASCII ควรถูกห้ามทั้งหมด แม้แต่ซอฟต์แวร์คีย์บอร์ดเองก็ไม่อาจรับประกันความสม่ำเสมอของ UTF-8 ในการแสดงผลเมื่อออกนอก ASCII ได้ และยิ่งสับสนตามระบบปฏิบัติการกับการตั้งค่าที่ต่างกัน อีกทั้งก็ไม่มีอะไรรับประกันได้ว่าไบนารีที่ถูกทิ้งไว้ในอนาคตกับ AI ที่ตีความ Unicode จะเข้าใจตรงกัน นอกจากนี้ในประเด็นความสม่ำเสมอ RFC 9839 และบทความก็ยังไม่ชัดเจนว่าจัดการเรื่อง IVS หรือ normalization (NFC/NFD/NFKC/NFKD) ว่าอยู่ในขอบเขตหรืออยู่นอกขอบเขตอย่างไร ดูเหมือนว่าส่วนวัตถุประสงค์จะหายไปเลย มีเพียงการพูดแบบคลุมเครือประมาณว่ามี "non-character code point"
ผมสงสัยว่าทำไมต้องห้ามอีโมจิในชื่อผู้ใช้
อยากจะบอกว่า IETF ไม่ได้รอจนถึงปี 2025 ค่อยรองรับ Bad Unicode ก่อนหน้านี้ก็มี RFC 8264: PRECIS Framework ที่จัดการปัญหา Bad Unicode หลากหลายแบบไว้อย่างครอบคลุมแล้ว ดู RFC ที่เกี่ยวข้องอย่าง RFC 8265(ลิงก์), 8266(ลิงก์) เป็นต้น ก็น่าจะช่วยได้ โดยทั่วไป สิ่งอย่างรหัสผ่านที่อาจเปลี่ยนทิศทางข้อความหรือเข้ารหัสต่างกันไปตามอุปกรณ์ป้อนข้อมูลไม่ควรถูกใช้ในชื่อผู้ใช้/รหัสผ่าน โปรไฟล์ RFC เหล่านี้ช่วยจัดการได้อย่างปลอดภัย สำหรับจุดประสงค์แบบนี้ การ "failing closed" (บล็อกให้เข้มงวดกว่า) ปลอดภัยกว่า แม้จะมีอีโมจิใหม่ออกมา ผมก็ยังชอบแนวทางห้ามไว้ก่อนและค่อยเป็นค่อยไป มากกว่าจะปล่อยให้ใช้ในชื่อผู้ใช้แล้วไปกระทบทุกหน้า
Unicode มีส่วนที่ "ดี" อยู่แน่นอน แต่ก็น่าผิดหวังที่เราต้องรู้ว่ามีอักขระบางตัวที่ต้องยกเว้นเป็นพิเศษ มันเป็นผลจากการพยายามรองรับวิธีบันทึกภาษาต่าง ๆ แบบครอบคลุมจนซับซ้อนเกินไป มันเหนื่อยตรงที่ต้องคอยคิดเสมอว่าอักขระไหนต้องถูกปฏิบัติเป็นกรณีพิเศษ ดังนั้นผมเลยมองสตริง Unicode ว่าเป็นหน่วยข้อมูลแบบเฉพาะตัว รับเข้า, เก็บ, render, เทียบความเท่ากันของข้อมูลได้ แต่ไม่พยายามตีความเนื้อหาของมัน แม้แต่การต่อสตริงหรือจัดการกับมันก็ยังรู้สึกไม่สบายใจ
Unicode เหมือนห้วงลึกที่เต็มไปด้วยเรื่องจุกจิกไม่รู้จบและการตัดสินใจที่ไม่ดี ตัวอย่างเช่น RFC ที่เกี่ยวข้องมีคำเตือนเกี่ยวกับอักขระควบคุม ASCII แบบเก่า (ที่เสี่ยงทำให้สับสนในการแสดงผล) แต่กลับไม่พูดถึงอักขระควบคุมทิศทางที่มีปัญหาด้านความปลอดภัยร้ายแรงอย่าง Explicit Directional Overrides เลย
ยกตัวอย่างง่าย ๆ ถ้าสตริงแรกลงท้ายด้วยตัวแก้ไขอีโมจิที่ลอยอยู่เดี่ยว ๆ และสตริงที่สองเริ่มด้วยอีโมจิที่แก้ไขได้ ปัญหาก็เกิดแล้ว และยิ่งมีเคสซับซ้อนขึ้น ปัญหาก็ยิ่งมากขึ้น
มันซับซ้อนมากก็จริง แต่บางอย่างในนั้น เช่น surrogate และ control code ไม่ได้มีไว้เพื่อการบันทึกภาษา แต่เป็นผลจากดีไซน์ประหลาดที่หลงเหลือมาจากอดีต
Unicode น่าหงุดหงิดก็จริง แต่ผมก็ยังคิดว่ามันน่าหงุดหงิดน้อยกว่ามาตรฐาน encoding แบบเก่าอื่น ๆ
ผมคิดว่าปัญหาส่วนใหญ่แก้ได้ด้วยการปฏิเสธลำดับไบต์ UTF-8 ที่ไม่ถูกต้อง หรือไม่ก็คืนค่า error ออกไปทั้งชุด เช่น surrogate นั้นผิดกฎใน UTF-8 อยู่แล้ว ดังนั้นถ้าเป็นภาษาที่ใช้ utf-8 ก็ควรคืน error เมื่อเจอลำดับแบบนี้ ปัญหาที่เป็นปัญหาจริง ๆ คือพวก "code point" ที่มีคุณสมบัติน่ามีปัญหา (เช่น non-printing เป็นต้น) ซึ่งควรถูกจัดการเป็นแนวคิดแยกจากลำดับไบต์ที่ผิดกฎหมายอย่างชัดเจน จะได้มีประโยชน์กว่า
Unicode ได้กำหนดหมวดหมู่ของแต่ละ code point (General Category) ไว้อยู่แล้วเพื่อจัดประเภทอักขระแปลก ๆ ดูได้จากบทความใน Wikipedia ที่เกี่ยวข้อง ตัวอย่างเช่น ใน Python,
unicodedata.category(chr(0))จะคืนค่า "Cc" (control), และunicodedata.category(chr(0xdead))จะคืนค่า "Cs" (surrogate)ผมคิดว่าการตัด "legacy control" ออกทั้งหมด ไม่เฉพาะตัวลิเทอรัลแต่รวมถึงสตริงแบบ escape ด้วย (เช่น "\u0027") มันมากเกินไป C1 ไม่ค่อยได้ใช้ก็จริงจึงพอรับได้ แต่ในบรรดาอักขระ C0 บางตัวก็มีตัวอย่างการใช้งานจริง escape, EOF, NUL ฯลฯ ยังมีประโยชน์ใช้งานชัดเจนอยู่
อักขระ C0 ที่ค่อนข้างเฉพาะทางหน่อย (เช่น U+001E Record Separator) ผมคิดว่ามีประโยชน์มากใน data stream ต่อให้ห้ามในเอกสาร แต่ในข้อมูลแบบสตรีมมันยังมีประโยชน์
ผมเคยเห็นมีการใช้อักขระ form feed (U+000C) ในซอร์สโค้ดโปรแกรม Emacs รองรับการนำทางเป็นหน้าอยู่แล้วแต่เดิม เลยทำให้ของแบบนี้มีหลุดเข้ามาได้
ผมไม่คิดว่า Unicode จะดี ไม่ว่าจะใช้ชุดอักขระอะไร ประเภทของอักขระที่ใช้งานจริง (อักขระควบคุม, อักขระกราฟิก, ความยาวสูงสุด ฯลฯ) สุดท้ายก็ต้องกำหนดตามแต่ละแอปพลิเคชันอยู่ดี การพยายามรวม/ตัดออกใน JSON หรืออะไรทำนองนั้นไม่ค่อยช่วยมากนัก จะเป็น Unicode, ASCII หรือชุดอักขระอื่น การตั้งชื่อให้ subset (หรือ superset) เฉพาะบางแบบบางครั้งก็มีประโยชน์ แต่ไม่ควรหลงคิดว่ามันเป็นตัวเลือกที่ดีสำหรับทุกคน RFC 9839 แค่ตั้งชื่อให้ subset ของ Unicode บางชุด แต่ไม่ได้รับประกันว่านั่นจะถูกต้องเสมอสำหรับบริการที่ผมจะสร้าง ข้อสรุปของผมคือควรพิจารณาด้วยว่าจะไม่ใช้ Unicode เลยหรือไม่บังคับใช้มัน
ผมกำลังคิดว่าจะควบคุมข้อมูลนำเข้า หรือจะห่อมันด้วย data type สำหรับการแสดงผลอย่างปลอดภัยกับข้อมูลที่ไม่น่าเชื่อถือ (ใช้กับ web+log+debug)
ผมอยากให้มาตรฐานมีข้อจำกัดเกี่ยวกับจำนวน Unicode scalar value ที่ใส่ได้ในหนึ่งหน่วยกราฟิก ตอนที่ผมเช็กล่าสุด (แม้จะเป็นเมื่อหลายปีก่อน) มาตรฐานไม่มีการจำกัดแบบนั้น มีแต่คำแนะนำว่าสำหรับแอปพลิเคชันแบบสตรีมให้จำกัดหน่วยกราฟิกไว้ที่ 128 ไบต์ ถ้ามีขีดจำกัดแบบชัดเจนในมาตรฐาน การ implement ก็น่าจะง่ายขึ้นมากและไม่ต้องมีข้อจำกัดเกินจำเป็น
ผมเคยเจอกรณีที่โปรแกรมพังเพราะตั้งสมมติฐานว่า "ไม่มี control character" (ทั้งที่ form feed ใช้แบ่งหน้า, escape character ก็ใช้กับเทอร์มินัลกันบ่อย) และสมมติฐานว่า "ทั้งหมดเป็น UTF-8" ก็พังได้เหมือนกัน (เช่น ไฟล์ข้อมูลเก่า, log ฯลฯ) ถ้าคุณไม่ได้จะทำอะไรกับข้อความในเชิงความหมาย การส่งต่อไปเป็นลำดับไบต์แบบไม่แก้ไขเนื้อหาเลยคือทางเลือกที่ดีที่สุด แต่บางครั้งเพราะ Microsoft Windows คุณก็จำเป็นต้องส่งลำดับ
char16_tแทน UTF-16 ต่างจาก UTF-8 อย่างพื้นฐานในเรื่อง input/output เวลาแปลงข้อมูล คุณต้องใช้แนวทาง WTF-8 สำหรับภายนอก → รูปแบบภายใน (UTF-16) และใช้ surrogate escape สำหรับ (UTF-8) โดยเฉพาะ ห้ามเอาสองวิธีมาปนกันเด็ดขาด