[{"data":1,"prerenderedAt":844},["ShallowReactive",2],{"doc:\u002Fhistory\u002Fhistory.20260701":3},{"id":4,"title":5,"body":6,"description":220,"extension":837,"meta":838,"navigation":839,"path":840,"seo":841,"stem":842,"__hash__":843},"docs\u002Fhistory\u002Fhistory.20260701.md","작업 이력 — 2026-07-01",{"type":7,"value":8,"toc":813},"minimark",[9,13,17,34,49,83,86,90,181,188,273,281,366,373,437,441,444,487,491,543,545,548,556,578,585,604,610,625,627,631,707,725,727,731],[10,11,5],"h1",{"id":12},"작업-이력-2026-07-01",[14,15,16],"h2",{"id":16},"종합",[18,19,20,24,25,29,30,33],"p",{},[21,22,23],"strong",{},"맑은도우미 PMS 애드온(사용자단)의 보안·세션을 처음부터 구축해 배포했다."," 애드온에는 사용자 세션 개념 자체가 없었고, PMS 데이터 읽기 GET·일부 DELETE·",[26,27,28],"code",{},"PUT \u002Fwbs","가 ",[21,31,32],{},"무인증 공개"," 상태였다(고객 게시글 본문·댓글·작성자 PII·브리핑 카드가 URL만 알면 노출, id만 알면 남의 브리핑\u002F평가 삭제 가능).",[18,35,36,37,40,41,48],{},"Cloudflare Access 기반 설계로 시작했으나, iframe + ",[26,38,39],{},"pages.dev"," 제약(서드파티 쿠키 차단)과 배포 게이트(AUD placeholder 배포 시 전원 401 장애) 부담이 커, ",[21,42,43,44,47],{},"malgn 생태계에 이미 있는 ",[26,45,46],{},"ek"," SSO 규약을 PMS 전용으로 확장","하는 방식으로 전환했다. 결과적으로 팝업·Access·서드파티쿠키 문제가 모두 사라지고 인프라가 단순해졌다.",[18,50,51,52,78,79,82],{},"최종 구조: ",[21,53,54,55,57,58,61,62,65,66,69,70,73,74,77],{},"PMS가 iframe URL에 ",[26,56,46],{},"(당일 유효 SHA-256 해시)+",[26,59,60],{},"email"," 부착 → 애드온이 조용히 ",[26,63,64],{},"\u002Fauth\u002Fpms-sso","로 교환해 JWT 획득 → 모든 API 호출에 ",[26,67,68],{},"Authorization: Bearer"," → ",[26,71,72],{},"\u002Fpms\u002F*"," 라우트가 ",[26,75,76],{},"requireAuth","로 가드."," 인증 실패 시에는 ",[21,80,81],{},"액션이 전무한 전역 오류 페이지","로 차단한다.",[84,85],"hr",{},[14,87,89],{"id":88},"_1-보안-진단-무엇이-열려-있었나","1. 보안 진단 (무엇이 열려 있었나)",[91,92,93,106],"table",{},[94,95,96],"thead",{},[97,98,99,103],"tr",{},[100,101,102],"th",{},"심각도",[100,104,105],{},"항목",[107,108,109,118,136,149,159,167],"tbody",{},[97,110,111,115],{},[112,113,114],"td",{},"🔴",[112,116,117],{},"애드온에 사용자 세션\u002F신원 없음 — URL과 id만 알면 누구나 접근",[97,119,120,122],{},[112,121,114],{},[112,123,124,125,128,129,128,132,135],{},"PMS 읽기 GET 전부 무인증(",[26,126,127],{},"\u002Fpms\u002Fposts\u002F:id",", ",[26,130,131],{},"\u002Fpms\u002Fprojects\u002F:id",[26,133,134],{},"\u002Fpms\u002Fbriefings\u002F:id"," 등)",[97,137,138,140],{},[112,139,114],{},[112,141,142,143,128,145,148],{},"무인증 파괴적 DELETE (",[26,144,134],{},[26,146,147],{},"\u002Fpms\u002Fevals\u002F:id",") — id만 알면 삭제",[97,150,151,153],{},[112,152,114],{},[112,154,155,156,158],{},"무인증 쓰기 ",[26,157,28],{}," — WBS 전체 덮어쓰기 가능 (리뷰 중 발견, 계획에 없던 구멍)",[97,160,161,164],{},[112,162,163],{},"🟡",[112,165,166],{},"서비스 토큰 관찰 모드 — LLM 생성 라우트 사실상 무인증",[97,168,169,171],{},[112,170,163],{},[112,172,173,174,177,178],{},"CORS가 임의 ",[26,175,176],{},"*.pages.dev"," 반사 + ",[26,179,180],{},"credentials:true",[14,182,184,185,187],{"id":183},"_2-설계-결정-cloudflare-access-ek-sso-전환","2. 설계 결정 — Cloudflare Access → ",[26,186,46],{}," SSO 전환",[189,190,191,202,253,267],"ul",{},[192,193,194,197,198,201],"li",{},[21,195,196],{},"폐기",": Access Application + 팝업 토큰 핸드오프. (iframe에서 Access 쿠키가 서드파티가 되어 Safari\u002F최신 Chrome에서 차단 → 구조적으로 깨짐. ",[26,199,200],{},"CF_ACCESS_AUD"," placeholder 배포 시 전원 401 장애 위험도.)",[192,203,204,207,208,210,211,221],{},[21,205,206],{},"채택",": PMS 전용 ",[26,209,46],{}," 해시 핸드오프.\n",[212,213,218],"pre",{"className":214,"code":216,"language":217},[215],"language-text","ek = SHA-256( `${email}_${id}_${yyyyMMdd(KST)}_MALGNHELPER_PMS` )   \u002F\u002F 소문자 hex\n     PMS: m.encrypt(loginId + \"_\" + m.ri(\"pid\"|\"id\") + \"_\" + m.time(\"yyyyMMdd\") + \"_MALGNHELPER_PMS\", \"SHA-256\")\n","text",[26,219,216],{"__ignoreMap":220},"",[189,222,223,232,246],{},[192,224,225,228,229],{},[26,226,227],{},"id"," = 컨텍스트 리소스 id (브리핑=프로젝트id, 평가=게시글id) → ",[21,230,231],{},"리소스 바인딩",[192,233,234,235,238,239,242,243],{},"salt ",[26,236,237],{},"_PMS"," → 맑은오피스 ",[26,240,241],{},"\u002Fauth\u002Fsso"," ek와 ",[21,244,245],{},"상호치환 불가",[192,247,248,249,252],{},"당일(오늘\u002F어제 KST) 유효 → ",[21,250,251],{},"매일 자동 회전",", 일회성 아님(401 재핸드오프 안전)",[192,254,255,258,259,262,263,266],{},[21,256,257],{},"권한 모델",": 인증된 직원 = 모든 PMS 읽기·생성 허용. ",[26,260,261],{},"\u002Fadmin\u002F*","·표준답변 변형은 기존 ",[26,264,265],{},"tb_user"," 레벨 가드 유지.",[192,268,269,272],{},[21,270,271],{},"PMS 변경 최소화",": 서버에서 SHA-256 한 번 계산해 URL에 부착하는 것이 전부.",[14,274,276,277,280],{"id":275},"_3-api-구현-malgn-helper-api","3. API 구현 (",[26,278,279],{},"malgn-helper-api",")",[189,282,283,308,324,348],{},[192,284,285,291,292,294,295,294,297,300,301,303,304,307],{},[21,286,287,290],{},[26,288,289],{},"GET \u002Fauth\u002Fpms-sso"," 신설"," — ",[26,293,46],{},"·",[26,296,60],{},[26,298,299],{},"pid"," 검증(오늘\u002F어제 대조) → ",[26,302,265],{}," 조회·직원 확인 → 기존 ",[26,305,306],{},"SessionPayload"," JWT 발급(TTL 8h). 쿠키 미사용(iframe은 Bearer 사용).",[192,309,310,315,316,319,320,323],{},[21,311,312,314],{},[26,313,76],{}," 가드 18개 라우트 적용"," — 읽기 GET 11종 + DELETE 2종 + LLM 생성 4종(+",[26,317,318],{},"requireServiceToken"," 이중) + ",[26,321,322],{},"GET\u002FPUT \u002Fwbs",".",[192,325,326,329,330,332,333,336,337,340,341,344,345,347],{},[21,327,328],{},"하드닝"," — 403 응답에서 ",[26,331,60],{}," 제거(로그 위생) · 브리지 HTML ",[26,334,335],{},"\u003Cscript>"," 안전 임베드 · ",[21,338,339],{},"CORS를 가족 프로젝트로 한정","(",[26,342,343],{},"malgn-helper(-admin|-pms|-mng).pages.dev"," + 프리뷰만, 임의 ",[26,346,176],{}," 반사 차단).",[192,349,350,291,353,294,356,294,359,294,362,365],{},[21,351,352],{},"Access 데드코드 전량 제거",[26,354,355],{},"requireAccess",[26,357,358],{},"\u002Fauth\u002Faccess-exchange",[26,360,361],{},"jose",[26,363,364],{},"CF_ACCESS_*"," (−113줄).",[14,367,369,370,280],{"id":368},"_4-애드온-구현-malgn-helper-pms","4. 애드온 구현 (",[26,371,372],{},"malgn-helper-pms",[189,374,375,410,422],{},[192,376,377,382,383,294,385,387,388,294,391,394,395,398,399,401,402,405,406,409],{},[21,378,379,290],{},[26,380,381],{},"useSession()"," — URL의 ",[26,384,46],{},[26,386,60],{},"·id(경로 ",[26,389,390],{},"\u002Fprojects\u002F\u003Cid>",[26,392,393],{},"\u002Fposts\u002F\u003Cid>","에서 자동 추출, ",[26,396,397],{},"?pid="," override)로 ",[26,400,64],{}," 조용히 호출 → JWT를 ",[21,403,404],{},"메모리에만"," 보관(쿠키·스토리지 미사용) → ",[26,407,408],{},"apiFetch","가 모든 호출에 Bearer 첨부 + 401 시 1회 재핸드오프.",[192,411,412,418,419,323],{},[21,413,414,415,417],{},"전 호출부 ",[26,416,408],{}," 전환","(10개 파일) — 팝업·클릭 제스처 불필요해져 ",[21,420,421],{},"진입 즉시 자동 로드",[192,423,424,430,431,433,434,436],{},[21,425,426,429],{},[26,427,428],{},"forward.ts"," Authorization 패스스루"," — 프록시 경유 LLM 생성이 ",[26,432,76],{},"+",[26,435,318],{}," 이중 가드를 통과.",[14,438,440],{"id":439},"_5-인증-오류-게이트-액션-차단","5. 인증 오류 게이트 (액션 차단)",[18,442,443],{},"미로그인·정보 없음·기타 오류 시 \"인증 후 불러오기\"·\"생성하기\" 버튼이 노출돼 무의미한 액션이 가능했던 문제를 차단.",[189,445,446,452,476,484],{},[192,447,448,451],{},[26,449,450],{},"useSession.authError"," 추가 — ek 없음·무효(401)·미등록(403)·교환 실패 시 세팅.",[192,453,454,457,458,291,461,464,465,468,469,475],{},[26,455,456],{},"app.vue"," ",[21,459,460],{},"전역 게이트",[26,462,463],{},"authError","면 ",[26,466,467],{},"\u003CNuxtPage\u002F>"," 대신 ",[21,470,471,474],{},[26,472,473],{},"AuthErrorPage","만 렌더"," → 어떤 페이지 콘텐츠·버튼도 노출 안 됨.",[192,477,478,480,481],{},[26,479,473],{}," — \"접근할 수 없습니다 \u002F 로그인 정보가 없거나 유효하지 않습니다\" + 사유 텍스트. ",[21,482,483],{},"버튼·링크·클릭 요소 전무.",[192,485,486],{},"7개 페이지의 \"인증 후 … 불러오기\" 배너·버튼 전량 삭제.",[14,488,490],{"id":489},"_6-검증","6. 검증",[189,492,493,503,524,540],{},[192,494,495,498,499,502],{},[21,496,497],{},"해시 재현",": 실제 PMS가 만든 ek 2건(프로젝트 1548 \u002F 게시글 93710)을 서버 검증 로직으로 재현 → ",[21,500,501],{},"정확히 일치·수락 확인"," 후 구현.",[192,504,505,508,509,512,513,516,517,520,521,523],{},[21,506,507],{},"실서버 E2E",": ek 교환 ",[26,510,511],{},"{ok:true, token}"," ✓ · 무토큰 GET ",[21,514,515],{},"401"," ✓ · Bearer GET ",[21,518,519],{},"200"," ✓ · 위조 ek ",[21,522,515],{}," ✓",[192,525,526,529,530,533,534,457,537,523],{},[21,527,528],{},"CORS",": 애드온 오리진 반사·",[26,531,532],{},"Authorization"," 프리플라이트 허용 ✓ \u002F ",[26,535,536],{},"evil.pages.dev",[21,538,539],{},"차단",[192,541,542],{},"타입체크 회귀 0(api 11=베이스라인) · pms 빌드 성공.",[84,544],{},[14,546,547],{"id":547},"배포",[549,550,552,553,555],"h3",{"id":551},"_1402-malgn-helper-api-cloudflare-workers","14:02 — ",[26,554,279],{}," → Cloudflare Workers",[189,557,558,565,568],{},[192,559,560,561,564],{},"커밋: ",[26,562,563],{},"0dad619"," (신규 커밋: yes)",[192,566,567],{},"메시지: merge: \u002Fauth\u002Fpms-sso — PMS 전용 ek 검증 라우트",[192,569,570,571,574,575,577],{},"Version ID: ",[26,572,573],{},"df122290"," · ",[26,576,76],{}," 가드 + CORS 한정 동시 활성",[549,579,581,582,584],{"id":580},"_1408-malgn-helper-pms-cloudflare-pages","14:08 — ",[26,583,372],{}," → Cloudflare Pages",[189,586,587,592,598],{},[192,588,560,589,564],{},[26,590,591],{},"d694a0f",[192,593,594,595],{},"메시지: merge: ek pid 추출 \u002Fposts\u002F",[227,596,597],{}," 확장",[192,599,600,601],{},"Deployment: ",[26,602,603],{},"9c4a1e8f",[549,605,607,608,584],{"id":606},"_1443-malgn-helper-pms-cloudflare-pages","14:43 — ",[26,609,372],{},[189,611,612,617,620],{},[192,613,560,614,564],{},[26,615,616],{},"ccb61de",[192,618,619],{},"메시지: merge: 전역 인증 오류 게이트 + 인증 배너\u002F버튼 제거",[192,621,600,622],{},[26,623,624],{},"108f65f3",[84,626],{},[14,628,630],{"id":629},"참여-활동내역","참여 활동내역",[91,632,633,643],{},[94,634,635],{},[97,636,637,640],{},[100,638,639],{},"담당",[100,641,642],{},"수행 내용",[107,644,645,656,674,693],{},[97,646,647,650],{},[112,648,649],{},"가온님 (총괄)",[112,651,652,653,655],{},"보안 진단·설계(Access→ek 전환 결정)·오케스트레이션·리뷰 판정·해시 재현 검증·배포·E2E 검증. ",[26,654,28],{}," 가드 직접 반영.",[97,657,658,661],{},[112,659,660],{},"바탕님 (백엔드 개발자)",[112,662,663,665,666,294,668,670,671,673],{},[26,664,355],{},"(초기안)·",[26,667,358],{},[26,669,64],{}," 라우트 · ",[26,672,76],{}," 18개 라우트 가드 · Access 데드코드 제거",[97,675,676,679],{},[112,677,678],{},"나래님 (프론트엔드 개발자)",[112,680,681,684,685,687,688,690,691],{},[26,682,683],{},"useSession"," 컴포저블(팝업→ek 사일런트 교환) · 10개 파일 ",[26,686,408],{}," 전환 · ",[26,689,428],{}," 패스스루 · 전역 인증 오류 게이트 + ",[26,692,473],{},[97,694,695,698],{},[112,696,697],{},"미쁨님 (보안 검토자)",[112,699,700,701,706],{},"라우트 가드·토큰 발급·브리지 XSS·origin 검증 리뷰. ",[21,702,703,705],{},[26,704,28],{}," 무인증 쓰기 발견","(계획 누락분) · 최종 전체 브랜치 리뷰(머지 승인)",[708,709,710],"blockquote",{},[18,711,712,713,716,717,720,721,724],{},"리뷰에서 잡아 고친 실결함: ",[26,714,715],{},"useSession._inflight","가 팝업 차단 시 영구 잠김(Critical) · ",[26,718,719],{},"init.headers"," 캐스팅으로 헤더 유실(Important) · ",[26,722,723],{},"eval.vue"," 마운트 자동 로드 누락 · watch 토큰 가드 누락.",[84,726],{},[14,728,730],{"id":729},"다음-작업-후보-알려진-한계","다음 작업 후보 \u002F 알려진 한계",[189,732,733,754,775,781,793,802],{},[192,734,735,738,739,742,743,745,746,749,750,753],{},[21,736,737],{},"서비스 토큰 ENFORCE"," — 현재 ",[26,740,741],{},"SERVICE_TOKEN_ENFORCE=\"0\"","(관찰). 생성 라우트는 ",[26,744,76],{},"로 이미 보호되나, 서버-서버 이중 증명까지 원하면 ",[26,747,748],{},"PMS_SERVICE_TOKEN"," secret 설정 → 관찰 로그 확인 → ",[26,751,752],{},"\"1\"","로 전환.",[192,755,756,291,759,762,763,766,767,770,771,774],{},[21,757,758],{},"임베드 하드닝",[26,760,761],{},"frame-ancestors"," CSP(PMS 오리진만 iframe 허용) · 모달 close ",[26,764,765],{},"postMessage","의 ",[26,768,769],{},"targetOrigin"," 고정(현재 ",[26,772,773],{},"\"*\"",").",[192,776,777,780],{},[21,778,779],{},"감사 로그"," — actor(email) 기준 생성·삭제 추적.",[192,782,783,788,789,792],{},[21,784,785,787],{},[26,786,46],{}," 특성상 한계"," — URL·PMS 페이지 소스·리퍼러에 남는 ",[21,790,791],{},"당일 유효 토큰","이며 salt가 소스에 있어 강한 비밀은 아니다(기존 맑은오피스 SSO와 동일 특성). \"PMS 로그인 사용자 = 애드온 허용\" + 리소스 바인딩 + 당일 회전 수준의 게이트.",[192,794,795,801],{},[21,796,797,800],{},[26,798,799],{},"posts\u002F[id]"," 빈 화면 갭"," — 인증 정상인데 게시물이 null인 극단 케이스에 빈 상태 UI가 없음(배너 도입 이전부터 존재한 기존 갭, 회귀 아님).",[192,803,804,805,812],{},"설계 정본: ",[806,807,809],"a",{"href":808},"..\u002F..\u002F..\u002Fmalgn-helper-pms\u002Fdocs\u002Fsuperpowers\u002Fspecs\u002F2026-06-30-pms-security-session-design",[26,810,811],{},"malgn-helper-pms\u002Fdocs\u002Fsuperpowers\u002Fspecs\u002F2026-06-30-pms-security-session-design.md"," (§11에 Access→ek 전환 부록)",{"title":220,"searchDepth":814,"depth":814,"links":815},3,[816,818,819,821,823,825,826,827,835,836],{"id":16,"depth":817,"text":16},2,{"id":88,"depth":817,"text":89},{"id":183,"depth":817,"text":820},"2. 설계 결정 — Cloudflare Access → ek SSO 전환",{"id":275,"depth":817,"text":822},"3. API 구현 (malgn-helper-api)",{"id":368,"depth":817,"text":824},"4. 애드온 구현 (malgn-helper-pms)",{"id":439,"depth":817,"text":440},{"id":489,"depth":817,"text":490},{"id":547,"depth":817,"text":547,"children":828},[829,831,833],{"id":551,"depth":814,"text":830},"14:02 — malgn-helper-api → Cloudflare Workers",{"id":580,"depth":814,"text":832},"14:08 — malgn-helper-pms → Cloudflare Pages",{"id":606,"depth":814,"text":834},"14:43 — malgn-helper-pms → Cloudflare Pages",{"id":629,"depth":817,"text":630},{"id":729,"depth":817,"text":730},"md",{},true,"\u002Fhistory\u002Fhistory.20260701",{"title":5,"description":220},"history\u002Fhistory.20260701","V88aq8vi02_h04XPOPZz-BjeTLMuvqDLekgEGSyZNIs",1783932938623]