템플릿이 나중에 반드시 필요해질 공용 기능을 미리 갖춘다.
multer 취약점을 해소한 김에, 안전한 업로드 기준도 함께 세운다.
- modules/upload — 단일 파일 업로드 엔드포인트(POST /api/files)와 재사용 가능한 검증 설정
- 확장자와 MIME 을 **둘 다** 검사하는 허용목록 방식.
확장자만 보면 위장이 쉽고, MIME 만 보면 클라이언트가 보낸 값이라 신뢰할 수 없다.
.html/.svg/.js 등 브라우저가 실행할 수 있는 형식은 허용 목록에서 제외.
- 저장 파일명은 서버가 UUID 로 생성한다. 클라이언트 파일명을 경로에 쓰면 경로 탈출이 가능하다.
- 크기 한도(UPLOAD_MAX_SIZE_MB, 기본 5MB) 초과 시 FILE_002(413).
- 도메인 에러 코드 FILE_001/FILE_002 추가 후 세 플랫폼 사전 동기화.
함께 고친 것:
- 전역 필터가 프레임워크의 영문 메시지를 그대로 노출하고 있었다.
크기 초과 시 multer 의 "File too large" 가 사용자에게 보였다 —
이제 코드에 대응하는 레지스트리 문구만 사용하며, 재정의는 BusinessException 으로만 가능하다.
- sync:error-codes 가 컨테이너 안에서 실행되면 형제 디렉터리를 찾지 못해
cryptic 한 mkdir 오류를 냈다. 원인과 해결 방법을 알리도록 보강.
- 업로드 볼륨 경로에 compose 기본값을 주어 기존 .env 에 키가 없어도 동작하게 함.
실제 multipart 요청으로 정상 업로드·형식 거부·MIME 위장·경로 탈출 시도·크기 초과·
파일 누락 시나리오를 모두 검증했다.
알려진 한계(확장자·MIME 검사는 파일 내용을 보지 않는다)는 security.md 에 명시하고,
업로드 디렉터리를 정적 서빙하지 말 것을 규칙으로 남겼다.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
useApi 의 baseURL 이 '/api' 상대경로인데 vite dev 서버에 프록시가 없어,
브라우저의 API 요청이 백엔드에 닿지 않고 SPA 폴백으로 index.html 을 받고 있었다.
더 나쁜 것은 조용히 실패한다는 점이다 — HTTP 200 에 Content-Type: text/html 이라
인터셉터의 success 판별을 통과하지 못하고 HTML 문자열이 그대로 반환된다.
템플릿의 화면이 아직 API 를 호출하지 않아 드러나지 않았을 뿐이다.
- vite.config.ts 에 /api 프록시 추가. 대상은 VITE_DEV_API_PROXY 로 조정 가능하며
docker 개발 환경에서는 compose 가 http://backend:3000 을 주입한다.
- DB_HOST 기본값을 'db' 에서 'localhost' 로 변경.
compose 는 항상 DB_HOST=db 를 주입하므로 이 기본값은 호스트에서 직접 실행할 때만
쓰인다 — 'db' 는 호스트에서 해석되지 않아 로컬 실행이 불가능했다.
- README 의 "Docker 없이 로컬 개발" 절차가 위 두 문제로 동작하지 않았다. 실제로 되는 절차로 교체.
프록시 경유 요청이 JSON 을 반환하고 SPA 라우팅도 유지되는 것을 확인했다.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
security.md 가 금지한다고 적어둔 것을 실제 설정이 어기고 있었다.
- 프론트엔드 운영 컨테이너가 root 로 nginx 를 실행하고 있었다.
nginx-unprivileged 로 전환하고 포트를 8080 으로 맞춤(비-root 는 1024 미만 바인드 불가).
- dev 오버라이드가 사용하는 빌드 스테이지에도 USER node 적용.
익명 볼륨이 node 소유로 생성되도록 chown 을 USER 전환 앞에 둔다
(그렇지 않으면 vite 가 node_modules/.vite 캐시를 쓰지 못한다).
- dev 의 DB/Redis 포트가 0.0.0.0 에 바인딩되어 같은 LAN 에 노출돼 있었다 → 127.0.0.1 로 한정.
- CSP 헤더 추가. 대신 현대 브라우저에서 권장되지 않는 X-XSS-Protection 은 제거.
- CORS 차단 시 Error 를 던져 500(SYS_001)이 되던 것을 헤더 미부여 방식으로 변경.
- 헬스체크가 localhost 를 ::1 로 해석해 항상 실패하던 문제 수정(127.0.0.1 명시).
- graceful shutdown 을 위해 init(tini)과 stop_grace_period 적용.
- *.example 의 실제같은 비밀번호를 placeholder 로 교체하고,
비어 있던 서비스 env 예시에 안내를 채움.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>