Perl - Taint mode の効果的な活用方法2007年02月12日 17時57分13秒

とりあえず、以前の記事を紹介しておいたほうがいいかな。

長くなってしまったので、記事内の目次を入れておきます。

議題

で、今回のテーマは主に以下の 2つ。

  1. Taint mode を XSS とかの予防に利用することはできないのか?
  2. ブラックリストだからダメで、ホワイトリストならば良い、ってのは正しいのか?

1. については、最初の記事で TB 飛ばした dankogai 氏の記事にそんなことが書いてあったから。

もちろんTaint Modeは銀の弾丸ではない。たとえばXSSなどに対しては効果は薄い。しかしそれを言えばuse strict;も銀の弾丸ではないのだ。

この根拠は、恐らく printsyswrite の引数が汚染チェックの対象外だからなのだと思う。 perlsec (邦訳) にも書いてあるし。

  • system あるいは exec に対する引数リストの 要素として渡した場合には、その要素に対する汚染検査は 行われません

  • printsyswrite の引数に対する汚染検査は 行われません

2. については、2つ目の記事で TB 飛ばした かなだまさかつさんの記事 にて以下の記述があった (強調は T.MURACHI による) ほか、

なお、このtaintperlやPerlの-Tオプションを指定した場合、この手のブラックリスト方式のメタキャラクタ漏れによる脆弱性は発生しない(記号を削るという方式では汚染は除去されていないとみなされる)。

perlsec にも以下のような記述があります。

しかし、汚染の検査は面倒です。あなたのデータの汚染を取り除くだけと いうこともあるでしょう。汚染検査機構をバイパスするためのただ一つの方法は、 マッチした正規表現のサブパターンを参照することです。 Perl は、あなたが $1、$2 などを使って部分文字列を参照したときに、 あなたがパターンを記述したときに何を行うのかを知っていたと仮定します。 つまり、汚染されていないものを束縛しないか、機構全体を無効にするということです。 これは、変数がなんらかの悪いキャラクターを持っているかどうかを 検査するというのではなく、変数が良いキャラクターのみを持っていることの 検査には都合が良いです。 これは(あなたが考えもしないような)悪いキャラクタを見失うことがあまりにも 簡単であるからです。

なるほど確かにそう言われると、ホワイトリスト方式の検査法の方が安全であるかのように見えます。

試行

そんなこんなで、とりあえず何かしらの簡単なプログラムを書いてみて、 汚染モードの有用性についてチェックしてみることにしてみました。 例えば、以下のようなプログラムについて考えてみることにします。

  • 単なるテキストファイルを HTML に変換するコマンドツール。
  • (単一の、または連続する) 空行を、段落の境目として扱う。
  • テキスト中にタグっぽい文字列があっても、それをタグとして扱ったりはしない。wiki 文法的な変換も一切なし。

このプログラムをなんとなーく記述すると、大体以下のようなものになるんじゃないかと思います。 とかいって、この時点でいろいろと突っ込まれそうな気もしないでもないですが。。。

#!/usr/bin/perl -T
# p2h.pl - プレーンテキストを HTML に変換する
use strict;
use warnings;

# 改行モードだけ気を使ってみる
use open IN => ':crlf';
binmode STDIN, ':crlf';

print <<ENDLINE;
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN">
<html>
<head>
    <title>Output from p2h.pl</title>
</head>
<body>

ENDLINE

$/ = '';    # 1 つ以上の連続する空行をレコードの終端として扱う
while (<>){
    chomp;
    s/[<>&"]/'&'.{qw(< lt > gt & amp " quot)}->{$&}.';'/ego;    # HTML の為のエスケープ処理
    print "<p>$_</p>\n\n";
}

# 残りの骨組みを出力
print <<ENDLINE;

</body>
</html>
ENDLINE

__END__

話がややこしくなるので文字セットとかは気にしないことにしましょう。 改行モードだけはなぜか気を使ってますが (Unix 風環境で CR-LF なテキスト処理をすると、:raw ではうまく行かなかったりする為)

それよりもこのプログラムの突っ込みどころは、行頭の sh-bang で -T オプションとかつけちゃってる割に、プログラム的には汚染モードを用いている意味がこれっぽっちも無いところです。 このプログラム、驚くべきことに、汚染チェックは一つも発生しません。 その為、テキスト内容を出力する箇所で行っているエスケープ処理も単なる置換操作であり、 ブラックリスト方式の処理であるにもかかわらず、スクリプトは普通に実行出来ちゃったりします。

murachi@maha ~ $ ./p2h.pl test.txt
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN">
<html>
<head>
    <title>Output from p2h.pl</title>
</head>
<body>

<p>汚染モードをうまく活用すると、
&lt;安全ではない&gt; Perl プログラムを作ってしまう事故が、
いくらか起こりにくくなります。</p>

<p>汚染モードを活用した Perl プログラミングに実践してみましょう。</p>


</body>
</html>
murachi@maha ~ $

Taint モジュール

ここでもし、汚染チェックを行うバージョンの print が存在したらいいんだがなぁ、とか考えてみることにします。 ていうか、実際問題として、引数の汚染チェックを行ってから print するサブルーチンを、 Taint モジュールを用いて実装することは可能です。例えば以下のように。

use Taint qw/tainted/;

sub spill {
    if (tainted @_){
        my (undef, $file, $line) = caller;
        die "Insecure request at $file line $line.\n";
    }
    print @_;
}

理想的には組み込みの print 関数をオーバーライドしてしまいたいところなのですが、 あいにく print 関数はオーバーライドできない模様。 仕方がないので別の名前のサブルーチンで置き換えることにします。 出力先が STDOUT で固定されているので返って使いやすいかもしれません (負け惜しみ)。

#!/usr/bin/perl -T -I.
# p2h.pl - プレーンテキストを HTML に変換する
use strict;
use warnings;

use Taint qw/tainted/;

sub spill {
    if (tainted @_){
        my (undef, $file, $line) = caller;
        die "Insecure request at $file line $line.\n";
    }
    print @_;
}

# 改行モードだけ気を使ってみる
use open IN => ':crlf';
binmode STDIN, ':crlf';

spill <<ENDLINE;
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN">
<html>
<head>
    <title>Output from p2h.pl</title>
</head>
<body>

ENDLINE

$/ = '';    # 1 つ以上の連続する空行をレコードの終端として扱う
while (<>){
    chomp;
    s/[<>&"]/'&'.{qw(< lt > gt & amp " quot)}->{$&}.';'/ego;    # HTML の為のエスケープ処理
    spill "<p>$_</p>\n\n";
}

# 残りの骨組みを出力
spill <<ENDLINE;

</body>
</html>
ENDLINE

__END__

さて、この状態で、さっきと同様にスクリプトを実行しようとした場合、 実行結果は以下のようになり、途中で — まさに汚染された値を出力しようとした段階で — プログラムは異常終了するようになります。 すなわち、汚染チェックはテキストの出力に対しても機能することが、これにて証明されたわけです。

murachi@maha ~ $ ./p2h.pl test.txt
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN">
<html>
<head>
    <title>Output from p2h.pl</title>
</head>
<body>

Insecure request at ./p2h.pl line 34.
murachi@maha ~ $

汚染された値の洗浄

汚染チェックは機能するようになりましたが、最終的に、スクリプトが実行できないことには仕方がないので、 汚染されている値の洗浄を試みることにします。 スクリプトの、注目すべき箇所についてのみクローズアップしてみることにしましょう。

$/ = '';    # 1 つ以上の連続する空行をレコードの終端として扱う
while (<>){
    chomp;
    s/[<>&"]/'&'.{qw(< lt > gt & amp " quot)}->{$&}.';'/ego;    # HTML の為のエスケープ処理
    spill "<p>$_</p>\n\n";
}

とはいえ、実質的にはこの中で行われている置換処理で、値の洗浄は十分に行われていることを、あなたは知っています。 そして、以下のようなコーディングでお茶を濁すという行為に魅入られてしまうことになるやも知れません。

$/ = '';    # 1 つ以上の連続する空行をレコードの終端として扱う
while (<>){
    chomp;
    s/[<>&"]/'&'.{qw(< lt > gt & amp " quot)}->{$&}.';'/ego;    # HTML の為のエスケープ処理
    /(.*)/s;    # 。。。おや?
    spill "<p>$1</p>\n\n";
}

このプログラムは正常に動作しますが、このようなコーディングは避けるべきです。 これでは汚染チェックを利用する意味がありません。 以下のように、より厳密なパターンを記述すべきです。

$/ = '';    # 1 つ以上の連続する空行をレコードの終端として扱う
while (<>){
    chomp;
    s/[<>&"]/'&'.{qw(< lt > gt & amp " quot)}->{$&}.';'/ego;    # HTML の為のエスケープ処理
    /((?:[^<>&"]|&(?:lt|gt|amp|quot);)*)/i;     # 抽出の為のパターンは厳密に。
    spill "<p>$1</p>\n\n";
}

もちろん、実体参照は他にもいろんな種類がありますし (&copy; とか &hearts; とか)、文字コードを直接指定する記述法も存在するわけですが (&#126; とか &#x7e; とか)、とりあえずこのプログラムに関してはこれで十分でしょう。 もしも将来的な拡張の可能性を考慮したいのであれば、以下のようにより一般化したパターンにしても良いと思います。

$/ = '';    # 1 つ以上の連続する空行をレコードの終端として扱う
while (<>){
    chomp;
    s/[<>&"]/'&'.{qw(< lt > gt & amp " quot)}->{$&}.';'/ego;    # HTML の為のエスケープ処理
    /((?:[^<>&"]|&(?:\w+|#(?:\d+|x[\da-f]+));)*)/i;     # 抽出の為のパターンは厳密に (実体参照は一般化してみた)。
    spill "<p>$1</p>\n\n";
}

ところで、抽出の為のパターンを見てみましょう。

    /((?:[^<>&"]|&(?:\w+|#(?:\d+|x[\da-f]+));)*)/i;

このパターン、実体参照を表現する部分を除けば、本質的にはブラックリスト方式による検査法です。 何故なら、[^<>&"] というパターンは、 HTML の非機能文字としては不正な文字となりうる <>&" の 4種類の文字以外にマッチする、という意味だからです。

では、この部分を、ちゃんとしたホワイトリスト方式の検査法として記述する場合、どのように記述すべきか、と考えてみます。 ホワイトリストとして記述する以上、それがどのようなホワイトリストなのかを、仕様として定義しておく必要があるでしょう。 例えば、以下のように。

  • ワード構成文字 (\w)。
  • 空白文字 (\s)。
  • 記号文字 !#$%'()=~|-^\@[`{;:]+*},./?
  • 漢字
  • ひらがな
  • カタカナ (全角および半角)

ちょっと待ってくださいな。今回書いているスクリプトでは、文字セットは気にしていなかった筈。 だとしたら、入ってきた文字が漢字やひらがなやカタカナであることを判定するにはどうしたらいいでしょう? テキストがどんな文字セットであっても動作するように書きたいならば、当然 use utf8 して UTF-8 でスクリプトを書く必要があるし、 Encode::Guess を用いて文字セットの解析もしなければなりません。 更には出力時に使用する文字セットも定義しておかねばならないでしょう。

そんなこんなで、スクリプトは全体的に大幅に書き直されることとなります。 えーっと。。。こんな感じでしょうか?

#!/usr/bin/perl -T -I.
# p2h.pl - プレーンテキストを HTML に変換する
use strict;
use warnings;

use utf8;
use Encode;
use Encode::Guess;

use Taint qw/tainted/;

sub spill {
    if (tainted @_){
        my (undef, $file, $line) = caller;
        die "Insecure request at $file line $line.\n";
    }
    print @_;
}

# 文字セットに気を使ってみる
use open IN => ':crlf';
binmode STDIN, ':crlf';
binmode STDOUT, ':encoding(cp932)';
my @text = do { local $/ = ''; <> };
my $enc = guess_encoding(join('', @text), qw/euc-jp shiftjis iso-2022-jp-1/);
$_ = ref $enc ? $enc->decode($_) : decode((split /\s+or\s+/, $enc)[0], $_)  foreach @text;


spill <<ENDLINE;
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN">
<html>
<head>
    <title>Output from p2h.pl</title>
</head>
<body>

ENDLINE

$/ = '';    # 1 つ以上の連続する空行をレコードの終端として扱う
foreach (@text){
    chomp;
    s/[<>&"]/'&'.{qw(< lt > gt & amp " quot)}->{$&}.';'/ego;    # HTML の為のエスケープ処理
    # ↓↓ ホワイトリスト方式の検査による文字列抽出 ↓↓
    /((?:[\w\s!#\$%'\(\)=~\|\-\^\\\@\[\`\{;:\]\+\*\},\.\/\?\p{Han}\p{Hiragana}\p{Katakana}]|&(?:\w+|#(?:\d+|x[\da-f]+));)*)/i;
    spill "<p>$1</p>\n\n";
}

# 残りの骨組みを出力
spill <<ENDLINE;

</body>
</html>
ENDLINE

__END__

もちろんこのスクリプトは UTF-8 で保存しなければなりません。 シグネチャ (Byte Order 示してないのに BOM とか呼ばれちゃってるかわいそうなヤツ) は不要。

あ、あと、ターミナルに Cygwin を利用している便宜上、 出力するテキストの文字セットは CP-932 にしちゃってます。 この辺は使用する環境に合わせて適当に直すなり、リダイレクトでファイルに書き出すなりして対処してちょ。

で、検査を行うパターンに注目するわけですが、

    # ↓↓ ホワイトリスト方式の検査による文字列抽出 ↓↓
    /((?:[\w\s!#\$%'\(\)=~\|\-\^\\\@\[\`\{;:\]\+\*\},\.\/\?\p{Han}\p{Hiragana}\p{Katakana}]|&(?:\w+|#(?:\d+|x[\da-f]+));)*)/i;

\p{Han} は漢字、\p{Hiragana} はひらがな、 \p{Katakana} はカタカナのことらしいです。 そーいえばつい最近 dankogai 氏のブログで出てきたなー とか思いつつライブラリを漁ってみたわけですが、/usr/lib/perl5/5.8.8/unicore/ とか覗いてみてもおいらには何だかよくわかりませんでした。 これらは use utf8 している状態の \w に含まれちゃうから指定する意味無いってウワサもありますがまぁそれはそれとして。

とりあえずこの状態でスクリプトを走らせて見ると、こんな感じになります。

murachi@maha ~ $ ./p2h.pl test.txt
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN">
<html>
<head>
    <title>Output from p2h.pl</title>
</head>
<body>

<p>汚染モードをうまく活用すると</p>

<p>汚染モードを活用した Perl プログラミングに実践してみましょう</p>


</body>
</html>
murachi@maha ~ $

期待される出力結果と比べてみましょう。 どうやら、読点はひらがなでもカタカナでも漢字でもないんだそうです。 もちろん、句読点や全角文字記号シリーズなども含めた文字属性も存在します。 非公式の情報源ですが、この辺のサイトが非常にわかりやすくまとまっていて便利です。 これによれば、\p{InCJKSymbolsAndPunctuation} とか \p{InHalfwidthAndFullwidthForms} とかを使えば良さそうです。

しかし実際に処理対象となるテキストには、 これらの文字属性に含まれない未知の文字が含まれているかもしれません。 全角アルファベットぐらいなら \w でカバーしてくれそうですが、 いわゆる機種依存文字の類とかはどうでしょう? 数学記号の類もカバーされるのでしょうか? こうして考えれば考えるほど、考慮すべきデータの範囲は爆発的に広がる一方です。

逆転の発想

そもそも、ブラックリスト方式の検査法は、本当に「常に悪」なのでしょうか? この問いに対して、おいらは「状況による」という回答しか、用意することはできません。

本来、この選択は、メンテナンスの優位性に基づいて為されるべきです。 ホワイトリストがもてはやされるのは、運用において安全性を優先したい場合、 その方が「メンテナンスが楽」だからです。 例えば、spam メールをブロックする場合、 無限に増え続ける spam メール送信元ホストや、 本文中に含まれるフィルタリング対象フレーズといったブラックリストを増やしていくより、 メールをやり取りする相手を限定してホワイトリスト化してしまったほうが手っ取り早かったりします。

しかしシステムの実装においては、この考え方が常に正しいわけではありません。 「この値は安全だから」と、安全であることがわかっている値のみをリストアップした結果、 他にも安全でありよく使われるはずの値が使えないと言うことになれば、 ユーザーはそんなシステムに価値を見出すでしょうか?

それでもその実装の結果が本当に安全であるならまだしも、 データの組み合わせのパターンによっては、 安全であると思われていた値が危険な値になる可能性もあるかもしれません。 「ホワイトリスト方式のほうが、ブラックリスト方式よりも安全になりやすい」が固定観念化すれば、 これが実装における盲点とも、なりかねないのではないでしょうか?

「安全なプログラムを書く方法」に、答えはありません。 強いていうならば、プログラマーは「安全なプログラム」ではなく、 「要求通りに動作するプログラム」を書くべきです。 その為には、プログラマーは、期待される要求を整理し、説明できる必要があります

今回のプログラムの場合、出力するデータは HTML なのですから、 HTML の仕様に基づいて出力値の検査を行い、出力すればよいのです。 HTML 4.01 で出力しようとしているので、参照すべき仕様はこの辺でしょうか。 これによれば、HTML の書式は以下の通り、非常に明確です。

  • 要素は、開始タグ <要素名>、内容、終了タグ </要素名> により構成される。
  • 要素の属性は、開始タグの終端の > の手前に、 属性名=値 の形式で、スペースを挟んで列挙する。 ここで、「値」は、二重引用符 " または単引用符 ' で括らなければならない。但し、値がアルファベット、数字、ハイフン -、ピリオド .、アンダースコア _、及びコロン : のみで構成される場合は、 二重引用符または単引用符による括りを省略することができる。
  • 文字参照を用いて、任意の文字を表すことができる。 文字参照は & 記号で始まり、セミコロンで終了する (より詳細な仕様)。
  • <!-- コメント --> の形式で、非表示のコメントを書くことができる。 コメント中に 2つ以上の連続するハイフンを書いてはいけない。

次に、この仕様に基づいて、あなたのプログラムの、出力データに関する仕様を定義します。 この定義が、HTML の仕様からは外れないように、注意しましょう。例えば以下の通りです。

  • 要素の開始タグおよび終了タグ、およびコメントは、プログラムが用意したものだけを出力する。
  • 要素の属性値は、常に二重引用符 " を用いて括るものとする。
  • タグ以外のコンテンツ (すなわち、テキストノード) と、要素の属性値は、事前に常に以下の変換処理を行う。
    • <> は、タグ、およびコメントの括りに使用される為、それぞれ &lt;&gt; に置換する。
    • & は文字参照の接頭子に使用される為、&amp; に置換する。
    • " は要素の属性値を括るのに使用される為、&quot; に置換する。
    また、これらは出力時に常に以下の検査を行う。
    • <>&" 以外の文字は、すべて許可する。
    • 以下の書式の文字列は文字参照として扱い、これを許可する。
      • &(ワード構成文字のみで構成される文字列);
      • &#(10進数値);
      • &#x(16進数値);
    • いずれにも当てはまらない文字は、許可しない。

厳密に言えばテキストノードには " を含んでいても構わないのですが、 属性値のための検査と別個に検査用のルーチンを用意するのは面倒なので、 属性値の括りを二重引用符で固定、ということにして、統一してしまうことにします。 もっとも、今回のプログラムでは属性値は扱っていませんが。。。

それでは、この仕様に基づいて、スクリプトを再び書き直してみましょう。 以下の通りです。

#!/usr/bin/perl -T -I.
# p2h.pl - プレーンテキストを HTML に変換する
use strict;
use warnings;

use Taint qw/tainted/;

# 汚染チェック機能付き出力
sub spill {
    if (tainted @_){
        my (undef, $file, $line) = caller;
        die "Insecure request at $file line $line.\n";
    }
    print @_;
}

# テキストを HTML 用に変換する
sub convertHTMLText ($) {
    $_[0] =~ s/[<>&"]/'&'.{qw(< lt > gt & amp " quot)}->{$&}.';'/ego;
}

# HTML テキストとして正しい値であることを検査する
sub checkHTMLText ($) {
    my ($checked) = $_[0] =~ /((?:[^<>&"]|&(?:\w+|#(?:\d+|x[\da-f]+));)*)/i;
    return  if $_[0] ne $checked;
    $_[0] = $checked;
    
    1
}

# 改行モードだけ気を使ってみる
use open IN => ':crlf';
binmode STDIN, ':crlf';

spill <<ENDLINE;
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN">
<html>
<head>
    <title>Output from p2h.pl</title>
</head>
<body>

ENDLINE

$/ = '';    # 1 つ以上の連続する空行をレコードの終端として扱う
while (<>){
    chomp;
    convertHTMLText $_;
    spill "<p>$_</p>\n\n"   if checkHTMLText $_;
}

# 残りの骨組みを出力
spill <<ENDLINE;

</body>
</html>
ENDLINE

__END__

変換処理や検査をサブルーチン化したりしているものの、 基本的にはこれこれ を組み合わせたものに戻ってしまいました。

あなたの意図することを言おう、自分が言っていることの意味を理解しよう

cover
Effective C++ 改訂第2版
(Scott Meyers 著 / 吉川邦夫 訳 / Addison Wesley / アスキー)

結局のところ、一番大切なのは、ブラックリストを使うのかホワイトリストを使うのか、 といったような方法論に寄りかかるのではなく、 自分がどういった目的・要求のために、何を実装し、何を表現しようとしているのかを、 自分の言葉で的確に説明できること、すなわち説明責任を果たすということなのだと思います。

プログラムの実装に間違いが無いならば、そのプログラムは安全であるはずです。 間違っていないはずなのに安全では無いならば、そもそも仕様が間違っているか、 あるいは潜在的な要求事項を把握できていない (から言葉で説明することもできない) ということなのです。

と、いうわけで、最後は Scott Meyers の言葉で締めてみました (吉川氏の訳語だけど)。 Perl の話題なのに貼り付けた広告がなぜか C++ なのは、多分そのせいです。