설명
경로 조작(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()으로 파일명을 제한합니다. 기준 디렉터리는 공격자가 파일이나 심볼릭 링크를 추가할 수 없도록 서버가 관리해야 합니다.