e7c00e6f65
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>
5.6 KiB
5.6 KiB
보안
비밀 정보
.env*등 민감정보 Git 커밋 금지 (.gitignore확인) — 예시는*.example(예:.env.development.example,.env.example) 로만 공유*.example파일의 값은 반드시 더미 placeholder 여야 한다. 실제로 쓰는(또는 쓸 법한) 비밀번호·키를 예시 파일에 적지 않는다.- ❌
DB_PASSWORD=dev_960426 - ✅
DB_PASSWORD=change-me-in-local-env
- ❌
- 시크릿이 한 번이라도 커밋되었다면 값을 교체(rotate) 한다. 커밋을 되돌리는 것만으로는 노출이 해소되지 않는다.
- 로그·에러 응답에 시크릿을 남기지 않는다 (logging).
인증 · 토큰
- JWT는 HttpOnly·Secure 쿠키 사용 (웹
localStorage금지) / 모바일은flutter_secure_storage사용 (키:jwt_token) - Access Token 만료는 짧게(15분~1시간), Refresh Token 은 HttpOnly 쿠키로만 전달
- 비밀번호는 단방향 해시(
bcrypt/argon2)로 저장한다. 평문·가역 암호화 금지 - 인증 실패 응답에서 원인을 구분해 알려주지 않는다 (아이디 없음/비밀번호 틀림 → 동일하게 AUTH_001)
입력 · 출력
- 입력값은 DTO(
class-validator) 검증을 거친 뒤 서비스 계층에 전달한다. 전역ValidationPipe의whitelist/forbidNonWhitelisted를 끄지 않는다 - TypeORM ORM 쿼리 사용으로 SQL Injection 차단 (Raw 쿼리 시 파라미터 바인딩 필수)
- 에러 응답에 스택·쿼리·내부 호스트명을 싣지 않는다 — 미정의 예외는 항상
SYS_001로 수렴시킨다 (error-codes) - 파일 업로드는 확장자·MIME·용량을 서버에서 검증하고, 업로드 경로를 실행 가능한 위치에 두지 않는다
네트워크 · 인프라
- 보안 헤더(
helmet) 적용 - CORS 는 화이트리스트 방식. 차단 시
callback(null, false)로 헤더를 부여하지 않아 브라우저가 막게 한다 (callback(new Error(...))를 쓰면 500(SYS_001)이 되어 원인 파악이 어려워진다) - Rate Limiting:
@nestjs/throttler하나로 일원화되어 있다 (app.module.ts의APP_GUARD). 초과 시 429 → 전역 필터가REQ_001로 매핑한다.- 기본 정책은 루트
.env의THROTTLE_TTL_MS/THROTTLE_LIMIT(미설정 시 60초/100회) - 라우트 단위 조정:
@Throttle({ default: { ttl, limit } })/ 제외:@SkipThrottle() - 로그인·비밀번호 재설정처럼 브루트포스 표적이 되는 엔드포인트는 더 엄격한 한도를 따로 건다
- ⚠️ rate limiter 를 이중으로 걸지 말 것 — 어느 쪽이 실제로 동작하는지 알 수 없게 되고 응답 헤더가 실제 한도와 어긋난다
- 기본 정책은 루트
- 컨테이너 root 실행 금지 — 개발 환경 포함. 운영뿐 아니라 dev 오버라이드가 사용하는 빌드 스테이지도 비-root 여야 한다.
- 프론트엔드 운영:
nginxinc/nginx-unprivileged+listen 8080(비-root 는 1024 미만 포트 바인드 불가) - Node 스테이지:
RUN chown node:node /app→USER node→COPY --chown=node:node(소유권을 먼저 넘겨야 익명 볼륨node_modules가 node 소유로 생성되어 캐시 쓰기가 가능하다)
- 프론트엔드 운영:
- DB·Redis 는
expose만 사용해 내부 네트워크에만 노출한다. 개발 편의로 호스트에 열어야 한다면 반드시127.0.0.1로 한정한다 —"${DB_PORT}:5432"는0.0.0.0바인딩이라 같은 공유기(사무실·카페)에 있는 누구나 개발 DB 에 접속할 수 있다.ports: - "127.0.0.1:${DB_PORT}:5432" # ✅ # - "${DB_PORT}:5432" # ❌ 외부 네트워크 노출 - 보안 헤더는 CSP 까지 설정한다 (
frontend/nginx.conf). 외부 CDN·폰트·분석 스크립트를 추가하면 해당 도메인을 CSP 에 명시한다.script-src에'unsafe-inline'/'unsafe-eval'을 넣으면 CSP 의 의미가 사라진다. API 도메인이 다르면connect-src에 추가해야 요청이 차단되지 않는다.
의존성
- 의존성 추가 시 유지보수 상태를 확인하고, 주기적으로
npm audit/flutter pub outdated로 취약점을 점검한다 npm audit fix는 반드시 컨테이너 안에서 실행한다. 호스트(Windows)에서 실행하면 lock 파일이 깨진다 (nestjs)docker exec <프로젝트>-backend npm audit fix--force는 함부로 쓰지 않는다. 메이저 버전을 올려 다른 패키지와의 호환이 깨질 수 있다. 실행 전npm audit fix --force --dry-run으로 어떤 패키지가 올라가는지 확인한다.
남아 있는 취약점 판단 기준
모든 취약점을 즉시 올릴 수 있는 것은 아니다. 다음을 근거로 판단하고 결정을 기록한다.
- 도달 가능한 코드 경로가 있는가 — 해당 기능을 쓰지 않으면 노출되지 않는다
- 수정이 다른 것을 깨뜨리는가 — 생태계 버전이 어긋나면 더 큰 위험
- 미룬다면 언제 다시 볼지 조건을 적어둔다
현재 유예 중인 항목
multer(high, DoS via deeply nested field names) —@nestjs/platform-express의 전이 의존성. 수정하려면@nestjs/platform-express@12가 필요한데 나머지 Nest 생태계가 v11 이라 호환이 깨진다. 이 템플릿에는 파일 업로드 엔드포인트가 없어 도달 경로가 없다(FileInterceptor미사용). → 파일 업로드를 구현하는 시점에 반드시 재검토한다. Nest v12 로 일괄 업그레이드하는 것이 정석.
환경변수 관리 규칙은 env-structure, 도메인별 적용은 nestjs · flutter 참조.