unoh.github.com

いまさら、ディレクトリトラバーサルについて語ってみる

Mon Nov 19 03:10:41 -0800 2007

Keitaです。
ディレクトリトラバーサルという言葉があります。
今では、常識になっており、開発するときには無意識に対策する(されている)人がほとんどだと思います。
ただ、DBにデータを格納することが当たり前の昨今ファイルの扱いをちゃんとできない人もたまーにお会いするのでので、個人的にPHPでやっていることを書いておきます。

どんなものか

詳しい定義は各自調べてもらうとして一例を一つ。
次のコードをみてください
<?php
    $file = $_GET['file'];
    $dir = '/pngdir/';
    $filepath .= $dir . $file;

    if (! file_exists($filepath)) {
        $filepath = $dir . 'nofile.png';
    }

    header("Content-Type: image/png");
    header("Content-Length: " . filesize($filepath));
    $fp = fopen($filepath,'rb');
    fpassthru($fp);
概要としては、「/pngdir/」には、PNG画像があると想定して、「http://example.com/hoge.php?file=ero.png」といったアクセスがあったら、/pngdir/ero.pngを表示するような簡単なコードです。

このコードを見たらすぐに、脆弱性があることがわかる人がほとんどだと思います。

http://example.com/hoge.php?file=../etc/passwd
とすれば、/pngpath/../etc/passwd = /etc/passwdのファイルをそのまま表示しようとします。

システム上の任意のコードが見られてしまうと、たとえば、ソースコードや、パスワードファイルなど秘密にしておくべきファイルが見られますし、何より、個人のサーバなどで性的なファイルをおいた場合の被害は計り知れません。
というわけで、こういう要件に対して安全にコードを書く方法を説明したいと思います。

入力値をチェックする


想定してない文字列のチェックはあってしかるべきですが実際の処理直前で
PHPの関数であるrealpathがそれです。
使い方は、マニュアルを見てもらうとして、これは、渡されたパス文字列の正規化を行います。
シンボリックリンクの場合は、実際のファイルのパスが渡されます。

ただし、../etc/passwdを指定されたときも、上記は/etc/passwdと帰ってくるのでこれだけではたりません。
そこで、そのディレクトリが公開していいファイルのディレクトリかをチェックするするようにします。

そこらへんの処理をまとめるとこんな感じになります。
function is_public_file($public_dir, $path)
{
    if (! is_string($path)) {
        return false;
    }

    $realpath = realpath($path);

    if ($realpath === false) {
        return false;
    }

    if (file_exists($realpath) === false) {
        return false;
    }

    if (strncmp($public_dir, $realpath, strlen($public_dir)) === false) {
        return false;
    }

    return true;
}
realpathが、 BSD システム以外ではファイルが存在しない場合には、falseを返す仕様なので、ファイルの有無も一緒にチェックするようにしてあります。

というわけで、下記のようなコードになりました。
<?php
    $file = $_GET['file'];
    $dir = '/pngdir/';
    $filepath .= $dir . $file;
    $filepath = realpath($filepath);

    if (is_public_file($dir, $filepath) === false) {
        //本来は不正なURLを書いた場合は、適切なロギングやエラー処理をするべき
        $filepath = $dir . 'nofile.png';
    }

    header("Content-Type: image/png");
    header("Content-Length: " . filesize($filepath);
    $fp = fopen($filepath,'rb');
    fpassthru($fp);
もう少しきれいにかけそうな気がひしひしとしますが、こんな感じで概念はつかんでいただけたのではないかと思います。