[BACK-010][실무] 파일을 안전하게 받고 내려주기 > IT 기술 공유

본문 바로가기
사이트 내 전체검색

IT 기술 공유

[BACK-010][실무] 파일을 안전하게 받고 내려주기

페이지 정보

profile_image
작성자 기술팀장
댓글 0건 조회 124회 작성일 26-09-04 20:33

본문

[이번 수업]
서버가 파일을 받고 내려줄 때 필요한 안전 경계를 설계합니다. 파일명, 확장자, 브라우저가 보낸 형식 정보를 그대로 믿지 않고 저장·조회·응답 단계를 나눠 확인합니다.

[선수지식]
HTTP 요청·응답, 인증과 인가, 경로를 알아야 합니다. 인가는 사용자가 특정 파일을 다룰 권한이 있는지 확인하는 절차입니다.

[학습목표]
1. 업로드 파일의 이름·크기·형식을 검증합니다.
2. 사용자 파일명과 서버 저장명을 분리합니다.
3. 다운로드마다 소유권과 헤더를 확인합니다.

[핵심개념]
업로드는 신뢰 경계를 넘는 입력입니다. 원래 파일명과 Content-Type은 참고값일 뿐입니다. 허용 확장자를 목록으로 정하고 실제 내용도 확인합니다. 크기, 파일 수, 처리 시간을 제한하고 필요하면 격리된 악성 파일 검사를 거친 뒤 공개합니다.

원래 파일명을 저장 경로로 쓰면 경로 조작과 이름 충돌이 생길 수 있습니다. 서버가 예측하기 어려운 새 이름을 만들고 원래 이름은 표시용 메타데이터로만 둡니다. 저장소는 웹 공개 폴더 밖에 두고 실행 권한을 주지 않습니다. 소유자, 저장 키, 크기, 형식, 검사 상태도 기록합니다.

다운로드는 파일 ID를 경로에 바로 붙이지 않습니다. 현재 사용자가 해당 ID를 읽을 권한이 있는지 확인하고 저장 키를 조회합니다. 검증된 Content-Type과 `Content-Disposition: attachment`를 사용합니다. 표시용 이름은 제어 문자를 제거하고 RFC 6266 인코딩 규칙을 따릅니다. `X-Content-Type-Options: nosniff`는 브라우저의 형식 추측을 제한합니다.

업로드는 인증→권한→요청 크기 제한→확장자·내용 검사→새 이름→격리 저장 순서로 처리합니다. 다운로드는 인증→파일별 인가→키 조회→정확한 헤더→스트리밍 순서입니다. 오류에는 내부 경로를 노출하지 않습니다.

[따라하기]
UTF-8 텍스트만 최대 1KiB까지 저장하는 upload_demo.py입니다.

```python
from pathlib import Path
import secrets

UPLOAD_DIR = Path("safe_uploads").resolve()
MAX_BYTES = 1024

def save_text(original_name, data):
    if Path(original_name).suffix.lower() != ".txt":
        raise ValueError("허용하지 않는 확장자")
    if len(data) > MAX_BYTES:
        raise ValueError("파일이 너무 큼")
    data.decode("utf-8")

    UPLOAD_DIR.mkdir(mode=0o700, exist_ok=True)
    target = (UPLOAD_DIR / f"{secrets.token_hex(8)}.txt").resolve()
    if target.parent != UPLOAD_DIR:
        raise ValueError("잘못된 저장 경로")
    target.write_bytes(data)
    return target.name

stored = save_text("memo.txt", "안전한 연습 파일".encode())
print(f"stored={stored}")
```

macOS/Linux는 `python3 upload_demo.py`, Windows PowerShell은 `py upload_demo.py`로 실행합니다. 예상 결과는 `stored=무작위16진수.txt`이며 이름은 매번 달라집니다. safe_uploads에 내용이 생기지만 `memo.txt`는 저장 경로에 쓰이지 않습니다. 권한 적용은 운영체제마다 다르므로 배포 환경에서 별도로 확인합니다.

예제는 데이터를 메모리에 올립니다. 실제 서버는 요청 제한을 먼저 적용하고 큰 파일은 제한을 확인하며 스트리밍합니다. 이미지·문서는 확장자만으로 실제 형식을 판단하지 않습니다.

[흔한 실수]
확장자나 Content-Type 하나만 믿거나 클라이언트 검사로 서버 검증을 대신하기 쉽습니다. 오류에 절대 경로를 표시하거나 업로드 폴더에서 파일 실행을 허용하지 않습니다. 다운로드마다 파일별 인가를 확인합니다.

[보안 주의]
실습에는 직접 만든 작은 텍스트만 사용합니다. 실제 서비스는 허용 목록, 크기 제한, 격리, 검사, 최소 권한을 함께 적용합니다. 로그에는 내용·토큰·개인정보 대신 내부 ID와 결과만 남깁니다. 본인 소유의 로컬 또는 허가된 격리 환경에서만 시험합니다.

[직접 해볼 과제]
MAX_BYTES를 8로 바꿔 문자열이 거부되는지 확인하세요. `photo.jpg`는 거부되고 `memo.txt`는 저장되며 서버 이름이 원래 이름과 다른지도 검사합니다.

[확인문제]
1. 파일명과 Content-Type을 그대로 믿으면 안 되는 이유는 무엇인가요?
2. 서버 저장명을 무작위로 만들고 웹 공개 폴더 밖에 두는 이유는 무엇인가요?
3. 다운로드할 때 파일 ID 외에 어떤 권한 검사가 필요한가요?

[다음 학습]
다음 과정 DB-010에서는 스키마 마이그레이션을 배웁니다. BACK-011에서는 WebSocket을 다룹니다.

[공식 참고 자료]
- OWASP 파일 업로드 지침: https://cheatsheetseries.owasp.org/cheatsheets/File_Upload_Cheat_Sheet.html
- CWE-22 경로 조작 분류: https://cwe.mitre.org/data/definitions/22.html
- RFC 6266 Content-Disposition: https://www.rfc-editor.org/rfc/rfc6266.html
- MIME Sniffing 표준: https://mimesniff.spec.whatwg.org/
- Python secrets 공식 문서: https://docs.python.org/3/library/secrets.html

댓글목록

등록된 댓글이 없습니다.

회원로그인

회원가입

사이트 정보

회사명 : 회사명 / 대표 : 대표자명
주소 : OO도 OO시 OO구 OO동 123-45
사업자 등록번호 : 123-45-67890
전화 : 02-123-4567 팩스 : 02-123-4568
통신판매업신고번호 : 제 OO구 - 123호
개인정보관리책임자 : 정보책임자명

접속자집계

오늘
548
어제
5,103
최대
16,772
전체
771,602
Copyright © 소유하신 도메인. All rights reserved.