Inflearn brand logo image

인프런 커뮤니티 질문&답변

moljin님의 프로필 이미지
moljin

작성한 질문수

FastAPI: Python으로 3배 빠르게, 2배 정확한, 10배 빠른 API 만들기

15강 수정 요청: DOWNLOAD_DIR 확인 구문 부분

해결된 질문

작성

·

40

·

수정됨

0

아래의 엔드포인트 두개의 함수 부분에서

async def download_basic(file_name: str):

async def download_custom(file_name: str):

if not file_path.startswith(os.path.abspath(DOWNLOAD_DIR)): 이 부부의 file_path를 full_file_path를 아래처럼 할당한 후에

if not full_file_path.startswith(os.path.abspath(DOWNLOAD_DIR)): 으로 수정해야 할 듯 합니다.

@app.get("/download/basic/{file_name}")
async def download_basic(file_name: str):
    safe_base_filename = os.path.basename(file_name)
    file_path = os.path.join(DOWNLOAD_DIR, safe_base_filename)
    full_file_path = os.path.abspath(file_path)
    .
    '
    '
    if not full_file_path.startswith(os.path.abspath(DOWNLOAD_DIR)):
    # if not file_path.startswith(os.path.abspath(DOWNLOAD_DIR)): 
@app.get("/download/custom/{file_name}")
async def download_custom(file_name: str):
    safe_base_filename = os.path.basename(file_name)
    file_path = os.path.join(DOWNLOAD_DIR, safe_base_filename)
    full_file_path = os.path.abspath(file_path)
    .
    '
    '
    if not full_file_path.startswith(os.path.abspath(DOWNLOAD_DIR)):
    # if not file_path.startswith(os.path.abspath(DOWNLOAD_DIR)):

그렇지 않으면 검증이 되지 않습니다. 아래 참고

file_path: ./downloadables/스크린샷_3.png

full_file_path: D:\Python_FastAPI\Inflearn_Master\File_API\downloadables\스크린샷_3.png

os.path.abspath(DOWNLOAD_DIR): D:\Python_FastAPI\Inflearn_Master\File_API\downloadables

답변 2

0

해여님의 프로필 이미지
해여
지식공유자

안녕하세요, 강사 해여입니다.

먼저 피드백 감사드립니다. 말씀해주신 대로 상대경로(file_path)를 절대경로(os.path.abspath(DOWNLOAD_DIR))와 startswith로 비교한 기존 코드는 의도한 검증이 제대로 동작하지 않습니다.

제가 저렇게 쓴 이유는 예제의 흐름을 단순하게 보여주려다가 “경로를 안전한 루트 안에 가두자”는 취지만 남기고, 절대경로로 정규화하는 단계를 빠뜨렸기 때문입니다. 😿

덕분에 문제를 명확히 짚을 수 있었어요. 감사합니다.


말씀 주신 대로 절대경로로 변환 후 비교하면 의미 있는 검증이 됩니다.

DOWNLOAD_DIR_ABS = os.path.abspath(DOWNLOAD_DIR)

safe_base_filename = os.path.basename(file_name)
file_path = os.path.join(DOWNLOAD_DIR, safe_base_filename)
full_file_path = os.path.abspath(file_path)

# 기존: if not file_path.startswith(DOWNLOAD_DIR_ABS):  # 상대 vs 절대 비교라 문제
if not full_file_path.startswith(DOWNLOAD_DIR_ABS):
    raise HTTPException(status_code=403, detail="Access denied")

조금 더 고급 주제로 다뤄보자면, startswith.../downloadables2 같은 접두어 충돌이나 OS별 경로 차이, 심볼릭 링크에 취약합니다. 그래서 아래처럼 “좀 더 안전한” 방식으로 바꾸는 걸 권합니다.


pathlibresolve()relative_to()를 쓰면 심볼릭 링크, .. 포함 경로까지 정규화해 확실히 가둘 수 있어요.

from pathlib import Path
import os, mimetypes
from fastapi import HTTPException
from fastapi.responses import FileResponse

BASE = Path(DOWNLOAD_DIR).resolve()

def resolve_under_base(user_name: str) -> Path:
    safe = os.path.basename(user_name)          # 경로 성분 제거
    path = (BASE / safe).resolve()              # 심볼릭 링크/.. 정규화
    try:
        path.relative_to(BASE)                  # BASE 밖이면 ValueError
    except ValueError:
        raise HTTPException(status_code=403, detail="Access denied")
    if not path.is_file():
        raise HTTPException(status_code=404, detail="File not found")
    return path

@app.get("/download/basic/{file_name}")
async def download_basic(file_name: str):
    requested = resolve_under_base(file_name)
    return FileResponse(str(requested))

@app.get("/download/custom/{file_name}")
async def download_custom(file_name: str):
    requested = resolve_under_base(file_name)
    media_type, _ = mimetypes.guess_type(str(requested))
    media_type = media_type or "application/octet-stream"
    return FileResponse(
        path=str(requested),
        filename=f"downloaded_{requested.name}",
        media_type=media_type
    )

대안으로 os.path.commonpath([requested, base]) == base 비교도 안전한 편입니다. (realpath/abspath로 정규화 후 사용)


추가 팁!

  • 허용 목록(allowlist): 제공 가능한 파일명/확장자를 제한하세요.

  • 유니코드 정규화: unicodedata.normalize("NFC", name)로 이름 혼동을 줄입니다.

  • 로깅 최소화: 존재 여부를 과하게 로그로 노출하지 않기.

  • 테스트 케이스: .., %2e%2e/, 심볼릭 링크, 윈도우/리눅스 경로 차이를 포함해 단위테스트 추가.

다시 한 번 날카로운 피드백에 감사드립니다. 덕분에 자료를 더 탄탄하게 다듬을 수 있게 되었어요! 🙏

곧 해당 자료는 반영해두겠습니다 🙂

moljin님의 프로필 이미지
moljin
질문자

아하...

"pathlibresolve()relative_to()를 쓰면 심볼릭 링크, .. 포함 경로까지 정규화해 확실히 가둘 수 있어요." 이런게 있군요... 감사합니다.

0

안녕하세요, 인프런 AI 인턴입니다. moljin님.

질문해주신 내용은 다운로드 엔드포인트 함수에서 파일 경로의 검증 부분이 잘못 적용된 사례에 관한 것입니다. 안전한 파일 다운로드를 위해서, 요청된 파일의 경로가 지정된 다운로드 디렉토리 내부에 있는지 확인하는 과정에서 full_file_path를 사용하는 것이 추천됩니다. 이 방법은 경로 탐색 문제(Path Traversal)를 예방할 수 있습니다.

질문에서 언급하신 것처럼 os.path.abspath() 함수를 사용하여 full_file_path를 생성하고, 그 경로가 다운로드 디렉토리를 시작 부분으로 가지는지 확인하는 과정이 정확합니다. 이렇게 해야 사용자가 악의적으로 디렉토리 밖의 파일로 접근하는 것을 방지할 수 있습니다.

비슷한 질문이 Q&A 게시판에서도 자주 다뤄지곤 합니다. 이러한 경로 검증 문제는 보안상 중요하기 때문에 항상 신경 써야 합니다. 관련해서 참고할 수 있는 답변은 아래와 같습니다:

저는 질문자님의 이해를 돕고자 지식공유자님의 기존 답변을 참고하여 유사한 답변 링크를 추천해드리고 있습니다. 현재 베타 기능으로 답변이 만족스럽지 않을 수 있는 점 양해 부탁드립니다. 🙏 추가적으로 궁금한 점이 있으시면, 이어서 질문해 주세요. 곧 지식공유자께서 답변해 주실 것입니다.

moljin님의 프로필 이미지
moljin
질문자

오~~ AI 인턴 선생님...

제가 얘기하는 것과 딱 맞게 답을 주셨습니다.

moljin님의 프로필 이미지
moljin

작성한 질문수

질문하기