경로 조작 (Path Traversal)

사용자 입력으로 의도한 기준 디렉터리 밖의 파일에 접근하지 못하도록 제한하세요.

설명

경로 조작(Path Traversal)은 사용자 입력을 통해 파일 시스템 경로를 조작하여, 원래 의도된 파일이나 디렉토리 외부의 파일에 접근할 수 있게 하는 공격입니다. 공격자는 상대 경로(../ 등)를 사용하여 시스템의 민감한 파일에 접근할 수 있습니다.

잠재적 영향

  • 정보 유출: 공격자가 시스템의 민감한 파일을 읽어 중요한 정보를 획득할 수 있습니다.
  • 데이터 손상: 공격자가 시스템 파일을 덮어쓰거나 삭제하여 데이터 손상을 일으킬 수 있습니다.
  • 권한 상승: 공격자가 권한을 상승시키기 위해 중요한 파일을 수정할 수 있습니다.

해결 방법

  • 입력값 검증: 파일명과 경로를 검증하세요. ../ 문자열만 차단하는 데 의존하지 말고 실제 접근 경로가 허용한 디렉터리 안에 있는지 확인하세요.
  • 허용 목록: 허용된 디렉토리 내의 파일에만 접근할 수 있도록 제한합니다.
  • 보안 라이브러리 사용: Flask/Werkzeug에서는 secure_filename(), safe_join(), send_from_directory()처럼 경로 조작 방어를 고려한 API를 우선 사용합니다.
  • 파일명만 필요한 경우 os.path.basename() 또는 pathlib.PurePath(...).name으로 최종 파일명 구성요소만 추출하고, 반드시 신뢰된 기준 디렉토리와 함께 사용합니다.

예시

open

변경 전

python
# Unsafe Flask code
from flask import Flask, request

app = Flask(__name__)

@app.route('/read')
def unsafe_read():
    filename = request.args.get('filename')
    with open(filename, 'r') as file:
        content = file.read()
    return content

변경 후

python
# Safe Flask code
from flask import Flask, request, abort
from pathlib import Path
from werkzeug.security import safe_join
from werkzeug.utils import secure_filename

app = Flask(__name__)
# 공격자가 수정할 수 없는 서버 관리 디렉터리
BASE_DIR = Path("/safe/directory").resolve(strict=True)

@app.route('/read')
def safe_read():
    filename = secure_filename(request.args.get('filename', ''))
    if not filename:
        return abort(400, description="Invalid file path")

    candidate = safe_join(str(BASE_DIR), filename)
    if candidate is None:
        return abort(400, description="Invalid file path")

    try:
        fullpath = Path(candidate).resolve(strict=True)
        fullpath.relative_to(BASE_DIR)
    except FileNotFoundError:
        return abort(404, description="File not found")
    except ValueError:
        return abort(400, description="Invalid file path")

    if not fullpath.is_file():
        return abort(404, description="File not found")
    with open(fullpath, 'r') as file:
        content = file.read()
    return content

설명:

  • 변경 전: 사용자 입력값을 검증하지 않고 파일 경로로 사용하여, 공격자가 상대 경로(../)를 통해 민감한 파일에 접근할 수 있습니다.
  • 변경 후: 빈 이름을 거부하고 secure_filename()과 safe_join()으로 경로 구성요소를 제한합니다. 이어서 Path.resolve(strict=True)로 심볼릭 링크를 해석한 실제 파일이 공격자가 수정할 수 없는 기준 디렉터리 내부인지 확인합니다.

파일명 구성요소만 사용하는 예시

python
import os
from pathlib import PurePath

def filename_only(raw_name):
    if not isinstance(raw_name, str):
        raise TypeError("filename must be a string")
    # 둘 중 하나의 방식으로 디렉토리 구성요소를 제거
    name = os.path.basename(raw_name)
    # name = PurePath(raw_name).name
    if name in {"", ".", ".."}:
        raise ValueError("invalid filename")
    return name

basename 또는 PurePath.name은 경로의 마지막 구성요소만 뽑는 용도입니다. 파일을 열거나 전송할 때는 이 값도 신뢰된 기준 디렉토리와 결합해야 합니다.

Flask send_file

변경 전

python
# Unsafe Flask send_file
from flask import Flask, request, send_file

app = Flask(__name__)

@app.route('/read')
def unsafe_read():
    filename = request.args.get('filename')
    return send_file(filename)

변경 후

python
# Safe Flask send_from_directory
from flask import Flask, request, send_from_directory
from pathlib import Path
from werkzeug.utils import secure_filename

app = Flask(__name__)
BASE_DIR = Path("/safe/directory").resolve(strict=True)

@app.route('/read')
def safe_read():
    filename = secure_filename(request.args.get('filename', ''))
    if not filename:
        return "Invalid file path", 400
    return send_from_directory(BASE_DIR, filename, as_attachment=True)

설명:

  • 변경 전: 사용자 입력값을 검증하지 않고 파일 경로로 사용하여, 공격자가 상대 경로(../)를 통해 민감한 파일에 접근할 수 있습니다.
  • 변경 후: send_from_directory로 지정된 경로를 벗어나지 못하게 하고, 빈 이름을 거부한 뒤 secure_filename()으로 파일명을 제한합니다. 기준 디렉터리는 공격자가 파일이나 심볼릭 링크를 추가할 수 없도록 서버가 관리해야 합니다.

참조