ディレクトリとその内容をユーザーに公開するファイル ブラウザ アプリケーションがあります。
ファイルパスであるユーザー入力をサニタイズして、「/tmp/」などの絶対パスと「../../etc」などの相対パスを許可しないようにしたい
プラットフォーム間でこれを行う python 関数はありますか?
ディレクトリとその内容をユーザーに公開するファイル ブラウザ アプリケーションがあります。
ファイルパスであるユーザー入力をサニタイズして、「/tmp/」などの絶対パスと「../../etc」などの相対パスを許可しないようにしたい
プラットフォーム間でこれを行う python 関数はありますか?
A/./B -> A/B
また、A/B/../C -> A/C
パスを取り除く方法を探している人にも。そのために使えますos.path.normpath
。
パスをサニタイズするために利用できる方法のどれにも満足できなかったので、独自の比較的包括的なパス サニタイザーを作成しました。これは、パブリック エンドポイント (http アップロード、REST エンドポイントなど) から入力を取得し、結果のファイル パスにデータを保存する場合にシステムに損傷を与えないことを保証するのに適しています**。(注: このコードは Python 3+ を対象としています。2.x で動作させるには、おそらくいくつかの変更を加える必要があります)
※無保証!ご自身で十分に確認せずに、このコードに依存しないでください。
**繰り返しますが、保証はありません! あなたはまだ何かクレイジーなことをして、* nixシステムのルートパスを/dev/
または/bin/
そのようなものに設定することができます. そうしないでください。Windows には、損傷を引き起こす可能性のあるエッジ ケース (デバイス ファイル名など) もいくつかありsecure_filename
ます。werkzeug
utils
get_root_path
これを行う場所については、関数を確認してください。ルート パスの値は、ユーザーからの入力ではなく、独自の構成からのものであることを確認してください。make_valid_file_path
。必要に応じて、パラメーターでサブディレクトリ パスを渡すことができpath
ます。これはルート パスの下のパスであり、ユーザー入力から取得できます。必要に応じて、パラメーターでファイル名を渡すことができfilename
ます。これは、ユーザー入力から取得することもできます。渡すファイル名のパス情報は、ファイルのパスを決定するために使用されません。代わりに、ファイル名の有効で安全なコンポーネントにフラット化されます。
os.path.join
パス コンポーネントになります。OK、十分な警告と説明があります。コードは次のとおりです。
import os
def ensure_directory_exists(path_directory):
if not os.path.exists(path_directory):
os.makedirs(path_directory)
def os_path_separators():
seps = []
for sep in os.path.sep, os.path.altsep:
if sep:
seps.append(sep)
return seps
def sanitise_filesystem_name(potential_file_path_name):
# Sort out unicode characters
valid_filename = normalize('NFKD', potential_file_path_name).encode('ascii', 'ignore').decode('ascii')
# Replace path separators with underscores
for sep in os_path_separators():
valid_filename = valid_filename.replace(sep, '_')
# Ensure only valid characters
valid_chars = "-_.() {0}{1}".format(string.ascii_letters, string.digits)
valid_filename = "".join(ch for ch in valid_filename if ch in valid_chars)
# Ensure at least one letter or number to ignore names such as '..'
valid_chars = "{0}{1}".format(string.ascii_letters, string.digits)
test_filename = "".join(ch for ch in potential_file_path_name if ch in valid_chars)
if len(test_filename) == 0:
# Replace empty file name or file path part with the following
valid_filename = "(Empty Name)"
return valid_filename
def get_root_path():
# Replace with your own root file path, e.g. '/place/to/save/files/'
filepath = get_file_root_from_config()
filepath = os.path.abspath(filepath)
# ensure trailing path separator (/)
if not any(filepath[-1] == sep for sep in os_path_separators()):
filepath = '{0}{1}'.format(filepath, os.path.sep)
ensure_directory_exists(filepath)
return filepath
def path_split_into_list(path):
# Gets all parts of the path as a list, excluding path separators
parts = []
while True:
newpath, tail = os.path.split(path)
if newpath == path:
assert not tail
if path and path not in os_path_separators():
parts.append(path)
break
if tail and tail not in os_path_separators():
parts.append(tail)
path = newpath
parts.reverse()
return parts
def sanitise_filesystem_path(potential_file_path):
# Splits up a path and sanitises the name of each part separately
path_parts_list = path_split_into_list(potential_file_path)
sanitised_path = ''
for path_component in path_parts_list:
sanitised_path = '{0}{1}{2}'.format(sanitised_path, sanitise_filesystem_name(path_component), os.path.sep)
return sanitised_path
def check_if_path_is_under(parent_path, child_path):
# Using the function to split paths into lists of component parts, check that one path is underneath another
child_parts = path_split_into_list(child_path)
parent_parts = path_split_into_list(parent_path)
if len(parent_parts) > len(child_parts):
return False
return all(part1==part2 for part1, part2 in zip(child_parts, parent_parts))
def make_valid_file_path(path=None, filename=None):
root_path = get_root_path()
if path:
sanitised_path = sanitise_filesystem_path(path)
if filename:
sanitised_filename = sanitise_filesystem_name(filename)
complete_path = os.path.join(root_path, sanitised_path, sanitised_filename)
else:
complete_path = os.path.join(root_path, sanitised_path)
else:
if filename:
sanitised_filename = sanitise_filesystem_name(filename)
complete_path = os.path.join(root_path, sanitised_filename)
else:
complete_path = complete_path
complete_path = os.path.abspath(complete_path)
if check_if_path_is_under(root_path, complete_path):
return complete_path
else:
return None
これにより、ユーザーは次のようなファイル名を入力../../../../etc/shadow
できなくなりますが、以下のサブディレクトリ内のファイルも許可されませんbasedir
(つまりbasedir/subdir/moredir
、ブロックされます)。
from pathlib import Path
test_path = (Path(basedir) / user_input).resolve()
if test_path.parent != Path(basedir).resolve():
raise Exception(f"Filename {test_path} is not in {Path(basedir)} directory")
以下のサブディレクトリを許可する場合basedir
:
if not Path(basedir).resolve() in test_path.resolve().parents:
raise Exception(f"Filename {test_path} is not in {Path(basedir)} directory")