From 24bc876c1db298a30ed79c44452651451d28f9c9 Mon Sep 17 00:00:00 2001 From: Myron Blair Date: Sun, 5 Jul 2026 20:09:52 -0500 Subject: [PATCH] Fix code review findings: unauthenticated infra doc + backup file exposure, onclick-attribute XSS breakouts (admin panel and front-end), plaintext calendar passwords, k()->j() typo, wildcard CORS, session cookie hardening (HttpOnly/SameSite) --- public_html/admin/index.php | 35 ++++++++++++++----- public_html/api.php | 7 +++- public_html/assets/js/jarvis-app.js | 29 ++++++++++----- public_html/assets/js/panels/jarvis-agents.js | 8 ++--- 4 files changed, 56 insertions(+), 23 deletions(-) diff --git a/public_html/admin/index.php b/public_html/admin/index.php index c858e63..da67a7c 100644 --- a/public_html/admin/index.php +++ b/public_html/admin/index.php @@ -430,7 +430,7 @@ if ($action) { case 'cal_feeds_list': - j(JarvisDB::query("SELECT * FROM calendar_feeds ORDER BY source,name") ?? []); + j(JarvisDB::query("SELECT id,name,source,ics_url,username,(password != '' AND password IS NOT NULL) AS has_password,active,created_at FROM calendar_feeds ORDER BY source,name") ?? []); case 'cal_feed_save': $id = (int)($_POST['id'] ?? 0); @@ -552,7 +552,7 @@ if ($action) { } else { bad('Unknown cron worker'); } } elseif ($wType === 'daemon' && $wId === 'arc_reactor' && $wAction === 'restart') { shell_exec('systemctl restart jarvis-arc 2>&1'); - k(['ok'=>true,'msg'=>'Arc Reactor restarting via systemd']); + j(['ok'=>true,'msg'=>'Arc Reactor restarting via systemd']); } elseif ($wType === 'daemon' && $wId === 'arc_reactor' && $wAction === 'setup') { $log = '/var/log/jarvis/arc-setup.log'; $cmd = implode(' && ', [ @@ -567,7 +567,7 @@ if ($action) { 'systemctl restart jarvis-arc', ]); shell_exec("($cmd) >> " . escapeshellarg($log) . " 2>&1 &"); - k(['ok'=>true,'msg'=>'Arc Reactor setup started — check ' . $log]); + j(['ok'=>true,'msg'=>'Arc Reactor setup started — check ' . $log]); } elseif ($wType === 'agent' && $wAction === 'update_status') { $ag = JarvisDB::single('SELECT version, status FROM registered_agents WHERE agent_id=?', [$wId]); j(['ok'=>true,'version'=>$ag['version']??null,'status'=>$ag['status']??'unknown']); @@ -1255,6 +1255,17 @@ if ($action) { readfile($path); exit; + case 'docs_download': + $path = '/var/www/jarvis-private/INFRASTRUCTURE-REFERENCE.md'; + if (!file_exists($path)) bad('File not found', 404); + header('Content-Type: text/markdown'); + header('Content-Disposition: attachment; filename="INFRASTRUCTURE-REFERENCE.md"'); + header('Content-Length: ' . filesize($path)); + header('X-Accel-Buffering: no'); + ob_end_clean(); + readfile($path); + exit; + default: bad('Unknown action'); } } @@ -1644,7 +1655,7 @@ select.filter-sel:focus{border-color:var(--cyan)}
INFRASTRUCTURE REFERENCE
Complete server map, credentials, deployment workflow, service configs, and phone system reference.
- ↓ DOWNLOAD INFRASTRUCTURE-REFERENCE.MD @@ -2171,6 +2182,12 @@ let _alertFilter = 'active'; let _modalCb = null; function esc(s){ return String(s||'').replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"'); } +// For values embedded inside a single-quoted JS string literal within an HTML attribute +// (e.g. onclick="fn('${escJs(x)}')"). Escaping the quote via esc() alone is NOT enough: +// the browser HTML-decodes the attribute before parsing it as JS, so '/" would +// just turn back into a literal ' before the JS parser ever sees it. Backslash-escaping +// survives that decode step, so JS-escape first, then HTML-escape on top. +function escJs(s){ return esc(String(s||'').replace(/\\/g,'\\\\').replace(/'/g,"\\'").replace(/\n/g,'\\n').replace(/\r/g,'\\r')); } function ts(s){ if(!s) return '—'; const d=new Date(s); return d.toLocaleString('en-US',{month:'short',day:'numeric',hour:'2-digit',minute:'2-digit'}); } function ago(s){ if(!s) return '—'; const sec=Math.floor((Date.now()-new Date(s))/1000); if(sec<60) return sec+'s ago'; if(sec<3600) return Math.floor(sec/60)+'m ago'; return Math.floor(sec/3600)+'h ago'; } function fmtUp(s){ const d=Math.floor(s/86400),h=Math.floor((s%86400)/3600),m=Math.floor((s%3600)/60); return (d>0?d+'d ':'')+h+'h '+m+'m'; } @@ -3078,7 +3095,7 @@ function renderNetwork() { ${ago(d.last_seen)}
- +
`; }, null, null); @@ -3157,7 +3174,7 @@ async function loadAlerts() { ${ts(a.created_at)}
${!a.resolved?``:''} - +
`, null, null); } @@ -3267,7 +3284,7 @@ function renderIntents(intents) { ${i.active?'ON':'OFF'}
- +
`, null, null); } @@ -3499,7 +3516,7 @@ async function loadNews() {
${esc(c.title)}
${c.url?`
${esc(c.url)}
`:''}
- + `).join(''); } @@ -4884,7 +4901,7 @@ async function loadCalFeeds() { ${ts(f.last_sync)} ${f.last_count||0} ${f.active?'ACTIVE':'PAUSED'} - + `).join('')}`; } diff --git a/public_html/api.php b/public_html/api.php index f338446..ab07fc1 100644 --- a/public_html/api.php +++ b/public_html/api.php @@ -27,7 +27,12 @@ if (!$_skipSession) { } header('Content-Type: application/json'); -header('Access-Control-Allow-Origin: *'); +$_allowedOrigins = ['https://jarvis.orbishosting.com', 'http://jarvis.orbishosting.com']; +$_origin = $_SERVER['HTTP_ORIGIN'] ?? ''; +if (in_array($_origin, $_allowedOrigins, true)) { + header('Access-Control-Allow-Origin: ' . $_origin); + header('Access-Control-Allow-Credentials: true'); +} header('Access-Control-Allow-Methods: GET, POST, OPTIONS'); header('Access-Control-Allow-Headers: Content-Type, X-Session-Token'); diff --git a/public_html/assets/js/jarvis-app.js b/public_html/assets/js/jarvis-app.js index b71b683..4acac1e 100644 --- a/public_html/assets/js/jarvis-app.js +++ b/public_html/assets/js/jarvis-app.js @@ -1,4 +1,14 @@ // ── GLOBALS ────────────────────────────────────────────────────────── +function escHtml(s) { + return String(s == null ? '' : s).replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"'); +} +// For values embedded inside a single-quoted JS string literal within an HTML attribute +// (e.g. onclick="fn('${escJs(x)}')"). escHtml() alone isn't enough there: the browser +// HTML-decodes the attribute before parsing it as JS, so an encoded quote would just +// turn back into a literal ' before the JS parser ever sees it. +function escJs(s) { + return escHtml(String(s == null ? '' : s).replace(/\\/g,'\\\\').replace(/'/g,"\\'").replace(/\n/g,'\\n').replace(/\r/g,'\\r')); +} let sessionToken = ''; let sessionUser = ''; let sessionId = 'session_' + Date.now(); @@ -596,15 +606,15 @@ function renderNetworkStatus(n) { agent_id: d.agent_id, hostname: d.name}; const lat = d.latency_ms ? ' · ' + d.latency_ms + 'ms' : ''; const badge = d.source === 'agent' - ? `${(d.agent_type||'AGENT').toUpperCase()}` : ''; + ? `${escHtml((d.agent_type||'AGENT').toUpperCase())}` : ''; const del = d.deletable - ? `` : ''; + ? `` : ''; const bl = d.source === 'agent' ? 'border-left:2px solid ' + (alive ? 'var(--green)' : 'var(--red)') + ';' : ''; - return `
+ return `
-
${d.name||d.ip}${badge}
-
${d.ip||''}${lat}
+
${escHtml(d.name||d.ip)}${badge}
+
${escHtml(d.ip||'')}${lat}
${del}
`; } @@ -684,7 +694,7 @@ async function loadProxmox() { type_label:vm.type||'qemu', uptime:vm.uptime||0}; return `
- ${vm.name} + ${escHtml(vm.name)} ● ${(vm.status||'').toUpperCase()}
@@ -1031,10 +1041,11 @@ async function loadNews() { const ctxKey = 'news_' + (cat + '_' + a.title).replace(/[^a-z0-9]/gi,'').slice(0,30); _panelCtx[ctxKey] = {type:'news', label:a.title, title:a.title, source:a.source, pub:a.pub||'', category:cat}; + const titleDisplay = a.title.length > 90 ? a.title.slice(0,87)+'…' : a.title; html += `
-
${a.source}
-
${a.title.length > 90 ? a.title.slice(0,87)+'…' : a.title}
- ${a.pub ? '
' + a.pub + '
' : ''} +
${escHtml(a.source)}
+
${escHtml(titleDisplay)}
+ ${a.pub ? '
' + escHtml(a.pub) + '
' : ''}
`; } } diff --git a/public_html/assets/js/panels/jarvis-agents.js b/public_html/assets/js/panels/jarvis-agents.js index 46ad289..7270514 100644 --- a/public_html/assets/js/panels/jarvis-agents.js +++ b/public_html/assets/js/panels/jarvis-agents.js @@ -401,8 +401,8 @@ function renderAgentsTab(agents, metrics) { style="flex-direction:column;align-items:stretch;border-left:3px solid ${alive ? 'var(--green)' : 'var(--red)'}">
- ${ag.hostname} - ${ag.agent_type.toUpperCase()} · ${ag.ip_address} + ${escHtml(ag.hostname)} + ${escHtml(ag.agent_type.toUpperCase())} · ${escHtml(ag.ip_address)} ${alive ? 'ONLINE' : 'OFFLINE'}
${alive ? `
@@ -425,8 +425,8 @@ function renderAgentsTab(agents, metrics) { ${svcs ? `
${svcs}
` : ''}
${alive ? `
- - + +
` : ''}
`; }).join('');