1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

受け取ったファイル名をos.path.joinで処理する時の注意

Posted at

ほとんど以下の記事に書いてあることと同じなのですが、やらかしてしまったのでメモします。

os.path.join は以下の仕様になっています。

引数に絶対パスがあると、その前の引数は無視されるので注意。

基本的にこれだけなのですが、この仕様を知らずにどのようなやらかしをしたのか紹介します。

バグっているコード

ファイル名が入力されて、なんらかの処理をした上でテンポラリディレクトリに書く処理があったとします。

from tempfile import TemporaryDirectory
import os

def func(filename: str):
    with open(filename, "r") as f:
        some_str = f.read()
    
    # なんらかの処理
    
    with TemporaryDirectory() as tmp_dir:
        with open(os.path.join(tmp_dir, filename), "w") as f:
            f.write(some_str)
        # テンポラリファイルを使ったなんらかの処理

このとき、filenameがファイル名なら上手く動きます。

絶対パスが入力された場合

filenameに絶対パスが入力された場合、前述の仕様によってos.path.join(tmp_dir, filename)filenameを返します。

したがって、この場合、元のファイルを上書き、いや破壊してしまいます。

これを避けるには、filenameからbasenameを取り出す必要があります。

    with TemporaryDirectory() as tmp_dir:
        with open(os.path.join(tmp_dir, os.path.basename(filename)), "w") as f:
            f.write(some_str)
        # テンポラリファイルを使ったなんらかの処理

相対パスが入力されたとき

実は一番最初の例にはまだバグがあります。
filenamepath/to/fileのような相対パスが入力されたとき、テンポラリディレクトリの下にそのようなサブディレクトリはないのでFileNotFoundErrorを返します。

これはエラー落ちするだけですが、もう一つ、もっと深刻なバグがあります。
最初の例にはディレクトリトラバーサル脆弱性があります。
os.path.join..のような表現も結合するので、

    filename = "../../etc/passwd"
    with TemporaryDirectory() as tmp_dir:
        with open(os.path.join(tmp_dir, filename), "r") as f:
            print(f.read())

などとすれば見えてはいけないものが見えてしまいます。

この例では、filename = "/etc/passwd" でもアウトです。

余談

os.path.join がディレクトリトラバーサル脆弱性を生みやすいのは割と有名なようです。

上のOpenStackのガイドラインでは、ファイルを読む前に、ベースパスの配下にあることを確認しています。

import os
import sys

def is_safe_path(basedir, path, follow_symlinks=True):
    # resolves symbolic links
    if follow_symlinks:
        matchpath = os.path.realpath(path)
    else:
        matchpath = os.path.abspath(path)
    return basedir == os.path.commonpath((basedir, matchpath))


def main(args):
    for arg in args:
        if is_safe_path(os.getcwd(), arg):
            print("safe: {}".format(arg))
        else:
            print("unsafe: {}".format(arg))

if __name__ == "__main__":
    main(sys.argv[1:])

まとめ

  • 外部入力されたファイルパスの結合は意外と大変
  • ファイル名だけ入力してテストしているだけでは不十分
  • ディレクトリトラバーサルに注意

より良い方法があればぜひ教えてください。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?