generated from ttipo/project-base
docs: 메일 모듈에 맞춰 README·규칙·스킬 문서 갱신
구현한 인증·메일·인프라에 맞춰 공통 규칙과 스택별 스킬 문서를 실제 파일과 일치시킨다. - security: 세션 회전·로그인 실패 한도·출처 검사·프록시 신뢰·네트워크 분리·게이트웨이 비밀 등급, 외부로 나가는 지문은 받는 쪽이 모르는 비밀로 HMAC, 의존성 유예 표(2026-10-27 점검)와 보안 릴리스 예외 - error-codes: AUTH_004 와 세션 만료(AUTH_002) 흐름 - env-structure·checklist: 메일 엔진 env 와 초기 설정 순서(관리자 아이디는 bootstrap 전에) - nestjs·vue3·docker 스킬: 기본 제공 골격 표와 확장 규칙, Node·npm·이미지 고정 위치 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -13,13 +13,25 @@
|
||||
|
||||
- JWT는 HttpOnly·Secure 쿠키 사용 (웹 `localStorage` 금지) / 모바일은 `flutter_secure_storage` 사용 (키: `jwt_token`)
|
||||
- Access Token 만료는 짧게(15분~1시간), Refresh Token 은 HttpOnly 쿠키로만 전달
|
||||
- 리프레시 토큰은 서버 세션에 **해시만** 저장하고 **1회용**으로 회전한다. 이미 쓴 토큰이 다시 오면 탈취로 보고 세션 전체를 끊는다.
|
||||
동시 갱신(탭 여러 개)을 위한 유예는 "바로 이전 토큰 + 짧은 시간"으로 좁히고, 유예 응답에서는 새 토큰을 주지 않는다 (훔친 토큰으로 얻는 것이 없게)
|
||||
- 로그인 실패 한도는 자격 확인 **전에** 실패 한 번을 원자적으로 차지하고 성공하면 돌려준다.
|
||||
확인 뒤에 세면 동시에 들어온 요청 수백 건이 모두 한도 검사를 통과한다 (구현 [[nestjs]])
|
||||
- 비밀번호는 단방향 해시(`bcrypt`/`argon2`)로 저장한다. 평문·가역 암호화 금지
|
||||
- 인증 실패 응답에서 **원인을 구분해 알려주지 않는다** (아이디 없음/비밀번호 틀림 → 동일하게 AUTH_001)
|
||||
- **메일 게이트웨이(대리 접근) 계정은 관리자 계정까지 대리할 수 있다** (Stalwart 0.16 확인) — 게이트웨이 비밀번호는 관리자급 비밀로 다룬다.
|
||||
백엔드는 엔진 관리자 계정을 로그인·대리 접근 대상에서 막는다: 정적 목록(엔진에 묻기 전) + 로그인·리프레시 때 엔진 권한 확인 ([[nestjs]])
|
||||
- 인증 실패 응답에서 **원인을 구분해 알려주지 않는다** (아이디 없음/비밀번호 틀림 → 동일하게 AUTH_004)
|
||||
- 로그인 시도의 실패는 AUTH_004, 로그인 없이 보호 자원에 접근한 것은 AUTH_001 이다.
|
||||
로그인 실패에 AUTH_001 을 쓰면 "로그인이 필요한 서비스입니다"가 떠 사용자가 원인을 알 수 없다.
|
||||
|
||||
## 입력 · 출력
|
||||
|
||||
- 입력값은 DTO(`class-validator`) 검증을 거친 뒤 서비스 계층에 전달한다. 전역 `ValidationPipe` 의 `whitelist`/`forbidNonWhitelisted` 를 끄지 않는다
|
||||
- TypeORM ORM 쿼리 사용으로 SQL Injection 차단 (Raw 쿼리 시 파라미터 바인딩 필수)
|
||||
- 외부로 나가는 데이터(보내는 메일의 헤더 등)에 비교용 지문을 실어야 하면 평범한 해시가 아니라 **받는 쪽이 모르는 비밀로 HMAC** 한다
|
||||
(예: 메일마다 다른 무작위 `Idempotency-Key`). 평범한 해시는 받은 사람이 후보 값(숨은참조 주소 등)을 대입해 맞춰 볼 수 있다.
|
||||
- **서버 서명 키(`JWT_SECRET` 등)나 거기서 파생한 키를 쓰지 않는다** — 평문을 아는 받은 사람이 (평문, HMAC) 쌍으로
|
||||
서명 키 후보를 오프라인에서 대입해 볼 수 있고, 맞히면 토큰을 위조할 수 있다 (2026-10-26 리뷰)
|
||||
- 에러 응답에 스택·쿼리·내부 호스트명을 싣지 않는다 — 미정의 예외는 항상 `SYS_001` 로 수렴시킨다 ([[error-codes]])
|
||||
### 파일 업로드
|
||||
|
||||
@@ -32,6 +44,9 @@
|
||||
- **저장 파일명은 서버가 생성**한다(UUID + 검증된 확장자). 클라이언트가 보낸 이름을 경로에 쓰면
|
||||
`../../etc/passwd` 같은 경로 탈출이 가능하다. 원본 이름은 표시용 메타데이터로만 보관한다.
|
||||
- 크기 한도를 설정한다 (`UPLOAD_MAX_SIZE_MB`, 기본 5MB). 초과 시 `FILE_002`(413).
|
||||
- 큰 업로드(메일 첨부 등)는 **메모리가 아니라 임시 디스크**로 받아 스트리밍으로 넘기고, 성공·실패와 관계없이 지운다.
|
||||
메모리에 받으면 동시 업로드 수 × 한도만큼 상주해 컨테이너가 OOM 으로 죽고 모든 요청이 끊긴다.
|
||||
임시 디스크를 tmpfs 로 두면 메모리 한도에 잡혀 같은 문제가 된다.
|
||||
- 업로드 디렉터리를 **정적 파일로 그대로 서빙하지 않는다.** 내려줄 때는 컨트롤러를 거쳐
|
||||
`Content-Type` 을 서버가 지정하고 `Content-Disposition: attachment` 를 붙인다.
|
||||
|
||||
@@ -45,6 +60,14 @@
|
||||
- 보안 헤더(`helmet`) 적용
|
||||
- CORS 는 화이트리스트 방식. 차단 시 `callback(null, false)` 로 **헤더를 부여하지 않아** 브라우저가 막게 한다
|
||||
(`callback(new Error(...))` 를 쓰면 500(SYS_001)이 되어 원인 파악이 어려워진다)
|
||||
- 쿠키로 인증하므로 상태를 바꾸는 요청은 **출처를 검사**한다 (`OriginGuard`, 전역 — 계약 `docs/mail/api-contract.md` 머리말).
|
||||
비교 기준은 `Host` 가 아니라 허용 목록(`CORS_ORIGIN`)이다. 프록시 뒤에서는 `Host` 가 바뀌어 정상 요청까지 막힌다.
|
||||
- 클라이언트 IP 는 `TRUST_PROXY`(홉 수 또는 주소 목록)로만 해석한다. 모든 홉을 믿으면(`true`) `X-Forwarded-For` 위조로
|
||||
IP 기준 한도(로그인 실패·rate limit)가 무력화된다. 입구는 프론트 nginx 하나로 둔다 —
|
||||
프록시를 건너뛰어 백엔드에 닿는 경로가 있으면 홉 수가 어긋나 위조가 가능해진다.
|
||||
- **인터넷을 향한 컨테이너(프론트 nginx)는 백엔드에만 닿게** 한다 — DB·메일 엔진과 같은 네트워크에 두지 않는다
|
||||
(`edge`: frontend↔backend / `app-network`: backend↔db↔mail, [[docker]]). 메일 엔진의 신뢰 대역은 `app-network` 뿐이다.
|
||||
- 컨테이너가 쓰는 데이터(업로드 등)는 **named volume** 에 둔다 — 호스트 바인드 마운트는 root 소유로 생겨 비-root 컨테이너가 쓰지 못한다.
|
||||
- Rate Limiting: **`@nestjs/throttler` 하나로 일원화**되어 있다 (`app.module.ts` 의 `APP_GUARD`).
|
||||
초과 시 429 → 전역 필터가 **`REQ_001`** 로 매핑한다.
|
||||
- 기본 정책은 루트 `.env` 의 `THROTTLE_TTL_MS`/`THROTTLE_LIMIT` (미설정 시 60초/100회)
|
||||
@@ -85,7 +108,32 @@
|
||||
2. **수정이 다른 것을 깨뜨리는가** — 생태계 버전이 어긋나면 더 큰 위험
|
||||
3. 미룬다면 **언제 다시 볼지** 조건을 적어둔다
|
||||
|
||||
> **현재 유예 중인 항목**: 없음 (`npm audit` 0건)
|
||||
> **현재 유예 중인 항목** (2026-10-27 점검):
|
||||
> - frontend `eslint-plugin-vue@9` → `postcss-selector-parser` (moderate, 선택자 파싱 CPU 과다 사용)
|
||||
> - 도달 경로: 개발 시 린트에서만 돈다. 입력은 우리 소스 코드뿐이라 외부 공격 경로가 없다. 운영 번들·이미지에 포함되지 않는다.
|
||||
> - 미루는 이유: 수정판이 `eslint-plugin-vue@10`(메이저)이라 린트 규칙이 바뀌고 설정 이관이 필요하다.
|
||||
> - 다시 볼 시점: ESLint 스택을 올릴 때, 늦어도 2027-01.
|
||||
> - backend (2026-10-27 기준 7건 — high 3·moderate 4, 운영 의존성만 보면 6건 — 아래 5종):
|
||||
>
|
||||
> | 항목 | 도달 경로 | 미루는 이유 | 다시 볼 시점 |
|
||||
> | --- | --- | --- | --- |
|
||||
> | `body-parser` 2.3.0 (high, 압축 본문 DoS) | 없음 — Nest 기본 파서를 끄고 JSON 파서만 `inflate:false` 로 둔다. 압축 본문은 해제 전에 415 (실서버 확인) | 수정판 2.4.0 이 2026-10-22 배포 (2주 미만) | 2026-11-05 에 lock 갱신 (express 5.3.0 이 `^2.3.0` 이라 범위 안) |
|
||||
> | `multer` 2.4.0 (high, 비동기 fileFilter DoS) | 없음 — 우리 fileFilter(upload·mail)는 모두 동기 | 2.5.0 이 2026-10-22 배포 (2주 미만). `@nestjs/platform-express` 최신 12.2.0 도 2.4.0 고정이라 override 를 올릴 근거가 아직 없음 | 2026-11-05 이후 override `^2.5.0` 여부 결정 |
|
||||
> | `js-yaml` 5.3.0 (moderate, `@nestjs/swagger` 하위) | 없음 — swagger 는 `dump` 만 쓰고 우리 코드는 YAML 을 파싱하지 않는다 | swagger 11.x 가 5.3.0 으로 정확히 고정 (12.x 는 5.5.0 이지만 메이저) | swagger 11.x 가 ≥5.4.1 을 선언한 릴리스 + 2주. 또는 override `^5.4.3` 검토 (swagger 12 가 5.x 를 쓰는 것이 근거) |
|
||||
> | `fast-uri` 3.1.8 (moderate, 개발 전용) | 없음 — Nest CLI 의 ajv 에서만 쓰이고 운영 이미지에 없다 | 3.1.9 가 2026-10-19 배포 (2주 미만) | 2026-11-02 |
|
||||
> | `sanitize-html` 2.18.0 (moderate, 버린 iframe 의 대체 내용 재귀 — GHSA-c9rx) | 없음 — 우리 설정은 iframe 을 내용째 버려 대체 내용을 다시 정화하지 않는다 (5만 단계 중첩으로 확인, 2만 단계 회귀 테스트로 고정) | 수정판 2.18.1 이 2026-10-14 배포 (2주 미만) | 2026-10-28 이후 2.18.1 |
|
||||
>
|
||||
> 2026-10-27 Node 24 전환과 함께 sanitize-html 고정(2.17.5)을 풀어 이전 권고 4건을 해결했다.
|
||||
> 2.18 이 쓰는 htmlparser2 계열은 ESM 전용이라 Jest 설정에서 CJS 로 변환한다 ([[nestjs]] "의존성 추가/제거").
|
||||
>
|
||||
> 참고: `@nestjs/platform-express` 11.2.7(2026-09-30) 이상은 multer 2.4.0 을 직접 선언한다 — 올리면 아래 multer override 를 지울 수 있다.
|
||||
>
|
||||
> 같은 점검에서 정리한 것: frontend 22건 → 2건 (axios 1.20.0 · vue 3.5.43 · vite 8.3.4 · oxfmt 0.72.0 으로 갱신, lock 은 Linux 컨테이너에서 생성),
|
||||
> backend 12건 → 7건 (critical 0 — proxy-addr 2.0.8 등).
|
||||
> 새 버전은 배포 후 2주 이상 지난 패치 버전을 골랐다 (공급망 위험 — 갓 나온 버전은 피한다).
|
||||
> **예외: 우리에게 닿는 취약점을 고치는 보안 릴리스**는 2주를 기다리지 않고 CI 가 통과하는 대로 올린다 — 알려진 취약점을 2주 더 두는 위험이 더 크다
|
||||
> (예: Node 24.22.1 — 2026-10-20 보안 릴리스를 6일 만에 고정, 고정 위치는 [[docker]]).
|
||||
> 도달 경로가 없다고 확인한 취약점이면 평소처럼 2주를 기다린다 (예: 위 표의 sanitize-html 2.18.1).
|
||||
|
||||
### 전이 의존성 취약점은 overrides 로 끊는다
|
||||
|
||||
|
||||
Reference in New Issue
Block a user