Thế giới ký tự vô hình - Sự cố do ký tự zero-width và ký tự không nhìn thấy gây ra

Khoảng 9 phút đọc

Chuỗi ký tự bạn nhập lẽ ra phải là 10 ký tự, nhưng hệ thống khăng khăng nói là 12. Dù nhìn kỹ đến đâu cũng không thấy ký tự thừa nào. Thủ phạm là "ký tự zero-width" - những ký tự hoàn toàn không hiển thị trên màn hình nhưng chắc chắn tồn tại dưới dạng dữ liệu. Bài viết này giải thích các loại và mục đích của ký tự vô hình được định nghĩa trong Unicode, tác động đến việc đếm ký tự, cùng các trường hợp sự cố thực tế và cách xử lý.

Danh mục ký tự vô hình - Những ký tự tồn tại mà không được nhìn thấy

Unicode định nghĩa nhiều ký tự không hiển thị trên màn hình (hoặc có chiều rộng bằng không). Đây không phải "bug" - chúng tồn tại vì những lý do chính đáng trong xử lý văn bản.

Tên ký tựCode PointMục đíchĐếm ký tựChiều rộng hiển thị
Zero Width Space (ZWSP)U+200BChỉ định vị trí có thể ngắt dòngTính là 1 ký tự0
Zero Width Joiner (ZWJ)U+200DNối ký tự (tổng hợp emoji)Tính là 1 ký tự0
Zero Width Non-Joiner (ZWNJ)U+200CNgăn nối ký tựTính là 1 ký tự0
Left-to-Right Mark (LRM)U+200EĐiều khiển hướng văn bảnTính là 1 ký tự0
Right-to-Left Mark (RLM)U+200FĐiều khiển hướng văn bảnTính là 1 ký tự0
Byte Order Mark (BOM)U+FEFFNhận dạng encodingThường không tính0
Soft Hyphen (SHY)U+00ADChỉ định vị trí gạch nốiTính là 1 ký tựThường là 0 (chỉ hiển thị khi ngắt dòng)
Word Joiner (WJ)U+2060Chỉ định vị trí không ngắt dòngTính là 1 ký tự0

Tất cả các ký tự này đều có vai trò chính đáng trong xử lý văn bản. Vấn đề là khi chúng vô tình xâm nhập vào văn bản, chúng âm thầm làm sai lệch số đếm ký tự.

Zero Width Space (U+200B) - Ký tự vô hình phiền toái nhất

Zero Width Space (ZWSP) là ký tự nhúng thông tin "có thể ngắt dòng tại đây" vào văn bản. Nó được sử dụng trong các ngôn ngữ như tiếng Thái và tiếng Khmer không dùng khoảng trắng giữa các từ, cho phép trình duyệt ngắt dòng tại vị trí thích hợp.

Tuy nhiên, ZWSP dễ dàng xâm nhập vào văn bản khi sao chép và dán từ trang web, gây ra các sự cố như:

Xâm nhập mật khẩu đặc biệt nghiêm trọng. Khi ZWSP lẻn vào mật khẩu được sao chép từ trang web, bạn gặp tình huống mật khẩu trông đúng nhưng không thể đăng nhập. Khi xem xét độ dài mật khẩu và bảo mật, sự tồn tại của ký tự vô hình không thể bỏ qua.

Zero Width Joiner (U+200D) - Ký tự ma thuật tổng hợp emoji

Zero Width Joiner (ZWJ) đóng vai trò tích cực nhất trong số các ký tự vô hình. Như đã giải thích chi tiết trong đếm ký tự emoji, ZWJ kết hợp nhiều emoji để tạo ra emoji mới.

Emoji hiển thịThành phầnSố code pointĐếm ký tự (JavaScript)
👨‍👩‍👧‍👦 (Gia đình)👨 + ZWJ + 👩 + ZWJ + 👧 + ZWJ + 👦711 (bao gồm surrogate pair)
👩‍💻 (Nữ kỹ thuật viên)👩 + ZWJ + 💻35
🏳️‍🌈 (Cờ cầu vồng)🏳️ + ZWJ + 🌈46
👨‍🍳 (Đầu bếp nam)👨 + ZWJ + 🍳35

Emoji gia đình 👨‍👩‍👧‍👦 trông như một emoji duy nhất, nhưng bên trong gồm 4 emoji và 3 ZWJ. Thuộc tính .length của JavaScript trả về 11. Trên mạng xã hội có giới hạn ký tự, một emoji như thế này có thể tiêu tốn rất nhiều ký tự.

Ký tự điều khiển hướng - Cơ chế cho ngôn ngữ viết từ phải sang trái

Tiếng Ả Rập và tiếng Hebrew là ngôn ngữ viết từ phải sang trái (RTL). Trong văn bản mà các ngôn ngữ này cùng tồn tại với tiếng Anh (từ trái sang phải, LTR), cần có ký tự vô hình để điều khiển hướng văn bản.

U+200E (Left-to-Right Mark) và U+200F (Right-to-Left Mark) là các ký tự để chỉ định rõ ràng hướng văn bản. Khi chúng vô tình xâm nhập, có thể làm rối loạn thứ tự hiển thị hoặc sai lệch số đếm ký tự.

Năm 2021, lỗ hổng bảo mật "Trojan Source" lợi dụng ký tự điều khiển hướng đã được báo cáo. Bằng cách nhúng ký tự điều khiển hướng vào mã nguồn, code trông bình thường với mắt người nhưng được trình biên dịch hiểu là logic khác. Lỗ hổng này cho thấy ký tự vô hình cũng có thể gây ra rủi ro bảo mật.

BOM (U+FEFF) - Ký tự vô hình ẩn nấp ở đầu file

Byte Order Mark (BOM) là ký tự được thêm vào đầu file văn bản để nhận dạng encoding. BOM của UTF-8 là 3 byte (EF BB BF) và đôi khi được Windows Notepad thêm vào khi lưu file.

BOM bị nhiều chương trình bỏ qua, nhưng gây ra vấn đề trong các trường hợp sau:

Kỹ thuật giấu tin bằng ký tự zero-width (Công nghệ watermark)

Steganography (watermark kỹ thuật số) là công nghệ tận dụng ngược đặc tính "không nhìn thấy" của ký tự vô hình. Bằng cách nhúng các mẫu ký tự zero-width vào văn bản, có thể cài thông tin ẩn mà không thay đổi giao diện.

Phương phápKý tự sử dụngMục đíchĐộ khó phát hiện
Mã hóa ký tự zero-widthU+200B, U+200C, U+200D, U+FEFFNhúng tin nhắn ẩn vào văn bảnCao (mắt thường không thấy)
Theo dõi người dùngTương tựXác định nguồn rò rỉ khi thông tin bị lộCao
Phát hiện sao chépTương tựPhát hiện sao chép nội dung trái phépTrung bình

Ví dụ, bằng cách coi 4 loại ký tự zero-width là thông tin 2-bit (U+200B = 00, U+200C = 01, U+200D = 10, U+FEFF = 11) và chèn ký tự zero-width giữa mỗi từ trong văn bản, có thể giấu dữ liệu nhị phân bên trong.

Công nghệ này đôi khi được doanh nghiệp sử dụng để xác định nguồn rò rỉ tài liệu mật. Bằng cách nhúng mẫu ký tự zero-width khác nhau cho mỗi người nhận, khi tài liệu bị rò rỉ ra ngoài, có thể xác định nguồn rò rỉ.

Phát hiện và loại bỏ ký tự vô hình

Để xử lý đúng văn bản bị ký tự vô hình xâm nhập, bạn cần biết phương pháp phát hiện và loại bỏ.

Phương phápĐối tượngVí dụ code
JavaScript regexCác ký tự zero-width chínhstr.replace(/[\u200B-\u200F\u2028-\u202F\uFEFF]/g, '')
Python regexTương tựre.sub(r'[\u200b-\u200f\u2028-\u202f\ufeff]', '', text)
Trình soạn thảoTất cả ký tự vô hìnhVS Code: Bật "Hiển thị ký tự điều khiển"
Dòng lệnhKý tự vô hình trong filecat -v filename hoặc xxd filename
PHPCác ký tự zero-width chínhpreg_replace('/[\x{200B}-\x{200F}\x{FEFF}]/u', '', $str)

Regex JavaScript /[\u200B-\u200F\u2028-\u202F\uFEFF]/g loại bỏ cùng lúc các ký tự zero-width và ký tự điều khiển hướng phổ biến nhất. Áp dụng bộ lọc này cho giá trị nhập form trước khi gửi đến server có thể ngăn chặn sự không nhất quán trong đếm ký tự do ký tự vô hình.

Tuy nhiên, loại bỏ vô điều kiện tất cả ký tự vô hình là nguy hiểm. ZWJ cần thiết cho việc tổng hợp emoji, loại bỏ nó sẽ phân rã emoji. ZWNJ không thể thiếu cho hiển thị đúng tiếng Ba Tư và tiếng Hindi. Việc loại bỏ ký tự vô hình phải được thực hiện cẩn thận với sự hiểu biết về mục đích và ngữ cảnh.

Xử lý ký tự vô hình theo ngôn ngữ lập trình

Các ngôn ngữ lập trình khác nhau xử lý ký tự vô hình trong mã nguồn theo cách khác nhau. Một số ngôn ngữ bỏ qua chúng, trong khi số khác phát hiện chúng là lỗi.

Ngôn ngữZWSP trong mã nguồnZWSP trong chuỗi ký tựCông cụ phát hiện
JavaScriptCó thể không gây lỗi cú phápGiữ lại như một phần của chuỗiESLint no-irregular-whitespace
PythonSyntaxErrorGiữ lại như một phần của chuỗipylint, flake8
JavaLỗi biên dịchGiữ lại như một phần của chuỗiCheckstyle
GoLỗi biên dịchGiữ lại như một phần của chuỗigo vet
RustLỗi biên dịch (có cảnh báo)Giữ lại như một phần của chuỗiclippy
C/C++Phụ thuộc trình biên dịchGiữ lại như một phần của chuỗiclang-tidy

JavaScript cần đặc biệt chú ý. ZWSP (U+200B) không được coi là "khoảng trắng" trong đặc tả JavaScript, nên có thể được hiểu là một phần của tên biến. Nghĩa là var hellovar he\u200Bllo được coi là hai biến khác nhau. Trông giống nhau là "hello," nhưng là biến khác nhau. Các trường hợp điều này gây ra bug đã thực sự được báo cáo.

Khi xem xét hướng dẫn độ dài tên biến và hàm, rủi ro xâm nhập ký tự vô hình cũng nên được lưu ý. Vì không thể phát hiện bằng mắt trong code review, việc thiết lập cơ chế phát hiện tự động qua linter và cài đặt editor là rất quan trọng.

Các trường hợp sự cố thực tế

Dưới đây là một số sự cố thực tế do ký tự vô hình gây ra.

Công cụ đếm ký tự và ký tự vô hình

Cách các công cụ đếm ký tự xử lý ký tự vô hình khác nhau tùy công cụ. Một số bỏ qua ký tự vô hình khi đếm, số khác đếm nguyên trạng. Nếu không hiểu kiến thức cơ bản về Unicode, bạn không thể xác định tại sao số đếm ký tự khác nhau giữa các công cụ.

Nếu muốn đếm chính xác số ký tự văn bản, chúng tôi khuyên bạn nên kiểm tra sự tồn tại của ký tự vô hình trước, loại bỏ nếu cần rồi mới đếm. Chỉ cần biết rằng "ký tự vô hình" tồn tại đã có thể ngăn ngừa nhiều sự cố liên quan đến đếm ký tự.

Ký tự vô hình và bảo mật - Mối đe dọa không nhìn thấy

Ký tự vô hình cũng có thể trở thành mối đe dọa bảo mật. Cuộc tấn công "Trojan Source" được công bố năm 2021 lợi dụng ký tự điều khiển hướng (U+202A, U+202B, U+202C, U+202D, U+202E, U+2066, U+2067, U+2068, U+2069) để tạo ra khoảng cách giữa giao diện mã nguồn và logic thực thi thực tế.

Phương pháp tấn côngKý tự vô hình sử dụngTác độngBiện pháp đối phó
Trojan SourceKý tự điều khiển hướng (U+202A-U+2069)Logic độc hại không phát hiện được trong code reviewBật cảnh báo trình biên dịch
Tấn công homographKý tự khác nhau trông giống nhau (U+0430 vs U+0061)Giả mạo URL phishingKiểm tra hiển thị Punycode
ZWSP injectionU+200BVượt qua validation đầu vàoLoại bỏ ký tự vô hình phía server
BOM injectionU+FEFFLỗi file parserXử lý tự động loại bỏ BOM

Đây là ví dụ cụ thể về tấn công Trojan Source. Code dưới đây trông với mắt người như "chỉ thực thi xử lý khi truy cập được cho phép," nhưng vì ký tự điều khiển hướng được nhúng vào, kiểm tra truy cập thực tế đã bị vô hiệu hóa.

Cuộc tấn công này đặc biệt nguy hiểm vì nó vô hiệu hóa code review - quy trình xác minh bằng mắt người. Biện pháp đối phó bao gồm bật cài đặt cảnh báo về việc sử dụng ký tự điều khiển hướng trong compiler và linter, và tích hợp bước phát hiện ký tự vô hình vào pipeline CI/CD.

Như đã đề cập trong bài viết cách viết Git commit message, việc sử dụng linter là không thể thiếu cho quản lý chất lượng code. Phát hiện ký tự vô hình cũng là một trong những vai trò quan trọng của linter.

Sách về Unicode và xử lý văn bản có thể tìm thấy trên Amazon.

Chia sẻ bài viết này