fix: 작업실 앱 리뷰 HIGH·XSS 수정 5건
- 새 프로젝트 생성 직전 syncLocalToProject — debounce 미완료 편집 유실 방지 - softRefreshWorkshop: 활성 프로젝트가 서버쪽에서 바뀐 경우에만 clearStlSelection/ refreshChatForProject/loadFiles — 채팅에서 전환 시 옛 STL이 슬라이서로 오전송되던 문제 - beforeunload/pagehide에서 debounce 저장 flush(keepalive fetch) — 600ms 안에 탭 닫으면 마지막 편집이 사라지던 문제 - XSS: escJs에 "·<·> 이스케이프 추가(속성 경계 탈출 차단), chatGreeting 프로젝트명/ 모델변경 실패 메시지 escHtml, 부품 참고링크 safeUrl로 javascript:/data: 스킴 차단 Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -316,7 +316,7 @@ function escAttr(s){return String(s||'').replace(/'/g,''').replace(/"/g,'&qu
|
||||
// onclick="fn('...')" 안의 JS 문자열 컨텍스트용 — escAttr의 HTML 엔티티(')는
|
||||
// HTML 파서가 속성값을 JS로 넘기기 전에 다시 ' 로 디코딩해버려 따옴표 주입을 못 막으므로
|
||||
// JS 수준에서 이스케이프한다. 폴더/파일 이름(사용자 입력)을 싣는 데만 쓴다.
|
||||
function escJs(s){return String(s??'').replace(/\\/g,'\\\\').replace(/'/g,"\\'").replace(/<\//g,'<\\/').replace(/\r?\n/g,'\\n');}
|
||||
function escJs(s){return String(s??'').replace(/\\/g,'\\\\').replace(/'/g,"\\'").replace(/"/g,'"').replace(/</g,'<').replace(/>/g,'>').replace(/\r?\n/g,'\\n');}
|
||||
function genId(prefix){return prefix+'_'+Math.random().toString(36).slice(2,10);}
|
||||
|
||||
// ── 상태 ──────────────────────────────────────────────────────────────────────
|
||||
@@ -395,6 +395,10 @@ function switchProject(id){
|
||||
}
|
||||
|
||||
function createProjectPrompt(){
|
||||
// 새 프로젝트로 넘어가기 전 지금 편집 중인 내용을 직전 프로젝트에 반영 —
|
||||
// 안 그러면 loadLocalFromProject()가 지역변수를 새 빈 프로젝트로 덮어써서
|
||||
// 600ms debounce 안 끝난 편집이 유실된다(switchProject와 동일 순서).
|
||||
syncLocalToProject();
|
||||
const name=(prompt('새 프로젝트 이름을 입력하세요:')||'').trim();
|
||||
if(!name)return;
|
||||
const id=genId('proj');
|
||||
@@ -446,6 +450,19 @@ function scheduleSave(){
|
||||
saveDebounce=setTimeout(saveProject,600);
|
||||
}
|
||||
|
||||
// 탭 닫기/새로고침 시 대기 중인 저장을 즉시 flush — 600ms debounce가 끝나기 전에
|
||||
// 닫아버리면 마지막 편집이 그대로 사라진다. unload 중엔 일반 fetch가 버려질 수 있어
|
||||
// keepalive:true로 브라우저에 전송을 보장받는다. beforeunload(닫기 직전)와
|
||||
// pagehide(실제 내비게이션 확정) 둘 다에 걸고, saveDebounce 널 처리로 이중 실행을 막는다.
|
||||
function flushPendingSave(){
|
||||
if(!saveDebounce)return;
|
||||
clearTimeout(saveDebounce);saveDebounce=null;
|
||||
syncLocalToProject();
|
||||
fetch('/api/workshop/data',{method:'PUT',headers:authH({'Content-Type':'application/json'}),body:JSON.stringify({projects:workshopData.projects,activeProjectId:workshopData.activeProjectId}),keepalive:true});
|
||||
}
|
||||
window.addEventListener('beforeunload',flushPendingSave);
|
||||
window.addEventListener('pagehide',flushPendingSave);
|
||||
|
||||
// 채팅 응답 후 호출: 서버 데이터를 다시 당겨오되, 내용이 안 바뀌었으면 화면을 건드리지
|
||||
// 않는다(편집 중 포커스/펼침 카드 보존). 바뀌었으면 살아있는 부품에 한해 펼침 상태를
|
||||
// 유지한 채 다시 그린다. loadWorkshopData(초기 로딩용, 무조건 전체 새로고침)와 다른 용도.
|
||||
@@ -460,6 +477,7 @@ async function softRefreshWorkshop(){
|
||||
const newProjects=Array.isArray(d.projects)?d.projects:[];
|
||||
const newActive=d.activeProjectId||(newProjects[0]?.id||'');
|
||||
if(JSON.stringify(newProjects)===JSON.stringify(workshopData.projects)&&newActive===workshopData.activeProjectId)return;
|
||||
const activeChanged=newActive!==workshopData.activeProjectId;
|
||||
workshopData.projects=newProjects;
|
||||
workshopData.activeProjectId=newActive;
|
||||
loadLocalFromProject();
|
||||
@@ -473,6 +491,15 @@ async function softRefreshWorkshop(){
|
||||
const ota=document.getElementById('overview-textarea');
|
||||
if(ota&&ota.value!==overviewDesc)ota.value=overviewDesc;
|
||||
updateBudget();
|
||||
if(activeChanged){
|
||||
// 채팅에서 set_active_project/create_project로 프로젝트가 바뀐 경우 —
|
||||
// switchProject와 같은 정리를 해줘야 한다. 안 그러면 옛 프로젝트에서
|
||||
// 선택해둔 STL이 그대로 남아 슬라이서에 잘못 전송되고, 채팅창도 옛
|
||||
// 프로젝트의 세션 기록을 보여준다. 매 응답마다가 아니라 실제 전환 시에만.
|
||||
clearStlSelection();
|
||||
refreshChatForProject();
|
||||
if(document.getElementById('pane-files').classList.contains('active'))loadFiles();
|
||||
}
|
||||
}catch{}
|
||||
}
|
||||
|
||||
@@ -671,7 +698,7 @@ function renderParts(){
|
||||
<div class="rb-link-row" data-link-id="${escAttr(link.id)}">
|
||||
<input type="text" class="rb-link-label" placeholder="라벨(예: 쿠팡)" value="${escAttr(link.label)}" oninput="updateLink('${p.id}','${link.id}','label',this.value)">
|
||||
<input type="text" placeholder="URL" value="${escAttr(link.url)}" oninput="updateLink('${p.id}','${link.id}','url',this.value)">
|
||||
${link.url?`<a class="rb-link-open" href="${escAttr(link.url)}" target="_blank" rel="noopener">↗</a>`:''}
|
||||
${(safeUrl(link.url)?`<a class="rb-link-open" href="${escAttr(safeUrl(link.url))}" target="_blank" rel="noopener">↗</a>`:'')}
|
||||
<button class="rb-del-btn" onclick="deleteLink('${p.id}','${link.id}')">✕</button>
|
||||
</div>
|
||||
`).join('')}
|
||||
@@ -1101,6 +1128,11 @@ async function loadHistory(){
|
||||
|
||||
function escHtml(s){return String(s??'').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));}
|
||||
|
||||
// 부품 참고링크 href용 — http(s)(프로토콜 상대 포함)만 통과시키고 나머지는 빈 값으로
|
||||
// 뭉갠다. javascript:/data: 스킴 링크는 채팅 도구(add_link)나 오타로도 들어올 수 있고
|
||||
// href에 그대로 쓰면 클릭 한 번에 스크립트가 돈다.
|
||||
function safeUrl(u){u=String(u??'').trim();return /^(https?:|\/\/)/i.test(u)?u:'';}
|
||||
|
||||
function addMsg(role,html){
|
||||
const msgs=document.getElementById('chat-msgs');
|
||||
const div=document.createElement('div');div.className='rb-msg '+role;
|
||||
@@ -1142,7 +1174,9 @@ function renderMd(text){
|
||||
}
|
||||
|
||||
function chatGreeting(){
|
||||
return '🛠️ 작업실 채팅입니다. 현재 프로젝트: "'+(currentProject()?.name||'')+'". 부품 구매/상태 변경, 작업 완료 체크, 예산 확인, 새 프로젝트 생성 등을 물어보세요.';
|
||||
// 프로젝트 이름은 addMsg→innerHTML로 들어가므로 반드시 이스케이프 — 이름에
|
||||
// <script>/태그 문자가 섞이면(예: 채팅으로 지은 프로젝트 이름) 채팅창에서 실행된다.
|
||||
return '🛠️ 작업실 채팅입니다. 현재 프로젝트: "'+escHtml(currentProject()?.name||'')+'". 부품 구매/상태 변경, 작업 완료 체크, 예산 확인, 새 프로젝트 생성 등을 물어보세요.';
|
||||
}
|
||||
|
||||
// 프로젝트 전환(수동/생성/삭제) 시 채팅창을 그 프로젝트의 세션 기록으로 리셋한다 —
|
||||
@@ -1278,7 +1312,7 @@ async function selectModel(name){
|
||||
_modelDropdownLoaded=false;
|
||||
}catch(e){
|
||||
btn.textContent=prev;
|
||||
addMsg('system','모델 변경 실패: '+e.message);
|
||||
addMsg('system','모델 변경 실패: '+escHtml(e.message));
|
||||
}
|
||||
}
|
||||
document.addEventListener('click',(e)=>{
|
||||
|
||||
Reference in New Issue
Block a user