fix(security): CRITICAL 보안 이슈 13건 일괄 수정

- SEC-42: JWT algorithm HS256 명시 (sign 5곳, verify 3곳)
- SEC-44: MariaDB/PhpMyAdmin 포트 127.0.0.1 바인딩
- SEC-29: escHtml = escapeHtml alias 추가 (XSS 방지)
- SEC-39: Python Dockerfile 4개 non-root user + chown
- SEC-43: deploy-remote.sh 삭제 (평문 비밀번호 포함)
- SEC-11,12: SQL SET ? → 명시적 컬럼 whitelist + IN절 parameterized
- QA-34: vacation approveRequest/cancelRequest 트랜잭션 래핑
- SEC-32,34: material_comparison.py 5개 엔드포인트 인증 + confirmed_by
- SEC-33: files.py 17개 미인증 엔드포인트 인증 추가
- SEC-37: chatbot 프롬프트 인젝션 방어 (sanitize + XML 구분자)
- SEC-38: fastapi-bridge 프록시 JWT 검증 + 캐시 키 user_id 포함
- SEC-58/QA-98: monthly-comparison API_BASE_URL 수정 + 401 처리
- SEC-61: monthlyComparisonModel SELECT FOR UPDATE 추가
- SEC-63: proxyInputController 에러 메시지 노출 제거
- QA-103: pageAccessRoutes error→message 통일
- SEC-62: tbm-create onclick 인젝션 → data-attribute event delegation
- QA-99: tbm-mobile/create 캐시 버스팅 갱신
- QA-100,101: ESC 키 리스너 cleanup 추가

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Hyungi Ahn
2026-04-01 10:48:58 +09:00
parent 766cb90e8f
commit f09c86ee01
24 changed files with 215 additions and 305 deletions

View File

@@ -164,11 +164,11 @@ ALLOWED_EXTENSIONS = {".xlsx", ".xls", ".csv"}
# API 정보는 /info 엔드포인트로 이동됨
@router.get("/test")
async def test_endpoint():
async def test_endpoint(current_user: dict = Depends(get_current_user)):
return {"status": "파일 API가 정상 작동합니다!"}
@router.post("/add-missing-columns")
async def add_missing_columns(db: Session = Depends(get_db)):
async def add_missing_columns(db: Session = Depends(get_db), current_user: dict = Depends(get_current_user)):
"""누락된 컬럼들 추가"""
try:
db.execute(text("ALTER TABLE files ADD COLUMN IF NOT EXISTS parsed_count INTEGER DEFAULT 0"))
@@ -602,7 +602,8 @@ async def upload_file(
@router.get("/")
async def get_files(
job_no: Optional[str] = None,
db: Session = Depends(get_db)
db: Session = Depends(get_db),
current_user: dict = Depends(get_current_user)
):
"""파일 목록 조회"""
try:
@@ -646,7 +647,8 @@ async def get_files(
@router.get("/list")
async def get_files_list(
job_no: Optional[str] = None,
db: Session = Depends(get_db)
db: Session = Depends(get_db),
current_user: dict = Depends(get_current_user)
):
"""파일 목록 조회 (리비전 모드 확인용)"""
try:
@@ -696,7 +698,8 @@ async def get_files_list(
@router.get("/project/{project_code}")
async def get_files_by_project(
project_code: str,
db: Session = Depends(get_db)
db: Session = Depends(get_db),
current_user: dict = Depends(get_current_user)
):
"""프로젝트별 파일 목록 조회"""
try:
@@ -733,7 +736,7 @@ async def get_files_by_project(
raise HTTPException(status_code=500, detail=f"프로젝트 파일 조회 실패: {str(e)}")
@router.get("/stats")
async def get_files_stats(db: Session = Depends(get_db)):
async def get_files_stats(db: Session = Depends(get_db), current_user: dict = Depends(get_current_user)):
"""파일 및 자재 통계 조회"""
try:
# 총 파일 수
@@ -774,7 +777,7 @@ async def get_files_stats(db: Session = Depends(get_db)):
raise HTTPException(status_code=500, detail=f"통계 조회 실패: {str(e)}")
@router.delete("/delete/{file_id}")
async def delete_file(file_id: int, db: Session = Depends(get_db)):
async def delete_file(file_id: int, db: Session = Depends(get_db), current_user: dict = Depends(get_current_user)):
"""파일 삭제"""
try:
# 자재 먼저 삭제
@@ -814,7 +817,8 @@ async def get_materials(
sort_by: Optional[str] = None,
exclude_requested: bool = True, # 구매신청된 자재 제외 여부
group_by_spec: bool = False, # 같은 사양끼리 그룹화
db: Session = Depends(get_db)
db: Session = Depends(get_db),
current_user: dict = Depends(get_current_user)
):
"""
저장된 자재 목록 조회 (job_no, filename, revision 3가지로 필터링 가능) - 신버전
@@ -1511,7 +1515,8 @@ async def get_materials(
async def get_materials_summary(
project_id: Optional[int] = None,
file_id: Optional[int] = None,
db: Session = Depends(get_db)
db: Session = Depends(get_db),
current_user: dict = Depends(get_current_user)
):
"""자재 요약 통계"""
try:
@@ -1566,7 +1571,8 @@ async def compare_revisions(
filename: str,
old_revision: str,
new_revision: str,
db: Session = Depends(get_db)
db: Session = Depends(get_db),
current_user: dict = Depends(get_current_user)
):
"""
리비전 간 자재 비교
@@ -1846,7 +1852,8 @@ async def compare_revisions(
async def get_pipe_details(
file_id: Optional[int] = None,
job_no: Optional[str] = None,
db: Session = Depends(get_db)
db: Session = Depends(get_db),
current_user: dict = Depends(get_current_user)
):
"""
PIPE 상세 정보 조회
@@ -1905,7 +1912,8 @@ async def get_pipe_details(
async def get_fitting_details(
file_id: Optional[int] = None,
job_no: Optional[str] = None,
db: Session = Depends(get_db)
db: Session = Depends(get_db),
current_user: dict = Depends(get_current_user)
):
"""
FITTING 상세 정보 조회
@@ -1960,7 +1968,8 @@ async def get_fitting_details(
async def get_valve_details(
file_id: Optional[int] = None,
job_no: Optional[str] = None,
db: Session = Depends(get_db)
db: Session = Depends(get_db),
current_user: dict = Depends(get_current_user)
):
"""
VALVE 상세 정보 조회
@@ -2016,7 +2025,8 @@ async def get_user_requirements(
file_id: int,
job_no: Optional[str] = None,
status: Optional[str] = None,
db: Session = Depends(get_db)
db: Session = Depends(get_db),
current_user: dict = Depends(get_current_user)
):
"""
사용자 요구사항 조회
@@ -2090,7 +2100,8 @@ class UserRequirementCreate(BaseModel):
@router.post("/user-requirements")
async def create_user_requirement(
requirement: UserRequirementCreate,
db: Session = Depends(get_db)
db: Session = Depends(get_db),
current_user: dict = Depends(get_current_user)
):
"""
사용자 요구사항 생성
@@ -2137,7 +2148,8 @@ async def create_user_requirement(
async def delete_user_requirements(
file_id: Optional[int] = None,
material_id: Optional[int] = None,
db: Session = Depends(get_db)
db: Session = Depends(get_db),
current_user: dict = Depends(get_current_user)
):
"""
사용자 요구사항 삭제 (파일별 또는 자재별)
@@ -2622,7 +2634,8 @@ async def process_missing_drawings(
file_id: int,
action: str = "delete",
drawings: List[str] = [],
db: Session = Depends(get_db)
db: Session = Depends(get_db),
current_user: dict = Depends(get_current_user)
):
"""
누락된 도면 처리