Flask 파일 업로드 — 안전하게 파일 받기

Flask 파일 업로드 — 안전하게 파일 받기

파일 업로드는 오래된 난제처럼 느껴지지만 기본 아이디어는 사실 간단해요. 폼을 multipart/form-data로 표시하고 파일 입력을 두고, 앱은 요청의 files 딕셔너리에서 파일을 꺼내 안전한 방법으로 저장하면 돼요. 다만 보안을 위해 확장자를 제한하는 등의 신경이 필요해요.

출처: Uploading Files — Flask Documentation

파일 업로드의 기본 흐름은 이래요.

  1. <form> 태그를 enctype=multipart/form-data로 표시하고 그 안에 <input type=file>을 둔다.
  2. 앱이 요청 객체의 files 딕셔너리에서 파일에 접근한다.
  3. 파일의 save() 메서드로 파일 시스템 어딘가에 영구 저장한다.

기본 예제

업로드 폴더에 파일을 올리고 사용자에게 파일을 표시하는 기본 앱을 만들어 볼게요. 부트스트랩 코드부터 볼게요.

import os
from flask import Flask, flash, request, redirect, url_for
from werkzeug.utils import secure_filename

UPLOAD_FOLDER = '/path/to/the/uploads'
ALLOWED_EXTENSIONS = {'txt', 'pdf', 'png', 'jpg', 'jpeg', 'gif'}

app = Flask(__name__)
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER

먼저 몇 가지 import 가 필요해요. 대부분은 직관적인데, werkzeug.secure_filename()은 잠시 후 설명할게요. UPLOAD_FOLDER는 업로드된 파일을 저장할 위치이고, ALLOWED_EXTENSIONS는 허용할 파일 확장자 집합이에요.

왜 허용 확장자를 제한할까요? 서버가 직접 데이터를 클라이언트에게 보내는 상황이라면, 사용자가 뭐든 업로드하게 두고 싶지 않기 때문이에요. 이렇게 하면 XSS 문제를 일으킬 HTML 파일을 업로드하지 못하게 막을 수 있어요. 그리고 서버가 executes 하면 .php 파일도 막아야 해요.

다음은 확장자가 유효한지 확인하고, 파일을 업로드하고, 업로드된 파일의 URL로 리다이렉트하는 함수들이에요.

def allowed_file(filename):
    return '.' in filename and \
           filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS

@app.route('/', methods=['GET', 'POST'])
def upload_file():
    if request.method == 'POST':
        # check if the post request has the file part
        if 'file' not in request.files:
            flash('No file part')
            return redirect(request.url)
        file = request.files['file']
        # If the user does not select a file, the browser submits an
        # empty file without a filename.
        if file.filename == '':
            flash('No selected file')
            return redirect(request.url)
        if file and allowed_file(file.filename):
            filename = secure_filename(file.filename)
            file.save(os.path.join(app.config['UPLOAD_FOLDER'], filename))
            return redirect(url_for('download_file', name=filename))
    return '''
    <!doctype html>
    <title>Upload new File</title>
    <h1>Upload new File</h1>
    <form method=post enctype=multipart/form-data>
      <input type=file name=file>
      <input type=submit value=Upload>
    </form>
    '''

여기서 secure_filename()은 파일 이름을 안전한 형태로 만들어 줘요. 업로드된 파일 이름에 경로 구분자나 위험한 문자가 섞여 서버 파일 시스템을 벗어나려는 시도를 막는 역할을 해요. 저장 전에 항상 이 함수로 파일 이름을 걸러 주는 것이 안전한 업로드의 핵심이에요.

연결 리셋 문제

로컬 개발 서버를 쓸 때는 413 응답 대신 연결 리셋 에러가 날 수 있어요. 프로덕션 WSGI 서버로 실행하면 올바른 상태 응답을 받을 수 있어요.

더 쉬운 해결책

더 빠르고 안정적인 해결책도 있어요. jQuery 같은 자바스크립트 라이브러리에 진행 바(progress bar) 구성을 돕는 폼 플러그인이 있어요. 그리고 파일 업로드의 공통 패턴은 거의 모든 앱에서 비슷하기 때문에, 어떤 확장자를 허용할지 제어하는 완전한 업로드 메커니즘을 구현한 Flask 확장도 있어요.

정리하면, 파일 업로드는 enctype=multipart/form-data 폼 + request.files 접근 + secure_filename()으로 안전하게 저장하는 세 단계가 전부예요. 허용 확장자를 만들어 검증하고, 파일 이름은 secure_filename()으로 정화한 뒤 저장하는 흐름을 기억하면 돼요.

더 알아보기