멀티파트 업로드와 콘텐츠 검증
UploadFile 입력을 받고 MIME 유형과 크기 제한을 검증하며 악성 데이터로부터 보호합니다.
멀티파트 업로드와 콘텐츠 검증은(는) CoddyKit의 무료 FastAPI Backend Development Bootcamp 강의입니다. 이것은 4개 중 1번째 강의입니다. 아래에서 전체 강의를 무료로 읽을 수 있으며, 내장 코드 에디터와 24/7 AI 튜터와 함께 브라우저에서 직접 실습할 수 있습니다. 이 강의는 FastAPI Backend Development Bootcamp 학습 경로의 일부이며, 진행 상황이 웹과 CoddyKit 앱에 동기화됩니다. FastAPI Backend Development Bootcamp 강의에는 총 4개의 강의가 포함되어 있습니다.
이 강의의 일부는 아직 번역되지 않았으며 영어로 표시됩니다.
Why Multipart Uploads Matter
Regular JSON request bodies cannot carry raw binary files efficiently. To upload an image, PDF, or video, browsers send a multipart/form-data request, which packs each field (text values and file bytes) into separate parts with their own headers.
FastAPI exposes incoming files through two helpers:
- UploadFile — a spooled file object that keeps small files in memory and large files on disk automatically.
- File() — a parameter marker that tells FastAPI to read this value from the multipart body.
In this lesson you will accept uploads, validate their MIME type and size, and reject malicious or oversized payloads before they touch your storage.
Your First UploadFile Endpoint
An UploadFile parameter gives you the original filename, the declared content_type, and async methods like read() and seek(). Always declare it with = File(...) so FastAPI parses it from the multipart body.
Note the handler is async because file I/O on UploadFile is awaitable.
from fastapi import FastAPI, UploadFile, File
app = FastAPI()
@app.post("/upload")
async def upload(file: UploadFile = File(...)):
contents = await file.read()
return {
"filename": file.filename,
"content_type": file.content_type,
"size_bytes": len(contents),
}Never Trust the Declared content_type
The content_type on an UploadFile comes straight from the client. An attacker can label a .exe as image/png. Use it as a cheap first filter, but never as your only check.
A robust pipeline does three things, in order:
- Reject obviously wrong declared types quickly (cheap).
- Enforce a hard size limit while streaming (prevents memory exhaustion).
- Inspect the real file bytes (magic numbers) to confirm the true type.
The next scenes build each layer.
Allowlisting MIME Types
Always use an allowlist, never a blocklist. List exactly the types you support and reject everything else. Return 415 Unsupported Media Type when the declared type is not allowed.
Keep the set small and explicit so new file formats are an intentional decision, not an accident.
from fastapi import FastAPI, UploadFile, File, HTTPException
app = FastAPI()
ALLOWED_TYPES = {"image/jpeg", "image/png", "application/pdf"}
@app.post("/documents")
async def create_document(file: UploadFile = File(...)):
if file.content_type not in ALLOWED_TYPES:
raise HTTPException(
status_code=415,
detail=f"Unsupported type: {file.content_type}",
)
return {"ok": True, "filename": file.filename}Enforcing a Size Limit by Streaming
Calling await file.read() loads the entire file into memory. A 2 GB upload could crash your worker. Instead, read in fixed-size chunks and abort the moment the running total exceeds your limit.
Return 413 Request Entity Too Large when the cap is breached. This keeps memory bounded no matter how big the client claims the file is.
from fastapi import FastAPI, UploadFile, File, HTTPException
app = FastAPI()
MAX_SIZE = 5 * 1024 * 1024 # 5 MB
CHUNK = 1024 * 1024 # 1 MB
@app.post("/upload")
async def upload(file: UploadFile = File(...)):
total = 0
while chunk := await file.read(CHUNK):
total += len(chunk)
if total > MAX_SIZE:
raise HTTPException(413, "File too large")
return {"filename": file.filename, "size": total}Streaming Straight to Disk Safely
Once size and type pass, stream the chunks to a destination file instead of holding them in memory. Combine the size check with the write loop so you stop early on oversized payloads and never persist a partial-but-huge file.
Use await file.seek(0) if you read the stream earlier and need to start over.
import aiofiles
from fastapi import UploadFile, File, HTTPException
MAX_SIZE = 5 * 1024 * 1024
async def save_upload(file: UploadFile, dest: str) -> int:
total = 0
async with aiofiles.open(dest, "wb") as out:
while chunk := await file.read(1024 * 1024):
total += len(chunk)
if total > MAX_SIZE:
raise HTTPException(413, "File too large")
await out.write(chunk)
return totalVerifying Real Content with Magic Numbers
The most reliable type check inspects the file's leading bytes, its magic number. A PNG always starts with \x89PNG\r\n\x1a\n; a JPEG with \xff\xd8\xff; a PDF with %PDF.
This pure-Python function maps a byte prefix to a real MIME type. You can run it on an online judge with no framework at all.
def sniff_mime(head: bytes) -> str | None:
signatures = {
b"\x89PNG\r\n\x1a\n": "image/png",
b"\xff\xd8\xff": "image/jpeg",
b"%PDF": "application/pdf",
}
for magic, mime in signatures.items():
if head.startswith(magic):
return mime
return None
if __name__ == "__main__":
print(sniff_mime(b"\x89PNG\r\n\x1a\nrest")) # image/png
print(sniff_mime(b"%PDF-1.7")) # application/pdf
print(sniff_mime(b"MZ\x90\x00")) # None (rejected)Cross-Checking Declared vs Real Type
Combine the layers: read just enough bytes to sniff the magic number, confirm it is in your allowlist, and verify it matches what the client declared. A mismatch (declared image/png but real bytes say PDF) is a strong signal of a malicious or buggy client, so reject it.
After sniffing, call await file.seek(0) so the full file can still be saved.
from fastapi import UploadFile, File, HTTPException
ALLOWED = {"image/png", "image/jpeg", "application/pdf"}
async def validate_type(file: UploadFile) -> str:
head = await file.read(8)
await file.seek(0)
real = sniff_mime(head)
if real not in ALLOWED:
raise HTTPException(415, "Content not allowed")
if file.content_type != real:
raise HTTPException(415, "Declared type mismatch")
return realSanitizing Filenames
Never use the client-supplied filename directly as a storage path. Names like ../../etc/passwd enable path-traversal, and odd characters break filesystems. Strip the directory part, keep a safe character set, and prefer a generated name plus a validated extension.
This helper is pure Python and judge-runnable.
import re
import uuid
from pathlib import PurePosixPath
EXT_FOR = {"image/png": ".png", "image/jpeg": ".jpg", "application/pdf": ".pdf"}
def safe_name(original: str, mime: str) -> str:
base = PurePosixPath(original).name # drop any path parts
base = re.sub(r"[^A-Za-z0-9._-]", "_", base) # keep safe chars
ext = EXT_FOR.get(mime, "")
return f"{uuid.uuid4().hex}{ext}"
if __name__ == "__main__":
print(safe_name("../../etc/passwd", "image/png").endswith(".png"))
print("/" not in safe_name("weird name!.jpg", "image/jpeg"))Handling Multiple Files at Once
To accept several files in one request, declare the parameter as a list[UploadFile]. The client sends the same form field name repeatedly. Validate each file independently and fail the whole request if any one is invalid, so partial uploads never leave inconsistent state.
from fastapi import FastAPI, UploadFile, File, HTTPException
app = FastAPI()
ALLOWED = {"image/png", "image/jpeg"}
@app.post("/gallery")
async def gallery(files: list[UploadFile] = File(...)):
if len(files) > 10:
raise HTTPException(400, "Too many files (max 10)")
for f in files:
if f.content_type not in ALLOWED:
raise HTTPException(415, f"{f.filename}: bad type")
return {"received": [f.filename for f in files]}Packaging Validation as a Dependency
Repeating type and size checks in every endpoint is error-prone. Wrap them in a reusable FastAPI dependency. The dependency runs the full pipeline and returns a clean, validated UploadFile, so your route stays focused on business logic.
This is the production-grade shape: allowlist, streamed size cap, magic-number sniff, and filename safety all in one place.
from fastapi import Depends, UploadFile, File, HTTPException
MAX_SIZE = 5 * 1024 * 1024
async def validated_upload(file: UploadFile = File(...)) -> UploadFile:
head = await file.read(8)
if sniff_mime(head) not in {"image/png", "image/jpeg", "application/pdf"}:
raise HTTPException(415, "Unsupported content")
total = len(head)
while chunk := await file.read(1024 * 1024):
total += len(chunk)
if total > MAX_SIZE:
raise HTTPException(413, "File too large")
await file.seek(0)
return file
@app.post("/secure-upload")
async def secure_upload(file: UploadFile = Depends(validated_upload)):
return {"filename": file.filename}Quick Check: Choosing the Right Guard
An endpoint accepts profile pictures. A user uploads a 3 GB file whose content_type header claims image/png, but the bytes are actually an executable. Which single combination of checks reliably protects the server?
Recap: A Layered Upload Defense
You built a complete, defensive upload pipeline for FastAPI:
- UploadFile + File() accept multipart data with streaming-friendly I/O.
- Allowlist the declared MIME type and return
415for anything unexpected, but never trust that header alone. - Stream in chunks and abort with
413once a size cap is exceeded, keeping memory bounded. - Sniff magic numbers to confirm the real content type and reject declared-vs-real mismatches.
- Sanitize filenames with generated names to stop path traversal.
- Package it as a dependency so every endpoint reuses the same guard.
Layered checks, ordered cheap-to-expensive, give you robust protection against oversized and malicious uploads.
자주 묻는 질문
“멀티파트 업로드와 콘텐츠 검증” 강의는 무료인가요?
네 — “멀티파트 업로드와 콘텐츠 검증” 전체 내용을 이 웹사이트에서 무료로 읽을 수 있습니다. 인터랙티브하게 실습하려면(내장 코드 에디터와 24/7 AI 튜터), CoddyKit PRO로 업그레이드하면 FastAPI Backend Development Bootcamp 강의 전체를 잠금 해제할 수 있습니다. FastAPI Backend Development Bootcamp 강의에는 총 4개의 강의가 포함되어 있습니다.
“멀티파트 업로드와 콘텐츠 검증”에서 뭘 배우나요?
UploadFile 입력을 받고 MIME 유형과 크기 제한을 검증하며 악성 데이터로부터 보호합니다. 브라우저에서 직접 실행하는 실습 코드로 FastAPI Backend Development Bootcamp을(를) 배우며, 24/7 AI 튜터가 강의를 진행하면서 질문에 답변해줍니다.
FastAPI Backend Development Bootcamp을(를) 시작하는 데 경험이 필요한가요?
사전 경험은 필요하지 않습니다. CoddyKit의 FastAPI Backend Development Bootcamp은(는) 초급자부터 고급 학습자까지를 위해 구성되어 있으므로, 여기서 시작하거나 처음부터 시작할 수 있으며 자신의 속도대로 진행할 수 있습니다. 이것은 4개 중 1번째 강의입니다.
“멀티파트 업로드와 콘텐츠 검증” 강의는 얼마나 걸리나요?
대부분의 CoddyKit 강의는 약 5~10분이 소요됩니다. 각 강의는 간결하고 인터랙티브하여 꾸준한 진행이 가능하며, 웹과 앱에서 중단한 부분부터 바로 시작할 수 있습니다.
이 FastAPI Backend Development Bootcamp 강의에서 코드를 작성하고 실행할 수 있나요?
네. 모든 FastAPI Backend Development Bootcamp 강의에는 내장 코드 에디터가 포함되어 있으므로, 브라우저에서 바로 실제 코드를 작성하고 실행한 후 즉시 AI 피드백을 받을 수 있습니다 — 로컬 설정이 필요 없습니다.
이 강의의 모든 강의
- 멀티파트 업로드와 콘텐츠 검증
- 스트리밍 응답과 범위 요청
- S3 호환 버킷으로 스토리지 오프로딩
- 비동기 이미지와 문서 변환