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
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 Point | Mục đích | Đếm ký tự | Chiều rộng hiển thị |
|---|---|---|---|---|
| Zero Width Space (ZWSP) | U+200B | Chỉ định vị trí có thể ngắt dòng | Tính là 1 ký tự | 0 |
| Zero Width Joiner (ZWJ) | U+200D | Nối ký tự (tổng hợp emoji) | Tính là 1 ký tự | 0 |
| Zero Width Non-Joiner (ZWNJ) | U+200C | Ngă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ản | Tính là 1 ký tự | 0 |
| Right-to-Left Mark (RLM) | U+200F | Điều khiển hướng văn bản | Tính là 1 ký tự | 0 |
| Byte Order Mark (BOM) | U+FEFF | Nhận dạng encoding | Thường không tính | 0 |
| Soft Hyphen (SHY) | U+00AD | Chỉ định vị trí gạch nối | Tính là 1 ký tự | Thường là 0 (chỉ hiển thị khi ngắt dòng) |
| Word Joiner (WJ) | U+2060 | Chỉ định vị trí không ngắt dòng | Tí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ư:
- Dữ liệu nhập form bị đánh giá "vượt giới hạn ký tự" (nhìn bằng mắt thì trong giới hạn)
- Sao chép dán mật khẩu thất bại (ZWSP xâm nhập biến thành chuỗi khác)
- Tìm kiếm không khớp (chuỗi trông giống nhau nhưng không tìm thấy)
- Dữ liệu file CSV không phân tích đúng
- Xâm nhập mã nguồn chương trình gây lỗi biên dịch
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ần | Số code point | Đếm ký tự (JavaScript) |
|---|---|---|---|
| 👨👩👧👦 (Gia đình) | 👨 + ZWJ + 👩 + ZWJ + 👧 + ZWJ + 👦 | 7 | 11 (bao gồm surrogate pair) |
| 👩💻 (Nữ kỹ thuật viên) | 👩 + ZWJ + 💻 | 3 | 5 |
| 🏳️🌈 (Cờ cầu vồng) | 🏳️ + ZWJ + 🌈 | 4 | 6 |
| 👨🍳 (Đầu bếp nam) | 👨 + ZWJ + 🍳 | 3 | 5 |
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:
- BOM ở đầu file PHP khiến hàm
header()không hoạt động (bị đánh giá là đầu ra đã bắt đầu) - BOM ở đầu file CSV khiến tên cột đầu tiên không được nhận dạng đúng
- BOM trong file JSON có thể khiến parser trả về lỗi
- BOM ở đầu shell script khiến shebang (
#!/bin/bash) không được nhận dạng
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áp | Ký tự sử dụng | Mục đích | Độ khó phát hiện |
|---|---|---|---|
| Mã hóa ký tự zero-width | U+200B, U+200C, U+200D, U+FEFF | Nhúng tin nhắn ẩn vào văn bản | Cao (mắt thường không thấy) |
| Theo dõi người dùng | Tương tự | Xác định nguồn rò rỉ khi thông tin bị lộ | Cao |
| Phát hiện sao chép | Tương tự | Phát hiện sao chép nội dung trái phép | Trung 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ượng | Ví dụ code |
|---|---|---|
| JavaScript regex | Các ký tự zero-width chính | str.replace(/[\u200B-\u200F\u2028-\u202F\uFEFF]/g, '') |
| Python regex | Tương tự | re.sub(r'[\u200b-\u200f\u2028-\u202f\ufeff]', '', text) |
| Trình soạn thảo | Tất cả ký tự vô hình | VS Code: Bật "Hiển thị ký tự điều khiển" |
| Dòng lệnh | Ký tự vô hình trong file | cat -v filename hoặc xxd filename |
| PHP | Các ký tự zero-width chính | preg_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ồn | ZWSP trong chuỗi ký tự | Công cụ phát hiện |
|---|---|---|---|
| JavaScript | Có thể không gây lỗi cú pháp | Giữ lại như một phần của chuỗi | ESLint no-irregular-whitespace |
| Python | SyntaxError | Giữ lại như một phần của chuỗi | pylint, flake8 |
| Java | Lỗi biên dịch | Giữ lại như một phần của chuỗi | Checkstyle |
| Go | Lỗi biên dịch | Giữ lại như một phần của chuỗi | go vet |
| Rust | Lỗi biên dịch (có cảnh báo) | Giữ lại như một phần của chuỗi | clippy |
| C/C++ | Phụ thuộc trình biên dịch | Giữ lại như một phần của chuỗi | clang-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 hello và var 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.
- Code review trên GitHub: ZWSP xâm nhập vào code của Pull Request, khiến so sánh chuỗi thất bại trên production trong khi test vẫn pass. Không thể phát hiện trong diff, chỉ được tìm ra bằng binary editor
- Tìm kiếm trên trang thương mại điện tử: Tên sản phẩm chứa ký tự zero-width, khiến người dùng tìm kiếm theo tên sản phẩm không ra kết quả, ảnh hưởng doanh số
- Kiểm tra trùng lặp database: Địa chỉ email trông giống nhau bị đánh giá "không trùng lặp," tạo nhiều tài khoản cho cùng một người dùng. Nguyên nhân là ZWSP trong địa chỉ email
- Sao chép dán từ PDF: Sao chép văn bản từ file PDF đưa vào lượng lớn soft hyphen (U+00AD), khiến validation nhập form đánh giá vượt giới hạn ký tự
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ông | Ký tự vô hình sử dụng | Tác động | Biện pháp đối phó |
|---|---|---|---|
| Trojan Source | Ký tự điều khiển hướng (U+202A-U+2069) | Logic độc hại không phát hiện được trong code review | Bật cảnh báo trình biên dịch |
| Tấn công homograph | Ký tự khác nhau trông giống nhau (U+0430 vs U+0061) | Giả mạo URL phishing | Kiểm tra hiển thị Punycode |
| ZWSP injection | U+200B | Vượt qua validation đầu vào | Loại bỏ ký tự vô hình phía server |
| BOM injection | U+FEFF | Lỗi file parser | Xử 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.