Skip to content

Commit 4bb8e38

Browse files
zzmlbclaude
andcommitted
fix: 安全加固+性能优化+边界修复
- /api/label: 使用 readBody 限制请求体 + safeWindowId 防注入 + label 长度限制100 - setBoardColor: 颜色白名单校验,防止 CSS 注入 - confirmBoardTitle: 长度限制100 + 成功后同步 data-original - cancelBoardEditTitle: 恢复输入框原始值,防止空值残留 - launchSSHPresets: getWindows 调用从2N次优化为2次 - renderLayouts: displayCols 除零保护 + displayRows 修正 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 620666d commit 4bb8e38

1 file changed

Lines changed: 35 additions & 27 deletions

File tree

lib/panel.js

Lines changed: 35 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -400,6 +400,12 @@ function launchSSHPresets(presetIds) {
400400
const launched = [];
401401
const presetWindowMap = {};
402402

403+
// 启动前记录一次窗口集合(避免每个预设都调用 AppleScript)
404+
let beforeIds;
405+
try {
406+
beforeIds = new Set(getWindows().map(w => w.id));
407+
} catch { beforeIds = new Set(); }
408+
403409
for (const pid of presetIds) {
404410
const preset = presets[pid];
405411
if (!preset) continue;
@@ -409,12 +415,6 @@ function launchSSHPresets(presetIds) {
409415
const dir = (preset.directory || '').replace(/'/g, "'\\''");
410416
const cmd = (preset.command || '').replace(/'/g, "'\\''");
411417

412-
// 记录启动前的窗口 ID 集合
413-
let beforeIds;
414-
try {
415-
beforeIds = new Set(getWindows().map(w => w.id));
416-
} catch { beforeIds = new Set(); }
417-
418418
if (conn.type === 'local') {
419419
let localCmd = '';
420420
if (dir && cmd) localCmd = `cd '${dir}' && ${cmd}`;
@@ -454,18 +454,21 @@ function launchSSHPresets(presetIds) {
454454
} catch {}
455455
}
456456

457-
// 启动后找出新增的窗口 ID
458-
try {
459-
const afterWindows = getWindows();
460-
for (const w of afterWindows) {
461-
if (!beforeIds.has(w.id)) {
462-
presetWindowMap[pid] = w.id;
463-
break;
464-
}
465-
}
466-
} catch {}
467457
}
468458

459+
// 启动后统一获取一次新窗口列表,找出所有新增窗口
460+
try {
461+
const afterWindows = getWindows();
462+
const newWindowIds = afterWindows.filter(w => !beforeIds.has(w.id)).map(w => w.id);
463+
// 按顺序将新窗口分配给各预设
464+
let ni = 0;
465+
for (const pid of presetIds) {
466+
if (ni < newWindowIds.length && launched.includes(presets[pid]?.name)) {
467+
presetWindowMap[pid] = newWindowIds[ni++];
468+
}
469+
}
470+
} catch {}
471+
469472
return { ok: true, launched, presetWindowMap };
470473
}
471474

@@ -954,8 +957,8 @@ function renderLayouts(wc){
954957
var maxCells=Math.min(wc,12);
955958
var cells='';for(var i=0;i<maxCells;i++)cells+='<div class="layout-cell">'+(NUM[i]||i+1)+'</div>';
956959
if(wc>12)cells+='<div class="layout-cell">+' +(wc-12)+'</div>';
957-
var displayCols=wc>12?Math.min(cols,cols):cols;
958-
var displayRows=wc>12?Math.ceil((maxCells+1)/displayCols):rows;
960+
var displayCols=cols||1;
961+
var displayRows=wc>12?Math.ceil((maxCells+1)/displayCols):Math.ceil(wc/displayCols);
959962
var gridH=Math.max(48,Math.min(80,displayRows*16));
960963
return '<div class="layout-card'+act+'" onclick="selectLayout(\\''+l.id+'\\','+l.cols+')"><div class="layout-grid" style="grid-template-columns:repeat('+displayCols+',1fr);height:'+gridH+'px">'+cells+'</div><div class="layout-name">'+l.name+(l.cols?' ('+rows+'x'+cols+')':'')+'</div></div>';
961964
}).join('')+'<div class="layout-actions"><button class="btn primary" onclick="applyLayout()" '+(selectedLayout?'':'disabled')+'>应用排布</button><button class="btn" onclick="bringToFront()">显示终端</button></div>';
@@ -1601,7 +1604,7 @@ function renderBoard(){
16011604
var colorBar='<div class="bn-color-bar" style="background:'+color+'">';
16021605
BOARD_COLORS.forEach(function(c){colorBar+='<div class="bn-cswatch" style="background:'+c+'" onclick="event.stopPropagation();setBoardColor(\\''+escA(w.id)+'\\',\\''+c+'\\')"></div>'});
16031606
colorBar+='</div>';
1604-
html+='<div class="board-node state-'+sc+'" data-winid="'+escA(w.id)+'" style="left:'+x+'px;top:'+y+'px;border-left:4px solid '+color+'" onmousedown="startNodeDrag(event,\\''+escA(w.id)+'\\')" ondblclick="onBoardNodeDblClick(event,\\''+escA(w.id)+'\\')"><div class="bn-header"><span class="bn-idx">'+(NUM[si-1]||si)+'</span><input class="bn-title" value="'+escA(titleText)+'" data-winid="'+escA(w.id)+'" readonly ondblclick="event.stopPropagation();startBoardEditTitle(this)" onkeydown="onBoardTitleKey(event,this)" onblur="cancelBoardEditTitle(this)"><span class="bn-dot dot '+dotCls+'"></span><span class="bn-stag '+sc+'">'+stLabel+'</span></div><div class="bn-preview">'+preview+'</div>'+colorBar+'</div>';
1607+
html+='<div class="board-node state-'+sc+'" data-winid="'+escA(w.id)+'" style="left:'+x+'px;top:'+y+'px;border-left:4px solid '+color+'" onmousedown="startNodeDrag(event,\\''+escA(w.id)+'\\')" ondblclick="onBoardNodeDblClick(event,\\''+escA(w.id)+'\\')"><div class="bn-header"><span class="bn-idx">'+(NUM[si-1]||si)+'</span><input class="bn-title" value="'+escA(titleText)+'" data-winid="'+escA(w.id)+'" data-original="'+escA(titleText)+'" readonly ondblclick="event.stopPropagation();startBoardEditTitle(this)" onkeydown="onBoardTitleKey(event,this)" onblur="cancelBoardEditTitle(this)"><span class="bn-dot dot '+dotCls+'"></span><span class="bn-stag '+sc+'">'+stLabel+'</span></div><div class="bn-preview">'+preview+'</div>'+colorBar+'</div>';
16051608
usedPositions[w.id]={x:x,y:y};
16061609
});
16071610
@@ -1802,6 +1805,7 @@ function resetBoardLayout(){
18021805
18031806
// --- 底部色条颜色选择 ---
18041807
function setBoardColor(winId,color){
1808+
if(BOARD_COLORS.indexOf(color)<0)return;
18051809
var node=document.querySelector('.board-node[data-winid="'+winId+'"]');
18061810
if(!node)return;
18071811
if(!boardData.nodes[winId])boardData.nodes[winId]={x:parseInt(node.style.left)||0,y:parseInt(node.style.top)||0};
@@ -1819,16 +1823,18 @@ function startBoardEditTitle(inp){
18191823
inp.select();
18201824
}
18211825
function confirmBoardTitle(inp){
1822-
var label=inp.value.trim();
1826+
var label=inp.value.trim().slice(0,100);
18231827
if(!label){cancelBoardEditTitle(inp);return}
18241828
inp.readOnly=true;
1829+
inp.dataset.original=label;
18251830
var winId=inp.dataset.winid;
18261831
fetch('/api/label',{method:'POST',headers:{'Content-Type':'application/json'},
18271832
body:JSON.stringify({windowId:winId,label:label})
18281833
}).catch(function(){});
18291834
}
18301835
function cancelBoardEditTitle(inp){
18311836
if(inp.readOnly)return;
1837+
inp.value=inp.dataset.original||'';
18321838
inp.readOnly=true;
18331839
}
18341840
function onBoardTitleKey(e,inp){
@@ -1980,18 +1986,20 @@ export async function startPanel(port) {
19801986
}
19811987

19821988
if (url.pathname === '/api/label' && req.method === 'POST') {
1983-
let body = '';
1984-
req.on('data', d => body += d);
1985-
req.on('end', async () => {
1989+
readBody(req, async (body) => {
19861990
try {
19871991
const { windowId, label, stableIndex, index } = JSON.parse(body);
1992+
// 安全:校验 windowId + 限制 label 长度
1993+
const safeLabel = String(label || '').slice(0, 100);
1994+
if (!safeLabel) { res.writeHead(400); res.end('empty label'); return; }
19881995
if (windowId) {
1989-
// 优先使用窗口 ID(稳定,不受焦点影响)
1990-
await labelWindowById(windowId, label, stableIndex || 1);
1996+
const wid = safeWindowId(windowId);
1997+
if (!wid) { res.writeHead(400); res.end('invalid windowId'); return; }
1998+
await labelWindowById(wid, safeLabel, stableIndex || 1);
19911999
} else {
1992-
await labelWindow(index, label, { displayIndex: stableIndex || index });
2000+
await labelWindow(index, safeLabel, { displayIndex: stableIndex || index });
19932001
}
1994-
_statusCache = null; // 清缓存让前端立即看到新标签
2002+
_statusCache = null;
19952003
res.writeHead(200, { 'Content-Type': 'application/json' });
19962004
res.end(JSON.stringify({ ok: true }));
19972005
} catch { res.writeHead(400); res.end('bad request'); }

0 commit comments

Comments
 (0)