BASE_DIR = "/var/www/uploads"@app.get("/download")
defdownload(file: str):
# ❌ 검증 없이 그대로 합쳐서 오픈
path = os.path.join(BASE_DIR, file)
returnFileResponse(path)
BASE_DIR = Path("/var/www/uploads").resolve()
@app.get("/download")
defdownload(file: str):
# ✅ 실제 경로로 정규화 후 접두사 검사
path = (BASE_DIR / file).resolve()
if not path.is_relative_to(BASE_DIR):
raiseHTTPException(403, "Forbidden")
returnFileResponse(path)
📋 서버 요청 로그 실시간
// 서버 대기 중...
📊 요청 현황
정상 다운로드0
웹루트 탈출 성공0
403 차단0
⚡ 경로 탈출 공격 흐름
① file=notice.txt 요청
→
② 웹루트 안 파일, 정상
↓
③ file=../../../etc/passwd
→
④ BASE_DIR과 이어붙임
↓
⑤ ".." 가 상위 폴더 3번 이탈
→
⑥ 웹루트 밖 파일 그대로 반환 💀
핵심 원인: 파일시스템은 ..를 "부모 디렉터리"로 정상 해석합니다.
서버가 합쳐진 경로를 열기 전에 웹루트 안에 있는지 확인하지 않으면, 사용자가 입력하는 경로만으로 임의 파일을 읽게 됩니다.
🛡️ 방어 방법 3가지
① 정규화 + 접두사 검사 경로를 실제 절대경로로 정규화한 뒤, 결과가 BASE_DIR로 시작하는지 확인.
문자열 치환보다 우회 불가능한 근본적 방어.
② 화이트리스트 매칭 파일명을 자유 입력 대신 DB의 파일 ID나 정해진 목록에서만 선택하도록 설계.
애초에 경로 조합 자체를 없앰.
③ 샌드박스 / chroot 프로세스가 볼 수 있는 파일시스템 범위 자체를 OS 레벨에서 제한.
컨테이너(Docker)도 같은 원리로 호스트 파일시스템을 격리.
주의:"../"를 1회만 치환하는 필터는
"....//" 같은 입력을 넣으면 치환 후 "../"가 재조합되어 우회됩니다.
"부실한 필터" 모드로 직접 확인해 보세요.