世界を動かす技術を、日本語で。

RFC 9839と不適切なUnicode

概要

  • Unicode を使う場合、全ての文字を許可すべきではないという指摘
  • RFC 9839 が問題のあるUnicode文字と推奨されるサブセットを定義
  • JSONなどのデータ構造設計時に 問題文字の除外 が重要
  • PRECIS (RFC 8264)は網羅的だが複雑で普及が進んでいない
  • RFC 9839は 実装しやすい簡易な指針 を提供

Unicode利用時の問題点とRFC 9839の概要

  • Unicode はテキストフィールドの標準として推奨
  • しかし、 全てのUnicode文字 を許可するのは危険
  • RFC 9839 は「問題のある文字」を定義し、除外すべき理由を解説
  • 3種類の 推奨サブセット を提示し、実装時の参考に最適
  • RFCは 10ページ と短く、ソフトウェア・ネットワーク技術者向けに記述

問題となるUnicode文字の具体例

  • U+0000 :ヌル文字。多くのプログラミング言語で誤動作の原因
  • U+0089 :C1制御文字。用途不明で多くのシステムで扱い困難
  • U+DEAD :非対のサロゲート。UTF-8ではエンコード禁止
  • U+7FFFF :ノンキャラクター。通信で利用禁止
  • これらのコードポイントは 相互運用性やセキュリティの問題 を引き起こす

JSON・他フォーマットでの問題

  • JSON 仕様は上記の問題文字も許可してしまう
  • JSON設計者のDoug Crockfordも 現代なら制限したはず との見解
  • 既存仕様のため、 プロトコル実装側で明示的に除外 する必要
  • 他のデータ形式(CBOR, TOML, XML, YAMLなど)も 対応状況はまちまち

PRECISとの比較

  • PRECIS(RFC 8264) は広範囲かつ詳細な文字セット規定
  • しかし 複雑さ・バージョン依存 が普及の障壁
  • Unicodeの新バージョン対応や アプリケーション間の整合性維持が困難
  • RFC 9839 はシンプルかつ実用的な選択肢を提供

RFC 9839のサブセットと実装例

  • RFC 9839は 3つのサブセット (Scalars, XML, Assignables)を定義

  • それぞれ どの問題文字を除外するか を明確化

  • Go言語向けの 検証ライブラリ も公開(最適化・バージョン管理は今後)

  • サブセットごとの対応状況を 表形式で整理

    • Surrogates, Legacy controls, Noncharactersの各除外有無
      • CBOR: サロゲート除外
      • I-JSON: サロゲート・ノンキャラクター除外
      • JSON: いずれも未対応
      • XML/YAML: 一部対応

RFC 9839策定の経緯と教訓

  • 多数の有識者による 議論・改善 を経て公開
  • 個人提出RFCは 大変だが価値あり、ただしWorking Group経由の方が効率的
  • 今後、 新しいデータ構造やプロトコル設計時にはRFC 9839の参照が推奨

まとめ

  • テキストフィールド設計時はUnicodeの「問題文字」除外が必須
  • RFC 9839 を活用し、 安全・互換性の高い実装 を推進
  • 仕様策定時の明確な根拠 としてRFC 9839の引用が有効

Hackerたちの意見

Unicodeって、良いところもあるけど、特定の文字を除外しなきゃいけないのが面倒だよね。まるで複雑さのジャングルみたい。言語の書き方をたくさん形式化しようとした結果なんだろうけど、他の文字と比べて特別な文字を考えなきゃいけないのが本当に嫌だ。唯一見つけたまともな対処法は、Unicodeの文字列を独自のデータユニット形式として扱うこと。受け入れて、保存して、表示して、データとして(意味じゃなく)等しいかどうかを比較することはできるけど、その内容について考えようとは思わない。連結するのさえも気が進まないよ。

例えば、最初の文字列が孤立した絵文字修飾子で終わって、2つ目の文字列が修飾可能な絵文字で始まると、もう問題が起きるよね。そこからはもっとエキゾチックなものが増えていく一方だし。

Unicodeは本当に底なしの雑学と悪い決定の宝庫だよね。別の例として、記事のRFCは、レガシーASCII制御文字を許可することに対して、人間にとって表示が混乱する可能性があるから注意するよう警告しているけど、セキュリティ上の懸念から「可能な限り避けるべき」とされている明示的方向オーバーライド文字については何も言っていない。

確かに、サロゲートや制御コードのような複雑さは、言語を書くための試みから来てるわけじゃないから、ただのひどいデザインが残ってるだけだね。

ユニコードはクソだけど、他のエンコーディング標準よりはマシだよ。

「意図的に制御文字を含まない」という盲目的な前提で、実際のプログラムが壊れたことがある。特にページ分割用のものではフォームフィードがよく使われるし、端末用に設計されたものではエスケープが一般的だよね。「完全にUTF-8である」という前提も危険で、古いデータファイルやログは簡単には消えないから。テキストで何か役立つことをしていないなら、バイト列をそのまま通すのが一番いい。残念ながら、Microsoft Windowsがあるから、時にはchar16_tのシーケンスを通さなきゃいけないこともある。UTF-16の最悪なところは、無効なUTF-16が無効なUTF-8とは根本的に違うこと。これらの間で変換する時(実際には、外部データを処理用の内部形式に変換する時)、前者はWTF-8を使えるけど、後者はPythonスタイルのサロゲートエスケープを使うことになる。これらを混ぜることはできないんだ。

確信はないけど… 一方では、ペアになっていないサロゲートのような問題のある(または無効な)文字には同意するよ。でも、最悪のシナリオは、データ構造やプロトコルを設計している人たちが、適切にエスケープされた文字の任意のクラスを禁止しなきゃいけないと感じることだと思う。例えば、ユーザー名の検証は別のレイヤーの仕事だよね。ユーザー名が60文字未満で、絵文字やzalgoテキストがなく、ヌルバイトもないことを確認して、APIから適切なエラーを返したい。JSONパースが全く別のレイヤーの事前検証で失敗するのは避けたいし。ユーザー名には明らかにダメなクラスがあるけど、もし変なタブを使っているテキストファイルを送ったらどうなるの?自分の言語のutf8「文字列」タイプで動くものがエンコード可能であることを期待してるし、ヌルバイトの使い道もたくさん見てる。実際、ヌルバイトは野生のJSONでもよく見かけるし。一方で、制限された「普通の」Unicode文字のセットを使わなきゃいけないなら、標準があると便利だと思う。みんなが自分のミニスタンダードを作るよりはマシだから。だから、そのアイデアは好きだけど、ブログの主張や例には納得できないな。

うん、2025年の低レベルなワイヤプロトコルで文字列表現を選ぶなら、実際に防御可能な選択肢はこれくらいだと思うよ。

  • 「Unicodeスカラー」、つまり「正しく形成されたUTF-16」、つまり「Pythonの文字列型」
  • 「潜在的に不正なUTF-16」、つまり「WTF-8」、つまり「JavaScriptの文字列型」
  • 「潜在的に不正なUTF-8」、つまり「バイトの配列」、つまり「Goの文字列型」
  • 上記のいずれかに加えて、「U+0000なし」、もしバッファオーバーフローの脆弱性が知られる前に設計された言語やライブラリとインターフェースしなきゃいけないならね。

アイデアはいいと思うけど、ブログの議論や例には納得できないな。どの部分が、そしてなぜ?ティムとポールは、ほとんどの人よりも約10万倍の経験があるから、具体的な批判を読むのは面白いと思う。これがJSON特有の標準だと思ってるの?

RFCがどのUnicodeがプロトコルやデータフォーマットに悪いかについてのもので、今後それを設計する際に避けるべきUnicodeがどれかを知るためのRFCがあるって部分を見逃したんじゃない?「Xのファイルがあったらどうする?」とか「ユーザー名にYを入れたい場合は?」って話じゃなくて、「普通で、ちゃんと動く、Unicodeテキストベースのプロトコルやデータフォーマットを作りたい場合はどうすればいい?」ってことなんだ。JSONやウェブの話じゃなくて、それは単なる議論のための例だよ。RFCは、プロトコルやデータフォーマットが何に使われるかには全く無関心で、テキストベース、特にUnicodeテキストベースであればいいんだ。だから、ブログを誤解してるみたいだし、今はRFCを読むべきだよ。短いから、数分で https://www.rfc-editor.org/rfc/rfc9839.html をサクッと見て、君が注目してることとは実際には関係ないってわかると思うよ。

マジで、プレーンテキストファイルにC0(LFと、渋々認めるけどHT以外)は使わないでほしい。確かに「ANSIカラーのマークアップ」を保存したい気持ちはわかるけど(「VT100カラー」じゃないし、VTシリーズは1994年のVT525までモノクロだったからね)、それってもうプレーンテキストじゃないんじゃない? どちらかと言えば、マークアップ形式のテキストで、Markdownに似てるけど、C0範囲を使う別のエンコーディングを使ってるやつだよ。お気に入りの出力デバイスがデータをきれいに表示できるからって、それがプレーンテキストだとは限らないよね。確かに、プレーンテキストにエンコードされるマークアップ形式はたくさんあって、相互運用性が良くなるのはわかるけど。

ユーザー名に絵文字を禁止する理由は何?

PRECISion · IETFが「Bad Unicode」に関して2025年まで待った理由を疑問に思うかもしれないけど、実際にはそうじゃない。RFC 8264があるよ:PRECISフレームワーク:アプリケーションプロトコルにおける国際化された文字列の準備、施行、比較。最初のPRECISの前身は2002年に発表されたんだ。8264は43ページもあって、9839よりもずっと多くの潜在的なBad Unicodeの問題について詳しく議論している。RFC 8265と8266もチェックすることをお勧めするよ:アプリケーションプロトコルにおける国際化された文字列の準備、施行、比較:ユーザー名とパスワードの表現 — https://www.rfc-editor.org/rfc/rfc8265 ニックネームを表す国際化された文字列の準備、施行、比較 — https://www.rfc-editor.org/rfc/rfc8266 一般的に言って、テキストの方向を変える可能性のあるユーザー名や、入力したデバイスによって異なるバイト表現を持つパスワードは表示したくない。これらのRFCにはそれを避けるための特定のプロファイルがあるよ。こういう目的のためには、失敗がオープンになるよりもクローズになる方が安全だと思う。最新の絵文字をユーザー名から禁止する方が、ユーザー名を表示するすべてのページを壊す可能性を許すよりはマシだよ。

閉じることに失敗すると、20年後も20年前の絵文字をサポートしていないままで、ユーザーがイライラすることになるんだよね…。

グラフィカルユニットが持てるUnicodeスカラー値の数に制限を設けるべきだと思う。数年前にチェックした時には、標準にはそんな制限はなかったけど、グラフィカルユニットを「ストリーミングアプリケーション」のために128バイトに制限することを推奨する文言はあった。これを標準に取り入れるか、少なくともスカラー単位に制限を設けることで、実装や処理がずっと楽になると思うし、合理的なアプリケーションを制限することもないはず。

Unicodeはすでに「一般カテゴリ」を定義していて、これらの「変な」文字のいくつかを分類していることに注意する価値があるよ。例えば、Pythonで、import unicodedataして、print(unicodedata.category(chr(0)))とprint(unicodedata.category(chr(0xdead)))を実行すると、「Cc」(制御)と「Cs」(サロゲート)が表示される。

「レガシーコントロール」をリテラルだけでなく、エスケープされた文字列(例: "\u0027")としても除外するのはやりすぎな気がする。C1は基本的に使われてないと思うけど、それはいいとして、いくつかのC0文字は実際に使われてる(エスケープ、EOF、NUL)。個人的には、いくつかの使用例は正当で合理的だと思う。

U+001E(レコードセパレーター)みたいな珍しいC0文字をうまく使ってるよ。ドキュメントから除外するのは理にかなってると思うけど、テキストデータストリームでは役立つこともあるよね。

ほとんどは、文字列をUTF-8として解釈する際に無効なUTF-8バイトシーケンスを拒否することで処理されてるみたいだね(理想的には、完全にエラーを返す)。つまり、ペアになってないサロゲートや、他のサロゲートも、すでにUTF-8バイトシーケンスとしては違法だし。UTF-8を使う文字列を持つ有能な言語なら、そういうシーケンスが与えられたときにはエラーを返すべきだよ。問題のあるコードポイントのリスト(非印刷可能なものなど)は、個人的にはもっと有用で重要だと思う。でも、それらを普通の違法なUTF-8バイトシーケンスとは別の概念として扱うのは有益だね。

それは妥当だね。選択はアプリケーションの実装者に任せるべきで、一般的なライブラリが決めるべきじゃないと思う。ユーザー名専用のJSONパーサーには出会ったことがないな。

一つの決断に迷ってるんだ:入力を制御するか、信頼できない入力を安全に表示するデータ型でラップするか(ウェブ+ログ+デバッグ)。

これがどう役立つのか理解できないな。受け入れるユニコードのサブセットを定義しても、値が型定義に従っているか確認する必要はなくならないよね。

そうだね。それは、ユーザー名が何かを知らない汎用的な低レベルのパースコード用だよ。フィールド特有のバリデーションも必要になるね。