작업 이력 — 2026-07-01
종합
맑은도우미 PMS 애드온(사용자단)의 보안·세션을 처음부터 구축해 배포했다. 애드온에는 사용자 세션 개념 자체가 없었고, PMS 데이터 읽기 GET·일부 DELETE·PUT /wbs가 무인증 공개 상태였다(고객 게시글 본문·댓글·작성자 PII·브리핑 카드가 URL만 알면 노출, id만 알면 남의 브리핑/평가 삭제 가능).
Cloudflare Access 기반 설계로 시작했으나, iframe + pages.dev 제약(서드파티 쿠키 차단)과 배포 게이트(AUD placeholder 배포 시 전원 401 장애) 부담이 커, malgn 생태계에 이미 있는 ek SSO 규약을 PMS 전용으로 확장하는 방식으로 전환했다. 결과적으로 팝업·Access·서드파티쿠키 문제가 모두 사라지고 인프라가 단순해졌다.
최종 구조: PMS가 iframe URL에 ek(당일 유효 SHA-256 해시)+email 부착 → 애드온이 조용히 /auth/pms-sso로 교환해 JWT 획득 → 모든 API 호출에 Authorization: Bearer → /pms/* 라우트가 requireAuth로 가드. 인증 실패 시에는 액션이 전무한 전역 오류 페이지로 차단한다.
1. 보안 진단 (무엇이 열려 있었나)
| 심각도 | 항목 |
|---|---|
| 🔴 | 애드온에 사용자 세션/신원 없음 — URL과 id만 알면 누구나 접근 |
| 🔴 | PMS 읽기 GET 전부 무인증(/pms/posts/:id, /pms/projects/:id, /pms/briefings/:id 등) |
| 🔴 | 무인증 파괴적 DELETE (/pms/briefings/:id, /pms/evals/:id) — id만 알면 삭제 |
| 🔴 | 무인증 쓰기 PUT /wbs — WBS 전체 덮어쓰기 가능 (리뷰 중 발견, 계획에 없던 구멍) |
| 🟡 | 서비스 토큰 관찰 모드 — LLM 생성 라우트 사실상 무인증 |
| 🟡 | CORS가 임의 *.pages.dev 반사 + credentials:true |
2. 설계 결정 — Cloudflare Access → ek SSO 전환
- 폐기: Access Application + 팝업 토큰 핸드오프. (iframe에서 Access 쿠키가 서드파티가 되어 Safari/최신 Chrome에서 차단 → 구조적으로 깨짐.
CF_ACCESS_AUDplaceholder 배포 시 전원 401 장애 위험도.) - 채택: PMS 전용
ek해시 핸드오프.ek = SHA-256( `${email}_${id}_${yyyyMMdd(KST)}_MALGNHELPER_PMS` ) // 소문자 hex PMS: m.encrypt(loginId + "_" + m.ri("pid"|"id") + "_" + m.time("yyyyMMdd") + "_MALGNHELPER_PMS", "SHA-256")id= 컨텍스트 리소스 id (브리핑=프로젝트id, 평가=게시글id) → 리소스 바인딩- salt
_PMS→ 맑은오피스/auth/ssoek와 상호치환 불가 - 당일(오늘/어제 KST) 유효 → 매일 자동 회전, 일회성 아님(401 재핸드오프 안전)
- 권한 모델: 인증된 직원 = 모든 PMS 읽기·생성 허용.
/admin/*·표준답변 변형은 기존tb_user레벨 가드 유지. - PMS 변경 최소화: 서버에서 SHA-256 한 번 계산해 URL에 부착하는 것이 전부.
3. API 구현 (malgn-helper-api)
GET /auth/pms-sso신설 —ek·email·pid검증(오늘/어제 대조) →tb_user조회·직원 확인 → 기존SessionPayloadJWT 발급(TTL 8h). 쿠키 미사용(iframe은 Bearer 사용).requireAuth가드 18개 라우트 적용 — 읽기 GET 11종 + DELETE 2종 + LLM 생성 4종(+requireServiceToken이중) +GET/PUT /wbs.- 하드닝 — 403 응답에서
email제거(로그 위생) · 브리지 HTML<script>안전 임베드 · CORS를 가족 프로젝트로 한정(malgn-helper(-admin|-pms|-mng).pages.dev+ 프리뷰만, 임의*.pages.dev반사 차단). - Access 데드코드 전량 제거 —
requireAccess·/auth/access-exchange·jose·CF_ACCESS_*(−113줄).
4. 애드온 구현 (malgn-helper-pms)
useSession()신설 — URL의ek·email·id(경로/projects/<id>·/posts/<id>에서 자동 추출,?pid=override)로/auth/pms-sso조용히 호출 → JWT를 메모리에만 보관(쿠키·스토리지 미사용) →apiFetch가 모든 호출에 Bearer 첨부 + 401 시 1회 재핸드오프.- 전 호출부
apiFetch전환(10개 파일) — 팝업·클릭 제스처 불필요해져 진입 즉시 자동 로드. forward.tsAuthorization 패스스루 — 프록시 경유 LLM 생성이requireAuth+requireServiceToken이중 가드를 통과.
5. 인증 오류 게이트 (액션 차단)
미로그인·정보 없음·기타 오류 시 "인증 후 불러오기"·"생성하기" 버튼이 노출돼 무의미한 액션이 가능했던 문제를 차단.
useSession.authError추가 — ek 없음·무효(401)·미등록(403)·교환 실패 시 세팅.app.vue전역 게이트 —authError면<NuxtPage/>대신AuthErrorPage만 렌더 → 어떤 페이지 콘텐츠·버튼도 노출 안 됨.AuthErrorPage— "접근할 수 없습니다 / 로그인 정보가 없거나 유효하지 않습니다" + 사유 텍스트. 버튼·링크·클릭 요소 전무.- 7개 페이지의 "인증 후 … 불러오기" 배너·버튼 전량 삭제.
6. 검증
- 해시 재현: 실제 PMS가 만든 ek 2건(프로젝트 1548 / 게시글 93710)을 서버 검증 로직으로 재현 → 정확히 일치·수락 확인 후 구현.
- 실서버 E2E: ek 교환
{ok:true, token}✓ · 무토큰 GET 401 ✓ · Bearer GET 200 ✓ · 위조 ek 401 ✓ - CORS: 애드온 오리진 반사·
Authorization프리플라이트 허용 ✓ /evil.pages.dev차단 ✓ - 타입체크 회귀 0(api 11=베이스라인) · pms 빌드 성공.
배포
14:02 — malgn-helper-api → Cloudflare Workers
- 커밋:
0dad619(신규 커밋: yes) - 메시지: merge: /auth/pms-sso — PMS 전용 ek 검증 라우트
- Version ID:
df122290·requireAuth가드 + CORS 한정 동시 활성
14:08 — malgn-helper-pms → Cloudflare Pages
- 커밋:
d694a0f(신규 커밋: yes) - 메시지: merge: ek pid 추출 /posts/
확장 - Deployment:
9c4a1e8f
14:43 — malgn-helper-pms → Cloudflare Pages
- 커밋:
ccb61de(신규 커밋: yes) - 메시지: merge: 전역 인증 오류 게이트 + 인증 배너/버튼 제거
- Deployment:
108f65f3
참여 활동내역
| 담당 | 수행 내용 |
|---|---|
| 가온님 (총괄) | 보안 진단·설계(Access→ek 전환 결정)·오케스트레이션·리뷰 판정·해시 재현 검증·배포·E2E 검증. PUT /wbs 가드 직접 반영. |
| 바탕님 (백엔드 개발자) | requireAccess(초기안)·/auth/access-exchange·/auth/pms-sso 라우트 · requireAuth 18개 라우트 가드 · Access 데드코드 제거 |
| 나래님 (프론트엔드 개발자) | useSession 컴포저블(팝업→ek 사일런트 교환) · 10개 파일 apiFetch 전환 · forward.ts 패스스루 · 전역 인증 오류 게이트 + AuthErrorPage |
| 미쁨님 (보안 검토자) | 라우트 가드·토큰 발급·브리지 XSS·origin 검증 리뷰. PUT /wbs 무인증 쓰기 발견(계획 누락분) · 최종 전체 브랜치 리뷰(머지 승인) |
리뷰에서 잡아 고친 실결함:
useSession._inflight가 팝업 차단 시 영구 잠김(Critical) ·init.headers캐스팅으로 헤더 유실(Important) ·eval.vue마운트 자동 로드 누락 · watch 토큰 가드 누락.
다음 작업 후보 / 알려진 한계
- 서비스 토큰 ENFORCE — 현재
SERVICE_TOKEN_ENFORCE="0"(관찰). 생성 라우트는requireAuth로 이미 보호되나, 서버-서버 이중 증명까지 원하면PMS_SERVICE_TOKENsecret 설정 → 관찰 로그 확인 →"1"로 전환. - 임베드 하드닝 —
frame-ancestorsCSP(PMS 오리진만 iframe 허용) · 모달 closepostMessage의targetOrigin고정(현재"*"). - 감사 로그 — actor(email) 기준 생성·삭제 추적.
ek특성상 한계 — URL·PMS 페이지 소스·리퍼러에 남는 당일 유효 토큰이며 salt가 소스에 있어 강한 비밀은 아니다(기존 맑은오피스 SSO와 동일 특성). "PMS 로그인 사용자 = 애드온 허용" + 리소스 바인딩 + 당일 회전 수준의 게이트.posts/[id]빈 화면 갭 — 인증 정상인데 게시물이 null인 극단 케이스에 빈 상태 UI가 없음(배너 도입 이전부터 존재한 기존 갭, 회귀 아님).- 설계 정본:
malgn-helper-pms/docs/superpowers/specs/2026-06-30-pms-security-session-design.md(§11에 Access→ek 전환 부록)