crowny-class MP4 하이브리드 복구 — 적대검증(실측)
개요
CVF 1080p가 실측상 실용불가(원본보다 큰 팽창·134배·인코딩 9~10시간) 판정에 따라, 강의 재생을 MP4(1080p 네이티브 h264, 원본 23편
데이터/영상/파일/*.mp4)로 복구한 하이브리드 구성(MP4=실재생, CVF=아카이브 병행 보존)에 대해 "페이월 뚫림·무인증 노출·재생경로 혼재·정적 우회로 유료 mp4 유출"을 가정하고 반증 시도. 코드 수정 없이 실측만 수행.
판정 요약 (5축 전부 안전)
| 축 | 판정 | 근거 |
|---|
| 페이월 (스트림) | 안전 | 무로그인/가짜owner/가짜Bearer 전부 401, 무료3클립만 200/206, 실가입 owner(활성수강 없음)도 401 |
| 정적 우회 | 안전 | DATA(데이터/)가 PUBLIC(public/) 밖 — 어떤 경로도 도달 불가. ../%2e%2e/%2f/이중인코딩 전부 무해(SPA index.html 폴백 or 400) |
| 단일 재생경로 | 안전 | class.html <video id="player"> 단일, CVF 캔버스/사이드카 코드 완전 제거(주석에 "폐지" 명시) |
| CVF 아카이브 격리 | 안전 | video.crowny.org(crowny-video :9877) /api/download/class_* 403(defense-in-depth). class 자체 CVF 엔드포인트(/api/video/cvf/<id>)도 mp4와 동형 게이트(401/200 확인) |
| 무료 과잉차단 아님 | 안전 | 무료 3클립(mrgilkb817j8,mrgilkgmynj9,mrgilklla64m) 무로그인 200/206 정상 |
실측 근거 (curl)
bash# 1) 무료 클립 — 무로그인 Range 재생
curl -H "Range: bytes=0-1000" http://127.0.0.1:9864/api/video/stream/mrgilkb817j8
# → 206
# 2) 유료 클립 — 무로그인
curl http://127.0.0.1:9864/api/video/stream/mrgilkupvvut
# → 401 {"error":"인증이 필요합니다"}
# 3) 유료 클립 — 가짜 owner 헤더/쿼리
curl -H "X-Crowny-Owner: fake-owner-123" .../stream/mrgilkupvvut # 401
curl ".../stream/mrgilkupvvut?owner=fake-owner-123" # 401
# 4) 유료 클립 — 가짜 admin Bearer
curl -H "Authorization: Bearer WRONG_TOKEN" .../stream/mrgilkupvvut # 401
# 5) 유료 클립 — 실제 admin Bearer(.env ADMIN_TOKEN)
curl -H "Authorization: Bearer <real>" .../stream/mrgilkupvvut # 206
# 6) 실가입(회원가입 API로 실제 owner 발급) — 활성 수강 없음
curl -X POST .../api/enroll/signup {id,pw,...} → owner 발급
curl -H "X-Crowny-Owner: <실owner>" .../stream/mrgilkupvvut # 401 (getEnrollment status=='active' 없음 → 게이트)
curl -H "X-Crowny-Owner: <실owner>" .../stream/mrgilkb817j8(free) # 200 (free bypass 정상)
# 테스트 계정은 감사 후 데이터/회원/audittest01.json 삭제로 정리
# 7) 정적 우회 — raw socket(curl 정규화 우회)로 원시 바이트 전송
GET /%2e%2e/데이터/영상/파일/mrgilkupvvut.mp4 → 200 text/html(SPA index.html, mp4 아님)
GET /..%2f데이터%2f영상%2f파일%2fmrgilkupvvut.mp4 → 400 bad path
GET /api/../데이터/영상/파일/mrgilkupvvut.mp4 → 200 text/html(SPA index.html)
GET /%252e%252e/... (이중인코딩) → 200 text/html(SPA index.html)
# 8) CVF 자체 엔드포인트도 동형 게이트
GET /api/video/cvf/mrgilkupvvut(유료, 무인증) → 401
GET /api/video/cvf/mrgilkb817j8(무료) → 200
# 9) video.crowny.org(별도 서비스 :9877) class_ 아카이브 차단
curl http://127.0.0.1:9877/api/download/class_o1-1.cvf → 403 "class 강의 파일은 class.crowny.org에서만 열람할 수 있습니다"
왜 정적 우회가 원천 차단되는지 (구조적 이유)
server.js: PUBLIC = path.join(__dirname,'public'), DATA = path.resolve(__dirname, config.DATA_DIR)(./데이터) — 둘은 형제 디렉토리, 데이터/는 public/ 하위가 아님.
- Node
new URL(req.url,...)가 WHATWG 스펙대로 %2e%2e를 리터럴 ..와 동일 취급해 파싱 단계에서 이미 dot-segment 정규화(루트 경계에서 상위 이탈 불가) → /%2e%2e/데이터/...도 이미 /데이터/...로 정규화된 채 들어옴.
- 앱 코드가 추가로
decodeURIComponent 후 fp.includes('..') 체크(400) — %2f로 슬래시만 인코딩해 세그먼트 병합을 피한 케이스까지 잡음.
- 설사 정규화·체크를 다 통과해도
path.join(PUBLIC, fp)는 public/데이터/... 형태가 되어 실제 파일이 없으므로 SPA index.html 폴백(200, text/html) — mp4 바이트가 나온 적 없음(모든 실패 케이스 응답 Content-Type이 text/html이거나 application/json, 크기가 index.html과 동일 22208B).
관련 파일
/Users/ef/crowny-class/server.js (정적서빙·라우팅 골격, 수정 없음)
/Users/ef/crowny-class/modules/video/routes.js (스트림/CVF 게이트, 수정 없음)
/Users/ef/crowny-class/public/class.html (단일 <video> 재생, CVF 캔버스 폐지 주석 확인)
/Users/ef/crowny-video/server.js:1630-1639 (class_* 403 defense-in-depth)
/Users/ef/crowny-gateway/gateway.yaml (video.crowny.org → 127.0.0.1:9877 별도 서비스, class.crowny.org와 무관)
잔여 이슈 / 관찰
- 이번 감사는 코드 수정 없이 실측만 수행(지시 준수). 구멍 미발견.
데이터/수강/, 데이터/회원/이 감사 시점에 비어 있어(실사용자 미등록) enrollment 활성 케이스는 실측 못 함 — pending/active 전환 로직 자체는 API-CONTRACT.md 상 confirm-deposit/webhook 경유이며 이번 범위 밖(코드 리딩으로만 확인, status==='active' 게이트는 getEnrollment에서 명확).
- CVF 파이프라인(
encode-upload.sh)은 원본 mp4를 이동/삭제하지 않음(자기 WORK 임시디렉토리만 정리) — 인코딩 성공/실패와 무관하게 mp4 재생 경로 불변 확인(코드 리딩).
- 크라우니코드 프로토콜: 이번 작업은 순수 검증(코드 생성 없음) — lookup/learn 해당 없음(0/0/0).