最終更新:
見えない文字の世界 - ゼロ幅文字・不可視文字が引き起こすトラブル
あなたが入力した文字列は 10 文字のはずなのに、システムは 12 文字だと言い張る。目を凝らしても余計な文字は見えない。犯人は「ゼロ幅文字」- 画面上に一切表示されないのに、データとしては確かに存在する不可視の文字です。この記事では、Unicode に定義された不可視文字の種類と用途、文字数カウントへの影響、そして実際に起きたトラブル事例と対処法を解説します。
不可視文字の一覧 - 見えないのに存在する文字たち
Unicode には、画面に表示されない (または幅がゼロの) 文字が複数定義されています。これらは「バグ」ではなく、テキスト処理に必要な正当な理由で存在しています。
| 文字名 | コードポイント | 用途 | 文字数カウント | 表示幅 |
|---|---|---|---|---|
| ゼロ幅スペース (ZWSP) | U+200B | 改行可能位置の指定 | 1 文字としてカウント | 0 |
| ゼロ幅接合子 (ZWJ) | U+200D | 文字の結合 (絵文字合成) | 1 文字としてカウント | 0 |
| ゼロ幅非接合子 (ZWNJ) | U+200C | 文字の結合を防止 | 1 文字としてカウント | 0 |
| 左から右マーク (LRM) | U+200E | テキスト方向の制御 | 1 文字としてカウント | 0 |
| 右から左マーク (RLM) | U+200F | テキスト方向の制御 | 1 文字としてカウント | 0 |
| バイトオーダーマーク (BOM) | U+FEFF | エンコーディングの識別 | 通常カウントしない | 0 |
| ソフトハイフン (SHY) | U+00AD | ハイフネーション位置の指定 | 1 文字としてカウント | 通常 0 (改行時のみ表示) |
| ワードジョイナー (WJ) | U+2060 | 改行禁止位置の指定 | 1 文字としてカウント | 0 |
これらの文字はすべて、テキスト処理において正当な役割を持っています。問題は、意図せずテキストに混入した場合に、目に見えないまま文字数カウントを狂わせることです。
ゼロ幅スペース (U+200B) - 最も厄介な不可視文字
ゼロ幅スペース (Zero Width Space, ZWSP) は、テキスト中に「ここで改行してもよい」という情報を埋め込むための文字です。タイ語やクメール語のように単語間にスペースを入れない言語で、ブラウザが適切な位置で改行できるようにするために使われます。
しかし、ZWSP は Web ページからテキストをコピー&ペーストする際に混入しやすく、以下のようなトラブルを引き起こします。
- フォームに入力した文字列が「文字数オーバー」と判定される (見た目は制限内なのに)
- パスワードのコピペが失敗する (ZWSP が混入して別の文字列になる)
- 検索が一致しない (同じに見える文字列なのに検索でヒットしない)
- CSV ファイルのデータが正しくパースされない
- プログラムのソースコードに混入してコンパイルエラーになる
特に深刻なのはパスワードへの混入です。Web サイトからパスワードをコピーして貼り付けた際に ZWSP が紛れ込むと、見た目は正しいパスワードなのにログインできない事態が発生します。パスワードの文字数と安全性を考える上でも、不可視文字の存在は無視できません。
ゼロ幅接合子 (U+200D) - 絵文字を合成する魔法の文字
ゼロ幅接合子 (Zero Width Joiner, ZWJ) は、不可視文字の中で最もポジティブな役割を果たしている文字です。絵文字の文字数カウントで詳しく解説していますが、ZWJ は複数の絵文字を結合して新しい絵文字を作り出します。
| 表示される絵文字 | 構成要素 | コードポイント数 | 文字数カウント (JavaScript) |
|---|---|---|---|
| 👨👩👧👦 (家族) | 👨 + ZWJ + 👩 + ZWJ + 👧 + ZWJ + 👦 | 7 | 11 (サロゲートペア含む) |
| 👩💻 (女性テクノロジスト) | 👩 + ZWJ + 💻 | 3 | 5 |
| 🏳️🌈 (レインボーフラッグ) | 🏳️ + ZWJ + 🌈 | 4 | 6 |
| 👨🍳 (男性料理人) | 👨 + ZWJ + 🍳 | 3 | 5 |
家族の絵文字 👨👩👧👦 は、見た目は 1 つの絵文字ですが、内部的には 4 つの絵文字と 3 つの ZWJ で構成されています。JavaScript の .length プロパティでは 11 と返されます。SNS の文字数制限では、この絵文字 1 つで大量の文字数を消費する可能性があります。
方向制御文字 - 右から左に書く言語のための仕組み
アラビア語やヘブライ語は右から左 (RTL: Right-to-Left) に書く言語です。これらの言語と英語 (左から右、LTR) が混在するテキストでは、文字の表示方向を制御する不可視文字が必要になります。
U+200E (Left-to-Right Mark) と U+200F (Right-to-Left Mark) は、テキストの方向を明示的に指定するための文字です。これらが意図せず混入すると、テキストの表示順序が乱れたり、文字数カウントが狂ったりします。
2021 年には、方向制御文字を悪用したセキュリティ脆弱性「Trojan Source」が報告されました。ソースコード中に方向制御文字を埋め込むことで、人間の目には正常に見えるコードが、コンパイラには異なるロジックとして解釈される攻撃手法です。この脆弱性は、不可視文字がセキュリティ上のリスクにもなりうることを示しました。
BOM (U+FEFF) - ファイルの先頭に潜む不可視文字
バイトオーダーマーク (BOM: Byte Order Mark) は、テキストファイルのエンコーディングを識別するためにファイルの先頭に付加される文字です。UTF-8 の BOM は 3 バイト (EF BB BF) で、Windows のメモ帳で保存したファイルに付加されることがあります。
BOM は多くのプログラムで無視されますが、以下のケースで問題を引き起こします。
- PHP ファイルの先頭に BOM があると、
header()関数が動作しない (出力が既に始まったと判定される) - CSV ファイルの先頭に BOM があると、最初のカラム名が正しく認識されない
- JSON ファイルに BOM があると、パーサーがエラーを返す場合がある
- シェルスクリプトの先頭に BOM があると、シバン (
#!/bin/bash) が認識されない
ゼロ幅文字を使ったステガノグラフィ (透かし技術)
不可視文字の「見えない」という特性を逆手に取った技術がステガノグラフィ (電子透かし) です。テキスト中にゼロ幅文字のパターンを埋め込むことで、見た目を変えずに隠し情報を仕込むことができます。
| 手法 | 使用する文字 | 用途 | 検出難易度 |
|---|---|---|---|
| ゼロ幅文字エンコーディング | U+200B, U+200C, U+200D, U+FEFF | テキストに隠しメッセージを埋め込む | 高い (目視不可能) |
| ユーザー追跡 | 同上 | 情報漏洩時の流出元特定 | 高い |
| コピー検出 | 同上 | コンテンツの無断コピーを検出 | 中程度 |
例えば、4 種類のゼロ幅文字を 2 ビットの情報として扱い (U+200B = 00, U+200C = 01, U+200D = 10, U+FEFF = 11)、テキストの各単語の間にゼロ幅文字を挿入することで、バイナリデータを隠し持たせることができます。
この技術は、企業が機密文書の流出元を特定するために使われることがあります。各受信者に異なるゼロ幅文字パターンを埋め込んでおけば、文書が外部に流出した際に、どの受信者から漏れたかを特定できます。
不可視文字の検出と除去
不可視文字が混入したテキストを正しく処理するには、検出と除去の方法を知っておく必要があります。
| 方法 | 対象 | コード例 |
|---|---|---|
| JavaScript 正規表現 | 主要なゼロ幅文字 | str.replace(/[\u200B-\u200F\u2028-\u202F\uFEFF]/g, '') |
| Python 正規表現 | 同上 | re.sub(r'[\u200b-\u200f\u2028-\u202f\ufeff]', '', text) |
| テキストエディタ | 全不可視文字 | VS Code: 「制御文字の表示」を有効化 |
| コマンドライン | ファイル内の不可視文字 | cat -v filename または xxd filename |
| PHP | 主要なゼロ幅文字 | preg_replace('/[\x{200B}-\x{200F}\x{FEFF}]/u', '', $str) |
JavaScript の正規表現 /[\u200B-\u200F\u2028-\u202F\uFEFF]/g は、最も一般的なゼロ幅文字と方向制御文字をまとめて除去します。Web フォームの入力値をサーバーに送信する前にこのフィルタを適用すれば、不可視文字による文字数カウントの不一致を防げます。
ただし、すべての不可視文字を無条件に除去するのは危険です。ZWJ は絵文字の合成に必要であり、除去すると絵文字が分解されてしまいます。ZWNJ はペルシア語やヒンディー語の正しい表記に不可欠です。不可視文字の除去は、用途と文脈を理解した上で慎重に行う必要があります。
プログラミング言語ごとの不可視文字の扱い
プログラミング言語によって、ソースコード中の不可視文字の扱いは異なります。一部の言語は不可視文字を無視し、別の言語はエラーとして検出します。
| 言語 | ソースコード中の ZWSP | 文字列リテラル中の ZWSP | 検出ツール |
|---|---|---|---|
| JavaScript | 構文エラーにならない場合がある | 文字列の一部として保持 | ESLint の no-irregular-whitespace |
| Python | SyntaxError | 文字列の一部として保持 | pylint, flake8 |
| Java | コンパイルエラー | 文字列の一部として保持 | Checkstyle |
| Go | コンパイルエラー | 文字列の一部として保持 | go vet |
| Rust | コンパイルエラー (警告付き) | 文字列の一部として保持 | clippy |
| C/C++ | コンパイラ依存 | 文字列の一部として保持 | clang-tidy |
JavaScript は特に注意が必要です。ZWSP (U+200B) は JavaScript の仕様上「ホワイトスペース」として扱われないため、変数名の一部として解釈される場合があります。つまり、var hello と var he\u200Bllo は異なる変数として扱われます。見た目は同じ「hello」なのに、別の変数。これがバグの原因になった事例は実際に報告されています。
変数名・関数名の長さの目安を考える際にも、不可視文字の混入リスクは頭の片隅に置いておくべきです。コードレビューでは目視で検出できないため、リンターやエディタの設定で自動検出する仕組みを整えることが重要です。
実際に起きたトラブル事例
不可視文字が原因で発生した実際のトラブルをいくつか紹介します。
- GitHub のコードレビュー: プルリクエストのコードに ZWSP が混入し、テストは通るのに本番環境で文字列比較が失敗するバグが発生。diff では検出できず、バイナリエディタで初めて発見された
- E コマースサイトの検索: 商品名にゼロ幅文字が含まれていたため、ユーザーが商品名で検索しても一致せず、売上に影響が出た
- データベースの重複チェック: 見た目が同じメールアドレスが「重複なし」と判定され、同一ユーザーのアカウントが複数作成された。原因はメールアドレス中の ZWSP
- PDF からのコピペ: PDF ファイルからテキストをコピーすると、ソフトハイフン (U+00AD) が大量に混入し、フォーム入力のバリデーションで文字数超過と判定された
文字数カウントツールと不可視文字
文字数カウントツールが不可視文字をどう扱うかは、ツールによって異なります。一部のツールは不可視文字を無視してカウントし、別のツールはそのままカウントします。Unicode の基本を理解していないと、ツール間で文字数が一致しない原因を特定できません。
テキストの文字数を正確にカウントしたい場合は、まず不可視文字の有無を確認し、必要に応じて除去してからカウントすることをお勧めします。「見えない文字」の存在を知っているだけで、文字数にまつわる多くのトラブルを未然に防ぐことができます。
不可視文字とセキュリティ - 見えない脅威
不可視文字はセキュリティ上の脅威にもなりえます。2021 年に発表された「Trojan Source」攻撃は、方向制御文字 (U+202A, U+202B, U+202C, U+202D, U+202E, U+2066, U+2067, U+2068, U+2069) を悪用して、ソースコードの見た目と実際の実行ロジックを乖離させる手法です。
| 攻撃手法 | 使用する不可視文字 | 影響 | 対策 |
|---|---|---|---|
| Trojan Source | 方向制御文字 (U+202A〜U+2069) | コードレビューで見抜けない悪意のあるロジック | コンパイラの警告を有効化 |
| ホモグラフ攻撃 | 見た目が同じ異なる文字 (U+0430 vs U+0061) | フィッシング URL の偽装 | Punycode 表示の確認 |
| ZWSP インジェクション | U+200B | 入力バリデーションの回避 | サーバーサイドでの不可視文字除去 |
| BOM インジェクション | U+FEFF | ファイルパーサーの誤動作 | BOM の自動除去処理 |
Trojan Source 攻撃の具体例を示します。以下のコードは、人間の目には「アクセスが許可された場合のみ処理を実行する」ように見えますが、方向制御文字が埋め込まれているため、実際にはアクセスチェックが無効化されています。
この攻撃は、コードレビューという人間の目による検証プロセスを無力化する点で特に危険です。対策としては、コンパイラやリンターで方向制御文字の使用を警告する設定を有効にすること、CI/CD パイプラインに不可視文字の検出ステップを組み込むことが推奨されます。
Git コミットメッセージの書き方の記事でも触れていますが、コードの品質管理においてリンターの活用は不可欠です。不可視文字の検出も、リンターの重要な役割のひとつです。
Unicode やテキスト処理の技術書は Amazon でも見つかります。