Files
TK-BOM-Project/backend/app/utils/file_validator.py
Hyungi Ahn 4f8e395f87
Some checks failed
SonarQube Analysis / SonarQube Scan (push) Has been cancelled
feat: SWG 가스켓 전체 구성 정보 표시 개선
- H/F/I/O SS304/GRAPHITE/CS/CS 패턴에서 4개 구성요소 모두 표시
- 기존 SS304 + GRAPHITE → SS304/GRAPHITE/CS/CS로 완전한 구성 표시
- 외부링/필러/내부링/추가구성 모든 정보 포함
- 구매수량 계산 모달에서 정확한 재질 정보 확인 가능
2025-08-30 14:23:01 +09:00

170 lines
6.0 KiB
Python

"""
파일 업로드 검증 유틸리티
보안 강화를 위한 파일 검증 로직
"""
import os
import magic
from pathlib import Path
from typing import List, Optional, Tuple
from fastapi import UploadFile, HTTPException
from ..config import get_settings
from .logger import get_logger
settings = get_settings()
logger = get_logger(__name__)
class FileValidator:
"""파일 업로드 검증 클래스"""
def __init__(self):
self.max_file_size = settings.security.max_file_size
self.allowed_extensions = settings.security.allowed_file_extensions
# MIME 타입 매핑
self.mime_type_mapping = {
'.xlsx': [
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'application/octet-stream' # 일부 브라우저에서 xlsx를 이렇게 인식
],
'.xls': [
'application/vnd.ms-excel',
'application/octet-stream'
],
'.csv': [
'text/csv',
'text/plain',
'application/csv'
]
}
def validate_file_extension(self, filename: str) -> bool:
"""파일 확장자 검증"""
file_ext = Path(filename).suffix.lower()
is_valid = file_ext in self.allowed_extensions
if not is_valid:
logger.warning(f"허용되지 않은 파일 확장자: {file_ext}, 파일: {filename}")
return is_valid
def validate_file_size(self, file_size: int) -> bool:
"""파일 크기 검증"""
is_valid = file_size <= self.max_file_size
if not is_valid:
logger.warning(f"파일 크기 초과: {file_size} bytes (최대: {self.max_file_size} bytes)")
return is_valid
def validate_filename(self, filename: str) -> bool:
"""파일명 검증 (보안 위험 문자 체크)"""
# 위험한 문자들
dangerous_chars = ['..', '/', '\\', ':', '*', '?', '"', '<', '>', '|']
for char in dangerous_chars:
if char in filename:
logger.warning(f"위험한 문자 포함된 파일명: {filename}")
return False
# 파일명 길이 체크 (255자 제한)
if len(filename) > 255:
logger.warning(f"파일명이 너무 긺: {len(filename)} 문자")
return False
return True
def validate_mime_type(self, file_content: bytes, filename: str) -> bool:
"""MIME 타입 검증 (파일 내용 기반)"""
try:
# python-magic을 사용한 MIME 타입 검증
detected_mime = magic.from_buffer(file_content, mime=True)
file_ext = Path(filename).suffix.lower()
expected_mimes = self.mime_type_mapping.get(file_ext, [])
if detected_mime in expected_mimes:
return True
logger.warning(f"MIME 타입 불일치 - 파일: {filename}, 감지된 타입: {detected_mime}, 예상 타입: {expected_mimes}")
return False
except Exception as e:
logger.error(f"MIME 타입 검증 실패: {e}")
# magic 라이브러리 오류 시 확장자 검증으로 대체
return self.validate_file_extension(filename)
def sanitize_filename(self, filename: str) -> str:
"""파일명 정화 (안전한 파일명으로 변환)"""
# 위험한 문자들을 언더스코어로 대체
dangerous_chars = ['..', '/', '\\', ':', '*', '?', '"', '<', '>', '|']
sanitized = filename
for char in dangerous_chars:
sanitized = sanitized.replace(char, '_')
# 연속된 언더스코어 제거
while '__' in sanitized:
sanitized = sanitized.replace('__', '_')
# 앞뒤 공백 및 점 제거
sanitized = sanitized.strip(' .')
return sanitized
async def validate_upload_file(self, file: UploadFile) -> Tuple[bool, Optional[str]]:
"""
업로드 파일 종합 검증
Returns:
Tuple[bool, Optional[str]]: (검증 성공 여부, 에러 메시지)
"""
try:
# 1. 파일명 검증
if not self.validate_filename(file.filename):
return False, f"유효하지 않은 파일명: {file.filename}"
# 2. 확장자 검증
if not self.validate_file_extension(file.filename):
return False, f"허용되지 않은 파일 형식입니다. 허용 형식: {', '.join(self.allowed_extensions)}"
# 3. 파일 내용 읽기
file_content = await file.read()
await file.seek(0) # 파일 포인터 리셋
# 4. 파일 크기 검증
if not self.validate_file_size(len(file_content)):
return False, f"파일 크기가 너무 큽니다. 최대 크기: {self.max_file_size // (1024*1024)}MB"
# 5. MIME 타입 검증
if not self.validate_mime_type(file_content, file.filename):
return False, "파일 형식이 올바르지 않습니다."
logger.info(f"파일 검증 성공: {file.filename} ({len(file_content)} bytes)")
return True, None
except Exception as e:
logger.error(f"파일 검증 중 오류 발생: {e}", exc_info=True)
return False, f"파일 검증 중 오류가 발생했습니다: {str(e)}"
# 전역 파일 검증기 인스턴스
file_validator = FileValidator()
async def validate_uploaded_file(file: UploadFile) -> None:
"""
파일 검증 헬퍼 함수 (HTTPException 발생)
Args:
file: 업로드된 파일
Raises:
HTTPException: 검증 실패 시
"""
is_valid, error_message = await file_validator.validate_upload_file(file)
if not is_valid:
raise HTTPException(status_code=400, detail=error_message)