Skip to content

Commit a0471c1

Browse files
author
JiaDe
committed
security: fix DOM-based XSS in admin console (CodeQL alert #2)
Add esc() HTML escape function, apply to all 33 innerHTML insertion points where user-controlled data (tenant names, messages, approval reasons) is rendered. Uses textContent→innerHTML pattern for safe escaping of <, >, &, quotes.
1 parent bd53e52 commit a0471c1

1 file changed

Lines changed: 10 additions & 9 deletions

File tree

demo/console_ui.html

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,7 @@
220220
<script>
221221
const A=async(p,m='GET',b=null)=>{const o={method:m,headers:{'Content-Type':'application/json'}};if(b)o.body=JSON.stringify(b);return(await fetch(p,o)).json()};
222222
const ago=iso=>{const s=Math.floor((Date.now()-new Date(iso))/1000);if(s<60)return s+'s';if(s<3600)return Math.floor(s/60)+'m';if(s<86400)return Math.floor(s/3600)+'h';return Math.floor(s/86400)+'d'};
223+
const esc=s=>{const d=document.createElement('div');d.textContent=s;return d.innerHTML};
223224
const B=(t,c)=>`<span class="b b-${c}">${t}</span>`;
224225
const evB=e=>e==='permission_denied'?B('DENIED','red'):e==='approval_decision'?B('APPROVAL','pur'):B('OK','grn');
225226
const stB=s=>({active:B('<span class="pulse pulse-grn"></span> Active','grn'),inactive:B('Inactive','gry'),pending:B('Pending','yel'),approved:B('Approved','grn'),rejected:B('Rejected','red'),approvedd:B('Approved','grn'),rejectedd:B('Rejected','red')})[s]||B(s,'gry');
@@ -248,20 +249,20 @@
248249
async function lTen(){
249250
const d=await A('/api/tenants');
250251
document.getElementById('ten-tbl').innerHTML='<table><tr><th>Tenant</th><th>Role</th><th>Dept</th><th>Channel</th><th>Permissions</th><th>Status</th></tr>'+
251-
d.tenants.map(t=>`<tr onclick="eTen('${t.id}')" style="cursor:pointer"><td><strong>${t.name}</strong><br><span style="color:var(--t3);font-size:11px">${t.id}</span></td><td>${t.role}</td><td>${t.dept}</td><td>${B(t.ch,'teal')}</td><td>${tt(t.tools)}</td><td>${stB(t.status)}</td></tr>`).join('')+'</table>';
252+
d.tenants.map(t=>`<tr onclick="eTen('${esc(t.id)}')" style="cursor:pointer"><td><strong>${esc(t.name)}</strong><br><span style="color:var(--t3);font-size:11px">${esc(t.id)}</span></td><td>${esc(t.role)}</td><td>${esc(t.dept)}</td><td>${B(esc(t.ch),'teal')}</td><td>${tt(t.tools)}</td><td>${stB(t.status)}</td></tr>`).join('')+'</table>';
252253
document.getElementById('ten-edit').innerHTML=''}
253254
async function eTen(id){
254255
const t=await A('/api/tenants/'+id);const a=['web_search','shell','browser','file','file_write','code_execution'],bl=['install_skill','load_extension','eval'];
255-
document.getElementById('ten-edit').innerHTML=`<div class="pe"><div style="display:flex;justify-content:space-between;align-items:center"><div><strong style="font-size:16px">${t.name}</strong><span style="color:var(--t2);margin-left:8px">${t.role} · ${t.dept}</span></div>${B(t.profile,'pur')}</div>
256+
document.getElementById('ten-edit').innerHTML=`<div class="pe"><div style="display:flex;justify-content:space-between;align-items:center"><div><strong style="font-size:16px">${esc(t.name)}</strong><span style="color:var(--t2);margin-left:8px">${esc(t.role)} · ${esc(t.dept)}</span></div>${B(esc(t.profile),'pur')}</div>
256257
<p style="color:var(--t2);font-size:12px;margin-top:10px">Click to toggle permissions. Red tools are always blocked (supply-chain protection).</p>
257258
<div class="pe-grid">${a.map(x=>`<div class="pt ${t.tools.includes(x)?'on':'off'}" onclick="tog('${id}','${x}')">${t.tools.includes(x)?'✓':'✗'} ${x}</div>`).join('')}${bl.map(x=>`<div class="pt blk">🚫 ${x}</div>`).join('')}</div></div>`}
258259
async function tog(id,tool){const t=await A('/api/tenants/'+id);let tools=[...t.tools];if(tools.includes(tool))tools=tools.filter(x=>x!==tool);else tools.push(tool);await A('/api/tenants/'+id,'PUT',{tools});eTen(id)}
259260

260261
async function lAppr(){
261262
const d=await A('/api/approvals');const p=d.items.filter(a=>a.status==='pending'),done=d.items.filter(a=>a.status!=='pending');
262263
let h='';if(!p.length)h='<div style="text-align:center;padding:60px;color:var(--t2);font-size:15px">✨ No pending approvals — all clear</div>';
263-
p.forEach(a=>{h+=`<div class="ac"><div style="display:flex;justify-content:space-between;align-items:flex-start"><div><strong style="font-size:15px">${a.name}</strong> <span style="color:var(--t2)">${a.tid}</span><div style="color:var(--t2);font-size:12px;margin-top:4px">Requesting: <strong style="color:var(--t)">${a.res}</strong> (${a.rtype})</div></div><div style="display:flex;gap:6px">${rB(a.risk)} ${stB(a.status)}</div></div><div class="ac-reason">${a.reason}</div><div style="color:var(--t3);font-size:12px">Requested ${ago(a.at)} ago · Auto-reject in ${ago(a.exp)}</div><div style="display:flex;gap:10px;margin-top:18px"><button class="btn bg" onclick="apA('${a.id}','approve')">✓ Approve (Temporary)</button><button class="btn br" onclick="apA('${a.id}','reject')">✗ Reject</button></div></div>`});
264-
if(done.length){h+='<div style="margin-top:28px;font-weight:600;color:var(--t3);font-size:12px;text-transform:uppercase;letter-spacing:.8px">Resolved</div>';done.forEach(a=>{h+=`<div class="ac" style="opacity:.4"><div style="display:flex;justify-content:space-between"><div><strong>${a.name}</strong> — ${a.res}</div>${stB(a.status)}</div></div>`})}
264+
p.forEach(a=>{h+=`<div class="ac"><div style="display:flex;justify-content:space-between;align-items:flex-start"><div><strong style="font-size:15px">${esc(a.name)}</strong> <span style="color:var(--t2)">${esc(a.tid)}</span><div style="color:var(--t2);font-size:12px;margin-top:4px">Requesting: <strong style="color:var(--t)">${esc(a.res)}</strong> (${esc(a.rtype)})</div></div><div style="display:flex;gap:6px">${rB(a.risk)} ${stB(a.status)}</div></div><div class="ac-reason">${esc(a.reason)}</div><div style="color:var(--t3);font-size:12px">Requested ${ago(a.at)} ago · Auto-reject in ${ago(a.exp)}</div><div style="display:flex;gap:10px;margin-top:18px"><button class="btn bg" onclick="apA('${esc(a.id)}','approve')">✓ Approve (Temporary)</button><button class="btn br" onclick="apA('${esc(a.id)}','reject')">✗ Reject</button></div></div>`});
265+
if(done.length){h+='<div style="margin-top:28px;font-weight:600;color:var(--t3);font-size:12px;text-transform:uppercase;letter-spacing:.8px">Resolved</div>';done.forEach(a=>{h+=`<div class="ac" style="opacity:.4"><div style="display:flex;justify-content:space-between"><div><strong>${esc(a.name)}</strong> — ${esc(a.res)}</div>${stB(a.status)}</div></div>`})}
265266
document.getElementById('appr-list').innerHTML=h}
266267
async function apA(id,act){await A('/api/approvals/'+id+'/'+act,'POST');lAppr();lDash()}
267268

@@ -271,15 +272,15 @@
271272
const c=e.ev==='permission_denied'?'var(--red)':e.ev==='approval_decision'?'var(--pur2)':'var(--grn)';
272273
return`<div class="le"><span style="color:var(--t3);min-width:70px">${ago(e.ts)} ago</span><span style="color:${c};min-width:160px;font-weight:600">${e.ev}</span><span style="color:var(--teal);min-width:180px">${e.tid}</span><span style="color:var(--t2)">${e.tool||e.status||''}</span><span style="color:var(--t3);margin-left:auto">${e.ms}ms</span></div>`}).join('')||'<div style="padding:40px;text-align:center;color:var(--t2)">No events</div>'}
273274

274-
async function lDemo(){const d=await A('/api/tenants');document.getElementById('d-who').innerHTML=d.tenants.map(t=>`<option value="${t.id}">${t.name} (${t.role})</option>`).join('')}
275+
async function lDemo(){const d=await A('/api/tenants');document.getElementById('d-who').innerHTML=d.tenants.map(t=>`<option value="${esc(t.id)}">${esc(t.name)} (${esc(t.role)})</option>`).join('')}
275276
async function send(){
276277
const tid=document.getElementById('d-who').value,msg=document.getElementById('d-msg').value.trim();if(!msg)return;document.getElementById('d-msg').value='';
277278
const t=await A('/api/tenants/'+tid),ch=document.getElementById('chat'),det=document.getElementById('details');
278-
ch.innerHTML+=`<div class="msg usr"><div class="who">${t.name} (${t.role})</div><div class="bbl">${msg}</div></div>`;
279+
ch.innerHTML+=`<div class="msg usr"><div class="who">${esc(t.name)} (${esc(t.role)})</div><div class="bbl">${esc(msg)}</div></div>`;
279280
const r=await A('/api/demo/send','POST',{tenant_id:tid,message:msg});
280-
ch.innerHTML+=`<div class="msg agt"><div class="who">🦞 OpenClaw Agent</div><div class="bbl">${r.response}</div></div>`;
281-
if(r.violations?.length)ch.innerHTML+=`<div class="msg vio"><div class="bbl">⚠️ Plan E AUDIT: blocked tool detected: ${r.violations.join(', ')}</div></div>`;
282-
det.innerHTML=`<div class="msg sys"><div class="bbl"><b>Tenant:</b> ${tid}\n<b>Profile:</b> ${t.profile}\n<b>Allowed:</b> ${t.tools.join(', ')}</div></div><div class="msg sys"><div class="bbl"><b>Plan A (System Prompt):</b>\n${r.sp}</div></div><div class="msg sys"><div class="bbl"><b>Plan E (Response Audit):</b> ${r.plan_e}${r.violations?.length?'\nViolations: '+r.violations.join(', '):''}</div></div>`+det.innerHTML;
281+
ch.innerHTML+=`<div class="msg agt"><div class="who">🦞 OpenClaw Agent</div><div class="bbl">${esc(r.response)}</div></div>`;
282+
if(r.violations?.length)ch.innerHTML+=`<div class="msg vio"><div class="bbl">⚠️ Plan E AUDIT: blocked tool detected: ${esc(r.violations.join(', '))}</div></div>`;
283+
det.innerHTML=`<div class="msg sys"><div class="bbl"><b>Tenant:</b> ${esc(tid)}\n<b>Profile:</b> ${esc(t.profile)}\n<b>Allowed:</b> ${esc(t.tools.join(', '))}</div></div><div class="msg sys"><div class="bbl"><b>Plan A (System Prompt):</b>\n${esc(r.sp)}</div></div><div class="msg sys"><div class="bbl"><b>Plan E (Response Audit):</b> ${esc(r.plan_e)}${r.violations?.length?'\nViolations: '+esc(r.violations.join(', ')):''}</div></div>`+det.innerHTML;
283284
ch.scrollTop=ch.scrollHeight}
284285

285286
// ===== ROADMAP PAGES =====

0 commit comments

Comments
 (0)