改行文字とフィルター

Date2026/02/02

メールに変な文字が混ざってます!

ある日のこと。
「" " っていう変な文字がメールで送られてくるんですが、なんでですかね?」という1通のチャットが届いた。
詳しく聞いてみると、PHPで作られたシンプルなフォームで、「入力->確認->メール送信->完了」という流れになっている中で、メールだけ変な文字が混入しているとのこと。
おそらく改行文字がエスケープされてしまったことが原因だとわかったが、なぜ起こってしまったのだろうか?

原因となっているソースコードを見たところ、

$content = filter_input(INPUT_POST, 'content', FILTER_SANITIZE_SPECIAL_CHARS);

というコードを発見した。
これは私が、初めて1からPHPでフォーム送信機能を作ったときに実装したコードである。

そもそも、「フィルターをかける」という処理について詳しく理解しておらず、「console.log(‘hello’)とかを埋め込まれたら困るからやった方が良い」くらいのイメージだったと思う。
なので、なぜ今回"\n"がエスケープされてしまったのかについて正直よくわからなかった。

FILTER_SANITIZE_SPECIAL_CHARSというフィルター

FILTER_SANITIZE_SPECIAL_CHARSは、以下のHTMLエンコードおよび、ASCII 値が32より小さい値をフィルタします。 ', ", <, >, &と公式で記述されている。
ASCII 値が32より小さい値というのがポイントで、これを理解する必要があった。
ASCII制御文字

ASCII 32未満では、改行文字(\n)や復帰文字(\r)などを含んでいる。(エスケープシーケンスの列を参照)
Wikipedia Wikipedia

要するに、本来エスケープしたいものである[’, “, <, >, &]に加えて、過剰にASCII 32未満をエスケープしてしまっていたのだ。
もしこのまま修正を加えるなら、以下のように書くべきである。

$content = filter_input(INPUT_POST, 'content', FILTER_SANITIZE_FULL_SPECIAL_CHARS);

// FILTER_SANITIZE_FULL_SPECIAL_CHARS (int)
// このフィルタは、 htmlspecialchars() を ENT_QUOTES と一緒にコールした場合と同じです。 [参考](https://www.php.net/manual/ja/filter.constants.php#constant.filter-sanitize-full-special-chars)

もっと正しく直すなら

今回はFILTER_SANITIZE_SPECIAL_CHARSをFILTER_SANITIZE_FULL_SPECIAL_CHARSに変更することで、表面上の修正は完了である。
しかし、そもそもの仕様として「なぜテキストをエスケープするのか?」を考えてみると、これを直すだけでは最適ではないことが分かる。

そもそもエスケープの目的は、HTMLレンダリング時に挙動が変化することを防ぐためである。
つまり、HTMLレンダリングが行われない場所でエスケープしてしまうと、むしろ文字列の本来の意味が失われてしまう。
すなわち、PHP側ではプレーンな状態で持っておいて、HTMLレンダリング時に変換するのが正しい。

<textarea name="content">
    <?php echo htmlspecialchars($content, ENT_QUOTES, 'UTF-8'); ?>
</textarea>

// FILTER_SANITIZE_FULL_SPECIAL_CHARS = htmlspecialchars() を ENT_QUOTES と一緒にコールした場合と同じ

これにより、後にメール送信やDB保存をするときはプレーンなテキストが保たれる。
ちなみに、改行を反映したいときはnl2br()関数を使えばよい。

おわりに

実は、FILTER_SANITIZE_FULL_SPECIAL_CHARSをしたときのコードはエージェントなしでChatGPTと開発していた頃に書いたものである。
ChatGPTへコンテキストの受け渡しが不十分であったために、とりあえず全部サニタイズするという方針で突き進んでしまった。
エージェント化したことでこの事故は自然消滅したが、それでも実装者がちゃんと理解していないと似たような事象が起こりそうだなと思った。
(まだ割と古い関数を使ってくるので、PHP8.1で非推奨になったFILTER_SANITIZE_STRINGとかが出てくることもザラ)

あと、さっきFILTER_SANITIZE_SPECIAL_CHARSについて聞いたところ誤回答があった。(GPT-5.2-Codex High)

FILTER_SANITIZE_SPECIAL_CHARS 単体では改行(\r / \n)を数値エンティティに変換しません。  
変換されるのは <, >, &, ", ' などです。
\r / \n が &#13; / &#10; になるのは、FILTER_FLAG_ENCODE_LOW を付けた場合です。

めっちゃ嘘。公式のURLを貼って読ませたところ、修正してくれた。
最近はエージェントに頼りきりだが、なんか怪しいなと思える嗅覚はしっかり残しておきたいと思った出来事だった。