正規表現で「制御文字以外」のチェック
一般に、セキュアコーディングの基本として入力値の検証(Validation)をせよということになっていますが、これが変な方向に行くといわゆる「サニタイズ」のような手法になってしまいます。以前も指摘したように、アプリケーションとしてのValidationは仕様に従って行うべきものです。
ですが、概ねどの場合でも行うべき検証として以下があると思います。
- 文字エンコーディングの妥当姓
- 制御文字(\x00〜\x1f, \x7f)のチェック
- 文字列長のチェック
このうち後ろ二つを正規表現として書くにはどうすればいいかを考えていました。つまり、「制御文字以外の文字でm文字以上n文字以下」というようなチェックです。m文字以上、n文字以下は、{m,n}で書けるので、問題は「制御文字以外の文字」です。これはtextタイプのinput要素で、かつアプリケーション仕様としては文字種の制限をしない場合を想定しています。プログのタイトル欄や、住所欄などが典型例です。
私は正規表現に特別詳しいわけではないので、試行錯誤的に調査して、POSIX正規表現の[:print:]を使って書けばいいのかと思いました。以下のような感じです。
if ("this is a pen" =~ /^[[:print:]]+$/) { # ... これは成立する
ところが、[:print:]は、use utf8;しないと日本語対応していないことと、utf8フラグが立った状態では、タブ、改行などにもマッチすることが分かりました。
PHPにも同様の問題があり、文字エンコーディングがUTF-8の場合は、タブ、改行などに[:print:]がマッチします。これは鬼車の仕様だそうです。wassr.jpにて、moriyoshitさんから教えていただきました。さらに、この件についての調査結果をブログにまとめてくださいました。ありがとうございます。
さらに、Javaの場合ですと、[:print:]に相当する\p{Print}がUS-ASCIIのみに対応しているということで使えない。
というわけで、[:print:]の使用をあきらめ、別の方法を考えることにしました。私に思いついたのは以下の二つですが、
- 16進で指定する
- 制御文字*以外*という指定
16進で指定する方法は、ミスが生じやすいことと、可読性が悪いことが気になりました。うっかり、DEL(\x7f)を忘れそうだし、制御文字のうち改行やタブを許したい場合にややこしくなりそうです。Javaの場合は、サロゲートペアを含む正規表現を16進で指定する方法が分からない。
そこで、制御文字*以外*を指定する方法として、[:^cntrl:]に着目しました。Javaの場合は\P{Cntrl}です。Perlの場合だと、次のように書けます。
use utf8; #... if ("日本語にも対応" =~ /^[[:^cntrl:]]+$/) { # ... これは成立する
テキストエリアの場合は改行を(場合によっては水平タブも)許可しないといけませんが、これも簡単に追加できます。
use utf8; #... if ("日本語\r\n対応" =~ /^[\r\n\t[:^cntrl:]]+$/) { # ... これは成立する
と、なんとかここまで漕ぎ着けたのですが、残る問題は、
- もっといい方法はないのか?
- Perlの場合末尾の\nがうまくチェックできない
後者についてですが、以下のサンプルプログラムは *match* を表示します。正規表現のオプションs, m, msのいずれを追加しても同じ結果でした。
my $a = "1234\n"; if ($a =~ /^[0-9]+$/) { print "*match*\n"; }
Perlにとって行末の\nは空気みたいな存在なんでしょうか。Webアプリ場合、通常改行は\r\nですが、その場合はチェックできます。問題は\n(ラインフィード)だけがついている場合です。パーセントエンコードで書くと、1234%0a ですね。
今のところ上手い方法を思いついてないので、一行用と複数行用で処理を分けることを考えていますが、もしよい方法をご存じの方があれば教えて下さい。
2009/9/24追記
ITproに、このあたりをまとめていますので参考になさってください。
第12回■主要言語別:入力値検証の具体例 | 日経 xTECH(クロステック)