From e8c1acba9fd5416861956313c841f53778c1b412 Mon Sep 17 00:00:00 2001 From: Your Name Date: Sun, 31 May 2026 02:06:42 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=AE=A1=E8=AE=A18=E4=B8=AAFINDING?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D=20(IDE=E7=BC=96=E8=BE=91=E5=99=A8+=E7=BB=88?= =?UTF-8?q?=E7=AB=AF)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - H-01 [MEDIUM] esc() 加引号转义('"\') 防止onclick XSS - H-02 [MEDIUM] initialPath 路径白名单校验(仅允许字母数字/.-_) - H-03 [LOW] doNewFile 文件名白名单正则(替代简单的includes('/')) - H-08 [LOW] Monaco CDN SRI — 已知限制, 版本锁定@0.45.0 - H-13 [MEDIUM] showModal 移除已删除的modalTextarea引用 - H-20 [LOW] initialPath仅首次连接cd, 不影响恢复的标签 - H-26 [MEDIUM] esc()跨文件一致性 — files.html对齐terminal.html - H-30 [LOW] esc()注释更新 --- web/app/files.html | 11 ++++------- web/app/terminal.html | 10 +++++++--- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/web/app/files.html b/web/app/files.html index 434c0502..1cf225fd 100644 --- a/web/app/files.html +++ b/web/app/files.html @@ -285,7 +285,7 @@ showModal({title:'📄 新建文件',input:true,inputDefault:'',confirmText:'创建', onConfirm:async(name)=>{ if(!name)return; - if(name.includes('/')){toast('error','文件名不能包含 /');return} + if(!/^[a-zA-Z0-9._\-]+$/.test(name)){toast('error','文件名只能包含字母、数字、点、下划线、连字符');return} const path=_currentPath.replace(/\/$/,'')+'/'+name; const r=await apiFetch(API+'/sync/write-file',{method:'POST',headers:apiHeadersJSON(),body:JSON.stringify({server_id:_currentServerId,path,content:''})}); if(r&&r.ok){toast('success',`已创建: ${name}`);browseDir(_currentPath)} @@ -315,8 +315,8 @@ let _filterExt=''; function filterByExt(ext){_filterExt=ext;renderFileList(_allEntries)} - /** H-29: HTML-escape a string for safe innerHTML insertion */ - function esc(s){if(!s)return'';const d=document.createElement('div');d.textContent=s;return d.innerHTML} + /** HTML-escape for safe innerHTML + onclick attribute insertion */ + function esc(s){if(!s)return'';const d=document.createElement('div');d.textContent=s;return d.innerHTML.replace(/"/g,'"').replace(/'/g,''')} // ── Open SSH terminal to current server/path ── function openTerminal(){ @@ -335,18 +335,15 @@ // ── Modal system (P2-9) ── let _modalResolve=null; - function showModal({title,text,input,textarea,inputDefault,textareaValue,confirmText,danger,onConfirm}){ + function showModal({title,text,input,inputDefault,confirmText,danger,onConfirm}){ document.getElementById('modalTitle').textContent=title; const textEl=document.getElementById('modalText'); const inputEl=document.getElementById('modalInput'); - const textareaEl=document.getElementById('modalTextarea'); const confirmBtn=document.getElementById('modalConfirm'); textEl.classList.toggle('hidden',!text); textEl.textContent=text||''; inputEl.classList.toggle('hidden',!input); if(input){inputEl.value=inputDefault||'';setTimeout(()=>inputEl.focus(),50)} - textareaEl.classList.toggle('hidden',!textarea); - if(textarea){textareaEl.value=textareaValue||'';setTimeout(()=>textareaEl.focus(),50)} confirmBtn.textContent=confirmText||'确认'; confirmBtn.className=`px-4 py-2 text-white text-sm rounded-lg transition ${danger?'bg-red-600 hover:bg-red-700':'bg-brand hover:bg-brand-dark'}`; document.getElementById('modal').classList.remove('hidden'); diff --git a/web/app/terminal.html b/web/app/terminal.html index 17f375fe..c1cccc7d 100644 --- a/web/app/terminal.html +++ b/web/app/terminal.html @@ -119,7 +119,10 @@ initLayout('servers'); // ── Parse params ── const params = new URLSearchParams(window.location.search); const initialServerId = parseInt(params.get('server_id')); -const initialPath = params.get('path') || ''; // Auto-cd after connect +const _rawPath = params.get('path') || ''; +// H-02: Strict path whitelist — only allow safe characters +const initialPath = _rawPath && /^[a-zA-Z0-9/._\-]+$/.test(_rawPath) ? _rawPath : ''; +let _initialCdSent = false; // H-20: Only cd on first connection, not restored tabs if (!initialServerId) window.location.href = '/app/servers.html'; // ── localStorage keys ── @@ -358,8 +361,9 @@ async function connectSession(idx) { if (d && s.ws && s.ws.readyState === WebSocket.OPEN) { s.ws.send(JSON.stringify({ type: 'RESIZE', cols: d.cols, rows: d.rows })); } - // Auto-cd to initial path if specified in URL - if (initialPath && s.ws && s.ws.readyState === WebSocket.OPEN) { + // H-20: Auto-cd to initial path only on first connection (not restored tabs) + if (initialPath && !_initialCdSent && s.ws && s.ws.readyState === WebSocket.OPEN) { + _initialCdSent = true; setTimeout(() => { s.ws.send(JSON.stringify({ type: 'DATA', data: `cd ${initialPath}\r` })); }, 200);