os.path.join を使わずに文字列結合するとなぜ危険か

パスを文字列結合で作成するコードをよく見かけるが、これは複数の問題を引き起こす危険なアンチパターンだ。

文字列結合の問題点

# ❌ 危険なコード
directory = 'data'
filename = 'report.csv'

path = directory + '/' + filename  # data/report.csv

一見動いているように見えるが、以下の問題がある。

Windows で動かない

Windows のパス区切り文字は \ であり、/ を使うと動作しないケースがある。

# Windows では以下のようなパスが必要
path = 'C:\\Users\\name\\data\\report.csv'

# しかし文字列結合で / を使うと
path = 'C:/Users/name' + '/' + 'data'  # 混在して問題を起こすことがある

os.path.join() は実行環境に応じて適切な区切り文字を使う。

import os

path = os.path.join('C:\\Users\\name', 'data', 'report.csv')
# Windows: C:\Users\name\data\report.csv
# Unix: C:\Users\name/data/report.csv(そもそもこのパスは Unix では使わない)

末尾スラッシュの問題

ディレクトリ名の末尾にスラッシュがあるかどうかで結果が変わる。

# ❌ 末尾スラッシュがあると二重になる
directory = 'data/'
path = directory + '/' + 'file.txt'  # data//file.txt

os.path.join() はこれを自動で処理する。

import os

path = os.path.join('data/', 'file.txt')  # data/file.txt(正規化される)

絶対パスの上書き

os.path.join() は絶対パスを渡されると、それより前の引数を無視する。

import os

# 2番目の引数が絶対パスなので、1番目は無視される
path = os.path.join('/home/user', '/etc/passwd')
print(path)  # /etc/passwd

これは仕様だが、ユーザー入力を受け取る場合はセキュリティリスクになる。

# ❌ ユーザーが絶対パスを入力すると意図しない場所を参照できる
user_input = '/etc/passwd'
path = os.path.join('/var/www/uploads', user_input)  # /etc/passwd

対策として、入力値の検証が必要だ。

from pathlib import Path

def safe_join(base, user_path):
    base = Path(base).resolve()
    full = (base / user_path).resolve()
    
    if not str(full).startswith(str(base)):
        raise ValueError('Invalid path')
    return full

pathlib を使う

pathlib/ 演算子は直感的で安全だ。

from pathlib import Path

base = Path('data')
path = base / 'subdir' / 'file.txt'

print(path)  # data/subdir/file.txt(OS に応じた区切り文字)

まとめ

文字列結合

OS 依存、末尾スラッシュで壊れる、パスの正規化なし、セキュリティリスク

os.path.join / pathlib

OS 非依存、自動正規化、可読性が高い、安全

パスを扱う際は必ず os.path.join()pathlib を使うべきだ。