ほとんど以下の記事に書いてあることと同じなのですが、やらかしてしまったのでメモします。
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)
# テンポラリファイルを使ったなんらかの処理
相対パスが入力されたとき
実は一番最初の例にはまだバグがあります。
filename
にpath/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:])
まとめ
- 外部入力されたファイルパスの結合は意外と大変
- ファイル名だけ入力してテストしているだけでは不十分
- ディレクトリトラバーサルに注意
より良い方法があればぜひ教えてください。