# InsuRo 보안 전수조사 보고서 — OWASP Top 10 + 코드 품질 감사

**작업 ID**: task-2262
**팀**: dev5-team (마르둑 팀장)
**대상**: `/home/jay/projects/InsuRo` (server/main.py 5068줄 + src/ 235개 파일)
**검사 기준**: OWASP Top 10 (2021) + 보험/금융 도메인 PII 특화
**검사 일시**: 2026-04-28
**검사 모드**: Full (A01~A10 전체)
**스택**: Python (FastAPI) + TypeScript (React/Vite) + Supabase (PostgreSQL)
**스킬 실행**: /owasp-security (Full) + /sanitize + /security-review

---

## 요약

- CRITICAL: 2건
- HIGH: 6건
- MEDIUM: 7건
- LOW: 4건
- INFO: 3건
- **합계: 22건**

---

## 1. 로키 (Loki) — 보안 전략 총괄

### 전체 공격 표면 조망

**백엔드 엔드포인트**: 약 40개 (server/main.py + mediscan_router)
- JWT 인증 적용: 37개 (Depends(verify_jwt) / require_plan / require_feature)
- 공개(인증 없음): 3개 (/api/status, /api/insuro/generate-queue-status, /api/insuro/premium/remaining-seats)
- Rate limiting: slowapi 적용 (대부분 30/minute)

**외부 연동 포인트**: Supabase, Google Drive, Naver API, Naver SearchAd, Claude CLI, PostHog, InsuWiki Sync
**인증 방식**: Supabase JWT (ES256/HS256) + JWKS 기반 서명 검증
**플랜 기반 접근 제어**: 5단계 (무료/베이직/프로/맥스/히든) + 기능별 feature key 검증

### 감사 오케스트레이션 결과

전반적으로 **인증/인가 체계가 잘 구축**되어 있으나, 아래 핵심 취약점 발견:

1. .env 파일 내 실운영 시크릿 노출 위험 (CRITICAL)
2. SSRF 취약점: file_url 파라미터 무검증 (CRITICAL)
3. subprocess 호출 시 입력값 검증 부재 (HIGH)
4. npm 의존성 HIGH 취약점 14건 (HIGH)
5. JWT 서명 검증 미수행 (미들웨어) (HIGH)

---

## 2. 펜리르 (Fenrir) — 침투 테스터

### A01: 접근 제어 취약점 (Broken Access Control)

#### [MEDIUM] 인증 없는 엔드포인트: generate-queue-status

**파일**: `server/main.py:1328-1331`
**심각도**: MEDIUM

```python
@app.get("/api/insuro/generate-queue-status")
async def get_queue_status():
    return generation_queue.get_queue_status()
```

**위험**: 인증 없이 AI 생성 큐 상태 정보 노출. 서비스 부하 상태, 대기 건수 등 운영 정보가 외부에 공개됨.
**권장**: `Depends(verify_jwt)` 추가.

#### [MEDIUM] CORS allow_methods/allow_headers 와일드카드

**파일**: `server/main.py:184-201`

```python
allow_methods=["*"],
allow_headers=["*"],
allow_credentials=True,
```

**위험**: TRACE/CONNECT 등 불필요한 HTTP 메서드 허용. `allow_credentials=True`와 조합 시 공격 표면 확대.
**권장**: `allow_methods=["GET","POST","PUT","DELETE","PATCH"]`, `allow_headers=["Content-Type","Authorization"]`으로 제한.

#### [MEDIUM] 파일 변환 엔드포인트 인증/Rate Limit 없음

**파일**: `server/main.py:3327, 3431`

- `POST /api/tools/convert/word-to-pdf` — 인증 없음, Rate limit 없음
- `POST /api/tools/convert/pdf-to-word` — 인증 없음, Rate limit 없음

**위험**: 누구나 50MB 파일 변환을 무제한 요청 가능. 서버 리소스 남용(DoS) 위험.
**권장**: Rate limit 추가 (최소 `10/minute`), 파일 크기 제한은 50MB로 설정되어 있어 양호.

#### [INFO] 의도적 공개 엔드포인트 (확인 완료)
- `/api/status` (L686) — 헬스체크 (의도적)
- `/api/insuro/premium/remaining-seats` (L1949) — 마케팅용 공개 API (의도적)

### A02: 암호화 실패 (Cryptographic Failures)

#### [CRITICAL] .env 파일 실운영 시크릿 포함

**파일**: `/home/jay/projects/InsuRo/.env`
**심각도**: CRITICAL

발견된 민감 정보:
- `INSURO_NEW_SERVICE_ROLE_KEY` — Supabase 서비스 롤 JWT (전체 DB 접근 권한)
- `NAVER_CLIENT_SECRET` — 네이버 API 비밀키
- `VAPID_PRIVATE_KEY` — 웹푸시 개인키
- `MEDISCAN_ENCRYPTION_KEY` — 메디스캔 암호화 키
- `NAVER_SEARCHAD_SECRET_KEY` — 네이버 검색광고 비밀키
- `INSURO_GOOGLE_CLIENT_SECRET` — 구글 OAuth 클라이언트 시크릿
- `INSURO_GOOGLE_REFRESH_TOKEN` — 구글 리프레시 토큰

**상태**: `.gitignore`에 `.env` 포함 확인 (git tracking 안 됨). 그러나 서버 파일시스템에 평문 존재.
**권장**:
1. 모든 시크릿 즉시 로테이션
2. Cloudflare/Vercel Secrets 또는 Vault 사용으로 전환
3. 서버 접근 시 시크릿 파일 접근 로그 모니터링

#### [LOW] MD5 해시 사용 (비보안 용도)

**파일**: `server/main.py:2819`

```python
hash_val = int(hashlib.md5(f"{user_id}:{req.experiment_name}".encode()).hexdigest(), 16)
```

A/B 테스트 버킷 배정용 (비보안 용도). 보안 해싱 목적이 아니므로 LOW.
**권장**: 가능하면 `hashlib.sha256`으로 교체.

### A03: 인젝션 (Injection)

#### [HIGH] subprocess 호출 시 사용자 영향 파일 경로/텍스트 주입

**파일**: `server/main.py:3685, 3712, 3874, 3935, 4034`
**심각도**: HIGH

```python
# L3685: Vision 배치 처리
subprocess.run(
    ["claude", "-p",
     f"{img_path} 파일을 읽고 이 보험 문서...",
     "--model", "haiku", ...],
    capture_output=True, text=True, timeout=60,
)
```

**위험**:
- `subprocess.run`이 배열 형태로 호출되므로 **쉘 인젝션은 불가** (shell=True 아님)
- 그러나 파일 경로에 특수문자가 포함될 경우 Claude CLI가 예상치 못한 동작을 할 수 있음
- 추출 텍스트가 프롬프트에 직접 주입되는 패턴 (L4034)

**완화 요소**: 파일은 tempfile로 생성되어 경로가 예측 가능하고 안전함.
**권장**:
1. 파일 경로를 정규표현식으로 화이트리스트 검증
2. 프롬프트에 주입되는 텍스트 길이 제한 (현재 L4031: `all_text[:2000]` — 적용됨)

#### [INFO] SQL 인젝션: 해당 없음
모든 DB 쿼리가 Supabase ORM `.select()/.insert()/.update()/.eq()` 패턴으로 안전하게 파라미터화됨.

#### [INFO] XSS: dangerouslySetInnerHTML
**파일**: `src/components/ui/chart.tsx:70`
CSS 변수 주입만 사용하며 사용자 입력 아님. **안전**.

---

## 3. 시긴 (Sigyn) — 보안 개발자

### 발견된 취약점 수정 코드 제안

#### [CRITICAL] A10-SSRF: file_url 무검증 다운로드

**파일**: `server/main.py:4076-4079`
**심각도**: CRITICAL

```python
# 현재 코드 (취약)
async with http_client.AsyncClient(timeout=30.0) as client:
    resp = await client.get(req.file_url)  # 사용자 입력 URL을 무검증 다운로드
```

**위험**: 공격자가 `file_url`에 내부 네트워크 주소(169.254.169.254, 10.x.x.x, 127.0.0.1)를 넣어 AWS 메타데이터, 내부 서비스 접근 가능.

**수정 제안**:
```python
import ipaddress
from urllib.parse import urlparse

_ALLOWED_FILE_DOMAINS = {
    "zayhfjuwviporbzokudr.supabase.co",
    "drive.google.com",
    "storage.googleapis.com",
}

def _validate_file_url(url: str) -> bool:
    """SSRF 방지를 위한 URL 화이트리스트 검증."""
    parsed = urlparse(url)
    if parsed.scheme not in ("https",):
        return False
    hostname = parsed.hostname or ""
    try:
        ip = ipaddress.ip_address(hostname)
        if ip.is_private or ip.is_loopback or ip.is_link_local:
            return False
    except ValueError:
        pass
    return any(hostname.endswith(d) for d in _ALLOWED_FILE_DOMAINS)

# 사용
if not _validate_file_url(req.file_url):
    raise HTTPException(status_code=400, detail="허용되지 않는 파일 URL입니다")
```

#### [HIGH] JWT verify_signature: False (다중 계정 탐지 미들웨어)

**파일**: `server/main.py:225-229`

```python
unverified = jwt.decode(
    token,
    options={"verify_signature": False},
    algorithms=["ES256", "HS256"],
)
```

**위험**: 서명 미검증 JWT에서 user_id를 추출하여 IP 매핑에 사용. 조작된 JWT로 false positive 유도 가능.
**수정 제안**: 탐지 목적이라도 서명 검증 후 사용하거나, 검증 실패 시 탐지를 스킵하는 방식으로 변경.

#### [HIGH] JWT verify_aud: False

**파일**: `server/main.py:670-675`

```python
payload = jwt.decode(
    token,
    signing_key.key,
    algorithms=["ES256", "HS256"],
    options={"verify_aud": False},
)
```

**위험**: audience 검증 비활성화. 다른 Supabase 프로젝트의 유효한 JWT로 인증 우회 가능.
**수정 제안**: `audience` 파라미터를 명시적으로 설정.

#### [MEDIUM] 에러 메시지 상세 노출 (11개소)

**관련 파일 및 라인**:
- `main.py:1480` — `detail=str(e)` (ingest)
- `main.py:4468` — `detail=str(exc)` (newsletter-chat)
- `main.py:4616` — `detail=str(exc)` (premium-chat)
- `main.py:1920` — `detail=f"기여 기록 저장 실패: {exc}"`
- `main.py:2719` — `detail=f"onboarding_step 업데이트 실패: {exc}"`
- `mediscan_router.py:223` — `"error_message": str(e)[:500]`
- `generation_queue.py:67,74` — `job.error = str(e)`
- `pipeline.py:138` — `stage_data["error"] = str(e)`
- `image_generator.py:100` — `"error": str(exc)`

**수정 제안**:
```python
# Before
raise HTTPException(status_code=500, detail=str(exc))
# After
logger.exception("newsletter-chat 오류")
raise HTTPException(status_code=500, detail="일시적인 오류가 발생했습니다.")
```

---

## 4. 스쿨드 (Skuld) — 보안 QA

### A09: 보안 이벤트 로깅 점검

#### [MEDIUM] 보안 이벤트 로깅 부족

**양호한 점**:
- 다중 계정 탐지 로깅 (L239-243)
- PostHog 분석 이벤트 추적
- AI 호출 감사 로그 (token_usage_log 테이블)

**부족한 점**:
- 로그인 실패 이벤트 미기록 (verify_jwt에서 401 반환 시)
- 인가 거부 이벤트 미기록 (require_plan/require_feature에서 403 반환 시)
- 비정상 요청 패턴 탐지 미구현
- 관리자 행위 감사 로그 부재

**수정 제안**:
```python
def verify_jwt(request: Request):
    auth = request.headers.get("Authorization", "")
    if not auth.startswith("Bearer "):
        logger.warning("auth_missing: ip=%s path=%s",
            request.client.host if request.client else "unknown",
            request.url.path)
        raise HTTPException(status_code=401, detail="Missing or invalid authorization")
```

### PII 데이터 흐름 추적

#### [MEDIUM] PII 마스킹 적용 범위 불충분

**양호한 점**:
- `_sanitize_pii()` 함수 구현 (L2600-2618): 주민번호, 전화번호, 이메일 마스킹
- thread_auto에서 PII 탐지 시 WARNING 로그 (L2636-2639)

**부족한 점**:
- `_sanitize_pii()` 적용이 thread_auto 1개 엔드포인트에만 한정
- 코파일럿 분석 (L2025-2027): 고객 상담 대화가 AI 프롬프트에 PII 마스킹 없이 전달
- 대화 요약 (L2293-2298): 고객/설계사 대화가 AI에 PII 마스킹 없이 전송
- 증권 분석 (L4806-4808): 고객명, 생년월일 처리 시 PII 검증 없음

**수정 제안**: AI에 전달되는 모든 사용자 입력 텍스트에 `_sanitize_pii()` 적용.

#### [LOW] 로그에 user_id 직접 노출

**파일**: `server/main.py:238-243`
Supabase UUID user_id가 로그에 직접 기록됨.

---

## 5. 마아트 (Ma'at) — QC 매니저

### A04: 불안전한 설계 (Insecure Design)

#### [LOW] Rate Limiting 미적용 엔드포인트

Rate limiting 적용: 15개 이상 엔드포인트 (양호)
**미적용 중 주의 필요**:
- `/api/insuro/wiki/contributions` — 히든 플랜이지만 rate limit 없음
- `/api/insuro/wiki/rankings` — 히든 플랜이지만 rate limit 없음
- `/api/insuro/onboarding-step` (PATCH/GET) — rate limit 없음
- `/api/tools/convert/*` — 인증 없고 rate limit 없음

### A06: 취약한 컴포넌트 (Vulnerable Components)

#### [HIGH] npm 의존성 취약점 (24건)

```
HIGH (14건):
  @remix-run/router <=1.23.1 — XSS via Open Redirects (CVSS 8.0)
  @xmldom/xmldom <0.8.13 — XML Injection 3건
  serialize-javascript — 불안전한 역직렬화
  vite — Path traversal 3건
  workbox-build/vite-plugin-pwa 체인

MODERATE (7건): yaml, follow-redirects, jsdom 등
LOW (3건): @tootallnate/once, loader-utils
```

**수정 제안**: `npm audit fix` 실행.

### A07: 인증 실패 (Authentication Failures)

JWT 취약점 2건 (시긴 섹션 참조):
1. `verify_signature: False` (L225)
2. `verify_aud: False` (L675)

### A08: 데이터 무결성

**양호**: pickle.loads, eval(), exec(), unsafe yaml.load 사용 없음.

### A10: SSRF

SSRF 취약점 1건 (시긴 섹션 참조): `file_url` 무검증 다운로드 (L4076)

### 프론트엔드 보안 점검

#### [MEDIUM] CSP 미적용
`index.html`에 Content Security Policy 메타 태그 없음.

#### [LOW] CSRF 방어 미구현
Bearer Token 인증 방식이므로 위험은 제한적이나, 쿠키 기반 세션 전환 시 대비 필요.

#### [LOW] 카카오 SDK SRI 미적용
`index.html:34` — Subresource Integrity 해시 미적용.

---

## 즉시 수정 필요 항목 (Critical + High)

1. **[CRITICAL] SSRF: file_url 화이트리스트 검증** — `main.py:4076`
2. **[CRITICAL] .env 시크릿 로테이션** — 모든 API 키/토큰 즉시 재발급
3. **[HIGH] JWT verify_signature: False 제거** — `main.py:225`
4. **[HIGH] JWT verify_aud: False 제거** — `main.py:675`
5. **[HIGH] npm 의존성 업데이트** — `npm audit fix` (14 HIGH 취약점)
6. **[HIGH] 에러 메시지 일반화** — 11개소 `str(exc)` → 일반 에러 메시지
7. **[HIGH] subprocess 입력 검증** — 파일 경로 및 텍스트 정규화/검증
8. **[HIGH] 파일 변환 엔드포인트 보호** — 인증 및 rate limit 추가

## 권장 수정 방안 (Medium + Low)

9. CORS allow_methods/allow_headers 제한 — `main.py:198-199`
10. generate-queue-status 인증 추가 — `main.py:1328`
11. CSP 헤더 추가 — index.html 또는 Cloudflare
12. 보안 이벤트 로깅 강화 — 로그인 실패, 인가 거부 이벤트
13. PII 마스킹 확대 — 코파일럿/대화요약 AI 프롬프트
14. 카카오 SDK SRI 해시 적용
15. MD5 → SHA256 교체 (A/B 테스트)
16. Rate limiting 미적용 엔드포인트 보강
17. Python 의존성 버전 업데이트 (FastAPI)
18. CSRF 대비 (향후 쿠키 기반 세션 전환 시)

---

**보고서 작성**: 보안팀 5인 합동 (로키/펜리르/시긴/스쿨드/마아트)
**다음 정기 감사 예정일**: 2026-05 스프린트 릴리즈 전
