# ADR-0007 — 보안 검수 (Phase 3)

상태: **Accepted (2026-05-08, 야간 자율)**

---

## 결정

`wiki/server.py` 외부 노출 인터페이스에 대한 정적 코드 보안 검수 결과, 현재 알려진 취약점 없음.
다음 항목을 Phase 4 입력으로 등록 — 자율 진행 영역 외 (실제 공격 시뮬, 모니터링 인프라).

## 검수 영역 + 결과

### 1. 인증

| 항목 | 결과 |
|---|---|
| BasicAuth on POST/DELETE | ✅ `_check_auth()` 모든 변형 endpoint |
| GET 공개 (보고서/자료 다운로드) | ✅ 정책 명시 — Phase 0 결정 |
| 패스워드 비교 timing-safe | ✅ `secrets.compare_digest` |
| 패스워드 파일 .gitignore | ✅ `.factory00-password` |
| 401 응답에 WWW-Authenticate header | ✅ Basic realm |

**미확보 (Phase 4 입력)**:
- 패스워드 brute-force rate limit 없음 — Phase 4 fail2ban 또는 자체 카운터
- 세션 토큰 없음 (BasicAuth만) — Phase 4 JWT 검토

### 2. Path Traversal

5개 정적 파일 서빙 endpoint 모두 `pathlib.Path.relative_to()` 패턴으로 보호:

| Endpoint | 디렉토리 검증 |
|---|---|
| `/exports/<>` | `target.relative_to(EXPORTS_DIR)` |
| `/uploads/<>` | `target.relative_to(UPLOADS_DIR)` |
| `/versions/<>` | `target.relative_to(VERSIONS_DIR)` |
| `/postprocess` (POST) | `src.relative_to(UPLOADS_DIR)` |
| `/archive` (POST) | `target.relative_to(UPLOADS_DIR)` |
| DELETE | `target.relative_to(base_dir)` |

`urllib.parse.unquote()` 사용 후 `Path.resolve()` 가 정규화 — `..%2F`, `%252e%252e` 같은 더블 인코딩도 처리됨.

### 3. 업로드 안전성

| 항목 | 결과 |
|---|---|
| Content-Type 검증 | ✅ `multipart/form-data` 만 허용 |
| 크기 한도 | ✅ 500MB (`if length > 500 * 1024 * 1024`) |
| filename sanitize | ✅ `safe_name()` 함수 |
| 처음 0KB / 음수 길이 | ✅ `length <= 0` → 413 |
| 확장자 화이트리스트 | ⚠ 명시 없음 — STEP/STP/IGES/glb/json 등 다양 |

**미확보**:
- 확장자 명시 화이트리스트 없음. 사용자가 `.exe`도 업로드 가능. 정적 파일 서빙은 `Content-Disposition: attachment`로 강제 다운로드 모드라 브라우저에서 자동 실행 안 됨 (브라우저 보안 모델 의존).
- 바이너스 콘텐츠 검사 없음 (마법 바이트 검증). 사용자가 STEP 헤더 없는 파일을 업로드해도 변환 실패로 끝남 — 서버 안전 OK.

### 4. 변환 파이프라인

| 항목 | 결과 |
|---|---|
| Python subprocess 인자 escape | ✅ `subprocess.run([list], shell=False)` |
| Blender headless 격리 | ✅ 별도 프로세스 |
| 임시 파일 cleanup | ✅ `try/finally` 패턴 |

### 5. CORS / Referer

현재 noreferrer 정책 미설정. GET endpoint는 공개라 CORS 이슈 없음. POST는 BasicAuth 통과 필수라 외부 사이트에서 fetch 불가.

**미확보 (Phase 4)**:
- explicit `Access-Control-Allow-Origin` 미설정 — 현재 응답에 안 들어감 → 외부 사이트 fetch from JS 시 차단됨 (브라우저 default deny). 의도 — OK.
- POST에 CSRF 토큰 없음 — BasicAuth 자체로 CSRF 방어 (브라우저는 cross-origin POST에 stored credentials 자동 첨부 안 함).

### 6. /home 라우트 (신규 2026-05-08)

- 정적 파일 서빙만 (`wiki/home.html`)
- `/home`, `/home/`, `/home.html` 3개 변형 — 모두 같은 파일
- 외부 데이터 입력 없음 → 추가 공격 표면 없음

## 식별된 약점 (낮음)

| ID | 영역 | 위험도 | 완화 |
|---|---|---|---|
| S1 | 패스워드 brute-force 무제한 | 낮 | 16자 랜덤 패스워드라 brute force 비현실적. Phase 4 rate limit |
| S2 | 업로드 확장자 화이트리스트 없음 | 매우 낮 | `Content-Disposition: attachment` + 서버 미실행 정책 |
| S3 | 세션 만료 없음 (BasicAuth) | 낮 | BasicAuth는 stateless, 매 요청 검증 |
| S4 | HTTP/1.1 over Tailscale Funnel | 낮 | TLS는 Tailscale이 처리 — 자체 처리 불필요 |

## Phase 4 보안 입력

- Rate limit (BasicAuth fail counter)
- 확장자 화이트리스트 명시
- 서버 로그 + 침입 시도 알림 (Slack/Telegram webhook)
- 자동 backup 패스워드 회전 (분기 1회)
- HTTPS 인증서 자체 호스팅 (Tailscale 의존성 회피 옵션)
- 펜테스트 시나리오 — Phase 4 외부 보안 컨설팅 검토

## 자동 검증

다음을 GitHub Actions / CI에 추가 검토 (Phase 4):
- `bandit` Python 정적 분석
- `safety` 의존성 CVE 스캔
- `trivy` 컨테이너 / 파일시스템 스캔

## 참고

- ADR-0006 — Phase 3 범위
- `wiki/server.py` 라인 373 (auth) / 422·493·509·561·635·658·711 (path traversal)
