* chore: 공개 저장소 전환 보안 강화 + README 재구성 보안 리뷰 결과를 반영하여 회사 직원 로컬 사용에 안전하도록 강화. README는 사용자가 보기 쉽도록 목차·기능별 anchor·보안 모델로 재구성. ## 보안 변경 (CRITICAL/HIGH 위주) ### 인프라 (docker-compose.yml) - Postgres 호스트 포트 5432 노출 제거 (컨테이너 내부 네트워크만 사용) → LAN의 다른 장치에서 직접 DB 접근 불가 - App 포트 127.0.0.1:3000으로 바인딩 (LAN 노출 차단) - DB 자격증명 fallback `:- meetingpass` 제거 → `:?` 강제 환경변수 → .env 누락 시 명시적 에러로 실패 (기본 비밀번호 사용 위험 제거) ### 응답 헤더 (next.config.ts) - X-Content-Type-Options: nosniff - X-Frame-Options: DENY - Referrer-Policy: strict-origin-when-cross-origin - Permissions-Policy: camera=(), microphone=(self), ... ### 입력 검증 - /api/summarize: transcript 100,000자 상한 (이전 무제한) - DOMPurify: FORBID_TAGS/ATTR ['style'] 추가 → 인라인 CSS 인젝션 차단 ### 런타임 안전성 - src/lib/db.ts: DATABASE_URL 미설정 시 production에서 명시적 경고 (build-time 빌드는 placeholder로 통과 유지) ### 사용자 안내 (UI) - /settings 페이지: 공유 PC 경고 + Web Speech API 외부 송출 안내 ## 문서 ### 신규 - LICENSE (MIT) - SECURITY.md (위협 모델 / 신뢰 모델 / 한계 / 신고 방법) ### README 재구성 - 4-line Quick Start 헤더 - 📑 목차 (anchor 점프) - ✨ 주요 기능 7개 sub-section (각각 사용법/동작/제한) - 🔒 보안 모델 섹션 추가 (공개 배포 권장 사항 명시) - ⚠️ 알려진 제약 표 정리 ### .env.example - 강력한 비밀번호 생성 가이드 (openssl rand -base64 24) - 필수/선택 명확히 구분 ### docs/ROADMAP.md - "보안 강화" 섹션 추가 (완료된 보안 조치 목록) ## 사용자 직접 조치 필요 (코드로 처리 불가) - ⚠️ .env 파일에 발급된 Gemini API 키가 주석 처리되어 남아 있음 Google AI Studio에서 폐기 후 .env에서 해당 줄 삭제 필요 - POSTGRES_PASSWORD를 강력한 임의 값으로 교체 (openssl rand -base64 24) - 비밀번호 변경 시 docker compose down -v 후 재시작 (데이터 초기화) ## 검증 - 102 tests passing - 보안 헤더 4종 적용 확인 (curl -sI) - LAN IP에서 접근 시도 차단 확인 (192.168.x.x:3000 → 000) - 127.0.0.1:3000 정상 동작 확인 (200) ## 보류된 항목 (문서화로 처리) - M-1: Web Speech API의 음성 → Google 송출 (구조적 한계) - M-2: npm audit 6건 (dev 의존성, 자동 수정 가능한 것 없음) - H-3: 파일 업로드 매직 바이트 검증 (업로드 파일 미사용 상태) - 인증 시스템: 단일 사용자 가정으로 비목표 * docs: 직원용 5분 시작 가이드 추가 EMPLOYEE_QUICKSTART.md에 직원 셋업 5분 가이드 작성: - 한 번에 끝나는 setup 명령 (openssl로 강력한 비밀번호 자동 생성) - 추천 사용 흐름 + 기능별 단축 액션 - 사내 정책 확인 안내 (음성 → Google 송출 명시) - 단순 변환 모드(API 키 없음) 옵션 안내 - 자주 쓰는 docker 명령 README에서 직원/팀원 공유 시 가이드로 링크.
7.8 KiB
🔒 Security
이 문서는 Meeting Minutes의 위협 모델, 알려진 한계, 안전한 사용 방법을 정리합니다. 보안 취약점을 발견했다면 신고 방법을 참고해주세요.
📑 목차
🎯 위협 모델
이 프로젝트는 개인용 단일 인스턴스를 가정합니다. 위협 모델은 다음과 같습니다:
In-scope (방어 대상)
- 같은 LAN/Wifi에 있는 다른 사용자가 사용자의 노트북에 접근하는 시나리오
- 공유 PC에서 잠시 자리를 비웠을 때 다른 사람이 데이터에 접근하는 시나리오
- 마크다운에 삽입된 악성 HTML/스크립트로 인한 XSS
- 클라이언트가 보낸 비정상 입력 (큰 텍스트, 잘못된 MIME 등)
Out-of-scope (방어하지 않음)
- 다중 사용자 / 인증 — 단일 사용자 가정으로 인증 시스템 미구현
- 호스트 OS / 디스크 암호화 — 운영체제와 디스크 암호화 설정에 위임
- 클라우드 환경 배포 — localhost 전용으로 설계됨
- 물리적 접근 통제 — 사용자 PC에 대한 물리적 보안
🔐 신뢰 모델
| 신뢰 수준 | 항목 |
|---|---|
| 신뢰 | 호스트 OS, Docker, 사용자가 발급한 Gemini API 키, npm 패키지 (marked, DOMPurify, Prisma 등) |
| 반신뢰 | 사용자 입력 transcript / markdown (DOMPurify로 sanitize) |
| 신뢰 안 함 | 같은 네트워크의 다른 장치, 외부 HTTP 요청, Web Speech API 결과 |
📡 데이터 흐름과 외부 송출
| 데이터 | 저장 위치 | 외부 송출 |
|---|---|---|
| 회의 transcript / 회의록 | PostgreSQL (meetings.markdownMinutes, rawTranscript) |
Gemini AI 요약 활성화 시 transcript가 Google로 전송 |
| 액션 아이템 | PostgreSQL (action_items) |
외부 송출 없음 |
| Gemini API 키 | 브라우저 LocalStorage 또는 .env |
요청 시 Google에만 전송 |
| 음성 데이터 | 메모리 (실시간), /app/uploads (파일 업로드) |
Chrome Web Speech API → Google 서버 ⚠️ |
⚠️ 주의: Web Speech API의 음성 외부 전송
Chrome의 SpeechRecognition API는 음성 데이터를 Google 서버로 전송하여 처리합니다. 이는 Chrome 자체의 동작이며 우리 서버를 경유하지 않습니다. 다음의 경우 사용을 재고하세요:
- 회의가 회사 기밀, 인사 정보, 환자 정보 등을 포함
- 사내 컴플라이언스 정책이 외부 클라우드 음성 전송을 금지
- GDPR / HIPAA 등 규제 환경
이 경우 파일 업로드 탭도 같은 한계가 있으므로(현재 업로드 후 전사는 미구현, Phase 4에서 자체 STT 검토 예정), 이 도구의 사용을 보류하는 것을 권장합니다.
✅ 구현된 보안 조치
인프라
- Postgres 포트 미노출 —
db컨테이너는app만 내부 네트워크로 접근, 호스트 포트 안 열림 - App 포트 localhost 바인딩 —
127.0.0.1:3000(LAN의 다른 장치는 접근 불가) - DB 비밀번호 fallback 제거 —
.env에서 명시적으로 설정해야 docker-compose 시작됨 - Prisma adapter 사용 — Prisma 7
@prisma/adapter-pg로 안전한 PostgreSQL 연결
응답 헤더
next.config.ts에서 다음 헤더 적용:
X-Content-Type-Options: nosniff— MIME sniffing 차단X-Frame-Options: DENY— 클릭재킹 방지Referrer-Policy: strict-origin-when-cross-originPermissions-Policy: camera=(), microphone=(self), geolocation=(), interest-cohort=()
입력 검증
- 마크다운:
marked→isomorphic-dompurify통과USE_PROFILES: { html: true }FORBID_TAGS: ['style'],FORBID_ATTR: ['style']<script>,onerror=,javascript:URL 등 제거
- /api/summarize: transcript 100,000자 상한, 초과 시 413
- /api/summarize-live: transcript 40,000자 상한 (뒤쪽만 사용)
- 파일 업로드: 500MB 상한, MIME allowlist (단, 한계 참고)
API 키 보호
/api/settings/status: 키 존재 여부만 응답 (값 노출 X)/api/settings/test: 401/403/429 분류, detail 200자로 제한getStoredApiKeyGET 요청에 절대 포함 안 함 (body 전송만)maskApiKey(): UI에 표시 시AIza••••XYZ12형태로 마스킹
⚠️ 알려진 한계
MEDIUM — 인증 부재
현황: /api/* 엔드포인트에 인증이 없습니다. 127.0.0.1:3000 바인딩으로 LAN 노출은 차단되지만, 같은 머신의 다른 사용자나 잠금 해제된 PC에서는 접근 가능합니다.
완화:
- 1인 1머신 + 잠금 해제 시 PC를 떠나지 않음
- 추가 보호가 필요하면 reverse proxy(nginx, Caddy)에 Basic Auth 추가
- 회사 환경에서는 SSO 통합이 필요하지만 현재 비목표
MEDIUM — 파일 업로드 매직 바이트 미검증
현황: audio-validation.ts는 file.type(클라이언트 제공)과 파일명 확장자만 검사. 매직 바이트 검증 없음.
완화: 업로드된 파일은 현재 어디에도 서빙되지 않으며 Phase 4까지 활용되지 않음. 디스크에 저장만 됨.
MEDIUM — 서버 측 레이트 리미팅 부재
현황: 클라이언트 측에 백오프/쿨다운은 있으나 서버는 IP당 요청 수 제한 없음.
완화:
- localhost 바인딩으로 외부 abuse 차단
- Gemini 호출은 사용자 본인 키로 본인 quota 소진 → 본인 비용
LOW — npm audit moderate 6건
현황:
@hono/node-server,hono,postcss(모두 dev 의존성)- 프로덕션 번들 미포함, 실제 공격 가능성 낮음
완화: 정기적으로 npm audit fix 실행
LOW — LocalStorage 키 노출
현황: 브라우저 개발자 도구로 localStorage.getItem('meeting-minutes:gemini-api-key') 실행 시 키 노출.
완화: 동일 출처 정책으로 다른 사이트에서 접근 불가. 사용 후 🗑️ 삭제 버튼으로 즉시 제거.
🚀 배포 권장 사항
✅ 권장 (개인용)
# 강력한 비밀번호로 .env 작성
echo "POSTGRES_PASSWORD=$(openssl rand -base64 24)" >> .env
# 로컬 머신에서만 접근
docker compose up -d
# → 127.0.0.1:3000 (LAN 노출 X)
⚠️ 비권장 (현재 미구현 보안 통제 필요)
- 클라우드 인스턴스(EC2, GCP 등) 배포
- 회사 LAN의 공용 서버 배포
- HTTPS 없이 외부 도메인에 노출
🔧 추가 보호 (선택)
Reverse proxy + Basic Auth (예: Caddy)
meeting.example.com {
basic_auth {
user $2a$14$... # bcrypt 해시
}
reverse_proxy 127.0.0.1:3000
}
VPN 뒤에서만 접근: 회사 VPN/Tailscale 등 + 호스트 IP 제한
디스크 암호화: macOS FileVault, Linux LUKS, Windows BitLocker
🚨 취약점 신고
보안 취약점을 발견하면:
- 공개 이슈 X — GitHub Issues에 공개로 올리지 마세요
- 메인테이너에게 직접 연락 또는 GitHub Security Advisory 사용
- 다음을 포함:
- 영향받는 버전 / 컴포넌트
- 재현 방법
- 영향 범위
- 가능한 완화책
응답 시간 목표: 72시간 이내 1차 응답.
📚 참고
최종 업데이트: 2026-04-28