mirror of
https://github.com/myronblair/jarvis
synced 2026-07-27 16:22:55 -05:00
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)
This commit is contained in:
@@ -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)}
|
||||
<div class="card" style="padding:24px;margin:20px 0">
|
||||
<div style="font-size:0.7rem;letter-spacing:2px;color:var(--cyan);margin-bottom:8px">INFRASTRUCTURE REFERENCE</div>
|
||||
<div style="color:var(--text-dim);font-size:0.75rem;margin-bottom:16px">Complete server map, credentials, deployment workflow, service configs, and phone system reference.</div>
|
||||
<a href="downloads/INFRASTRUCTURE-REFERENCE.md" download="INFRASTRUCTURE-REFERENCE.md"
|
||||
<a href="?action=docs_download" download="INFRASTRUCTURE-REFERENCE.md"
|
||||
style="display:inline-block;padding:8px 20px;background:rgba(0,212,255,0.1);border:1px solid var(--cyan);color:var(--cyan);font-size:0.7rem;letter-spacing:2px;text-decoration:none">
|
||||
↓ DOWNLOAD INFRASTRUCTURE-REFERENCE.MD
|
||||
</a>
|
||||
@@ -2171,6 +2182,12 @@ let _alertFilter = 'active';
|
||||
let _modalCb = null;
|
||||
|
||||
function esc(s){ return String(s||'').replace(/&/g,'&').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() {
|
||||
<td class="ts">${ago(d.last_seen)}</td>
|
||||
<td><div class="actions-col">
|
||||
<button class="btn btn-xs" onclick="pingDev('${esc(d.ip)}',this)">PING</button>
|
||||
<button class="btn btn-xs btn-yellow" onclick="netModal(${d.id},'${esc(d.ip)}','${esc(d.alias||'')}','${esc(d.device_type||'')}')">NAME</button>
|
||||
<button class="btn btn-xs btn-yellow" onclick="netModal(${d.id},'${escJs(d.ip)}','${escJs(d.alias||'')}','${escJs(d.device_type||'')}')">NAME</button>
|
||||
<button class="btn btn-xs btn-red" onclick="delNet(${d.id},'${esc(name)}')">DEL</button>
|
||||
</div></td>`;
|
||||
}, null, null);
|
||||
@@ -3157,7 +3174,7 @@ async function loadAlerts() {
|
||||
<td class="ts">${ts(a.created_at)}</td>
|
||||
<td><div class="actions-col">
|
||||
${!a.resolved?`<button class="btn btn-xs btn-green" onclick="apiPost('alerts_resolve',{id:${a.id}},()=>{toast('Resolved','ok');loadAlerts()})">RESOLVE</button>`:''}
|
||||
<button class="btn btn-xs btn-yellow" onclick="alertModal(${a.id},'${esc(a.alert_type)}','${esc(a.title)}','${esc(a.message||'')}','${esc(a.severity)}')">EDIT</button>
|
||||
<button class="btn btn-xs btn-yellow" onclick="alertModal(${a.id},'${escJs(a.alert_type)}','${escJs(a.title)}','${escJs(a.message||'')}','${escJs(a.severity)}')">EDIT</button>
|
||||
<button class="btn btn-xs btn-red" onclick="apiPost('alerts_delete',{id:${a.id}},()=>{toast('Deleted','ok');loadAlerts()})">DEL</button>
|
||||
</div></td>`, null, null);
|
||||
}
|
||||
@@ -3267,7 +3284,7 @@ function renderIntents(intents) {
|
||||
<td>${i.active?'<span class="badge badge-green">ON</span>':'<span class="badge badge-dim">OFF</span>'}</td>
|
||||
<td><div class="actions-col">
|
||||
<button class="btn btn-xs" onclick="apiPost('intents_toggle',{id:${i.id}},()=>{toast('Toggled','ok');loadIntents()})">${i.active?'DISABLE':'ENABLE'}</button>
|
||||
<button class="btn btn-xs btn-yellow" onclick='intentModal(${i.id},"${esc(i.intent_name)}","${esc(i.pattern)}",${JSON.stringify(i.response_template||"")},"${esc(i.action_type)}",${i.priority},${i.active})'>EDIT</button>
|
||||
<button class="btn btn-xs btn-yellow" onclick="intentModal(${i.id},'${escJs(i.intent_name)}','${escJs(i.pattern)}','${escJs(i.response_template||'')}','${escJs(i.action_type)}',${i.priority},${i.active})">EDIT</button>
|
||||
<button class="btn btn-xs btn-red" onclick="apiPost('intents_delete',{id:${i.id}},()=>{toast('Deleted','ok');loadIntents()})">DEL</button>
|
||||
</div></td>`, null, null);
|
||||
}
|
||||
@@ -3499,7 +3516,7 @@ async function loadNews() {
|
||||
<div style="font-size:0.75rem">${esc(c.title)}</div>
|
||||
${c.url?`<div style="font-size:0.6rem;color:var(--dim)">${esc(c.url)}</div>`:''}
|
||||
</div>
|
||||
<button class="btn btn-xs btn-yellow" onclick='newsCustomModal(${c.id},"${esc(c.title)}","${esc(c.url||"")}")'>EDIT</button>
|
||||
<button class="btn btn-xs btn-yellow" onclick="newsCustomModal(${c.id},'${escJs(c.title)}','${escJs(c.url||'')}')">EDIT</button>
|
||||
<button class="btn btn-xs btn-red" onclick="apiPost('news_custom_delete',{id:${c.id}},()=>{toast('Deleted','ok');loadNews()})">DEL</button>
|
||||
</div>`).join('');
|
||||
}
|
||||
@@ -4884,7 +4901,7 @@ async function loadCalFeeds() {
|
||||
<td>${ts(f.last_sync)}</td>
|
||||
<td>${f.last_count||0}</td>
|
||||
<td>${f.active?'<span class="badge badge-green">ACTIVE</span>':'<span class="badge badge-red">PAUSED</span>'}</td>
|
||||
<td><button class="btn btn-xs" onclick='calFeedModal(${JSON.stringify(f)})'>EDIT</button>
|
||||
<td><button class="btn btn-xs" onclick="calFeedModal(${esc(JSON.stringify(f))})">EDIT</button>
|
||||
<button class="btn btn-xs btn-red" onclick="calFeedDel(${f.id})">DEL</button></td>
|
||||
</tr>`).join('')}</tbody></table>`;
|
||||
}
|
||||
|
||||
+6
-1
@@ -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');
|
||||
|
||||
|
||||
@@ -1,4 +1,14 @@
|
||||
// ── GLOBALS ──────────────────────────────────────────────────────────
|
||||
function escHtml(s) {
|
||||
return String(s == null ? '' : s).replace(/&/g,'&').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'
|
||||
? `<span style="font-size:0.53rem;color:var(--cyan);letter-spacing:1px;margin-left:4px">${(d.agent_type||'AGENT').toUpperCase()}</span>` : '';
|
||||
? `<span style="font-size:0.53rem;color:var(--cyan);letter-spacing:1px;margin-left:4px">${escHtml((d.agent_type||'AGENT').toUpperCase())}</span>` : '';
|
||||
const del = d.deletable
|
||||
? `<button onclick="deleteNetworkDevice('${d.ip}',event)" style="background:none;border:none;color:var(--red);cursor:pointer;font-size:0.9rem;padding:0 2px;opacity:0.5;flex-shrink:0" title="Remove">×</button>` : '';
|
||||
? `<button onclick="deleteNetworkDevice('${escJs(d.ip)}',event)" style="background:none;border:none;color:var(--red);cursor:pointer;font-size:0.9rem;padding:0 2px;opacity:0.5;flex-shrink:0" title="Remove">×</button>` : '';
|
||||
const bl = d.source === 'agent' ? 'border-left:2px solid ' + (alive ? 'var(--green)' : 'var(--red)') + ';' : '';
|
||||
return `<div class="device-item" data-ctx-key="${ctxKey}" onclick="selectContext('${ctxKey}')" style="${bl}display:flex;align-items:center">
|
||||
return `<div class="device-item" data-ctx-key="${ctxKey}" onclick="selectContext('${escJs(ctxKey)}')" style="${bl}display:flex;align-items:center">
|
||||
<div class="device-status ${alive?'on':'off'}" style="flex-shrink:0"></div>
|
||||
<div class="device-info" style="flex:1;min-width:0">
|
||||
<div class="device-name" style="display:flex;align-items:center">${d.name||d.ip}${badge}</div>
|
||||
<div class="device-ip">${d.ip||''}${lat}</div>
|
||||
<div class="device-name" style="display:flex;align-items:center">${escHtml(d.name||d.ip)}${badge}</div>
|
||||
<div class="device-ip">${escHtml(d.ip||'')}${lat}</div>
|
||||
</div>${del}
|
||||
</div>`;
|
||||
}
|
||||
@@ -684,7 +694,7 @@ async function loadProxmox() {
|
||||
type_label:vm.type||'qemu', uptime:vm.uptime||0};
|
||||
return `<div class="vm-card" data-ctx-key="${ctxKey}" onclick="selectContext('${ctxKey}')" title="Click to ask Jarvis about this VM">
|
||||
<div class="vm-header">
|
||||
<span class="vm-name">${vm.name}</span>
|
||||
<span class="vm-name">${escHtml(vm.name)}</span>
|
||||
<span style="color:${statusColor};font-size:0.65rem">● ${(vm.status||'').toUpperCase()}</span>
|
||||
</div>
|
||||
<div class="vm-metrics">
|
||||
@@ -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 += `<div class="news-item" data-ctx-key="${ctxKey}" onclick="selectContext('${ctxKey}')" title="Click to ask Jarvis about this story">
|
||||
<div class="news-source">${a.source}</div>
|
||||
<div class="news-title">${a.title.length > 90 ? a.title.slice(0,87)+'…' : a.title}</div>
|
||||
${a.pub ? '<div class="news-time">' + a.pub + '</div>' : ''}
|
||||
<div class="news-source">${escHtml(a.source)}</div>
|
||||
<div class="news-title">${escHtml(titleDisplay)}</div>
|
||||
${a.pub ? '<div class="news-time">' + escHtml(a.pub) + '</div>' : ''}
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -401,8 +401,8 @@ function renderAgentsTab(agents, metrics) {
|
||||
style="flex-direction:column;align-items:stretch;border-left:3px solid ${alive ? 'var(--green)' : 'var(--red)'}">
|
||||
<div style="display:flex;align-items:center;gap:8px;margin-bottom:6px">
|
||||
<div style="width:8px;height:8px;border-radius:50%;background:${alive ? 'var(--green)' : 'var(--red)'};box-shadow:${alive ? '0 0 6px var(--green)' : 'none'};flex-shrink:0"></div>
|
||||
<span style="font-family:var(--font-mono);font-size:0.72rem;color:var(--text);flex:1">${ag.hostname}</span>
|
||||
<span style="font-size:0.58rem;color:var(--text-dim)">${ag.agent_type.toUpperCase()} · ${ag.ip_address}</span>
|
||||
<span style="font-family:var(--font-mono);font-size:0.72rem;color:var(--text);flex:1">${escHtml(ag.hostname)}</span>
|
||||
<span style="font-size:0.58rem;color:var(--text-dim)">${escHtml(ag.agent_type.toUpperCase())} · ${escHtml(ag.ip_address)}</span>
|
||||
<span style="font-size:0.58rem;color:${alive ? 'var(--green)' : 'var(--red)'};">${alive ? 'ONLINE' : 'OFFLINE'}</span>
|
||||
</div>
|
||||
${alive ? `<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:8px;margin-bottom:4px">
|
||||
@@ -425,8 +425,8 @@ function renderAgentsTab(agents, metrics) {
|
||||
${svcs ? `<div style="font-size:0.58rem">${svcs}</div>` : ''}
|
||||
</div>
|
||||
${alive ? `<div style="display:flex;gap:5px;margin-top:6px">
|
||||
<button onclick="event.stopPropagation();agentScreenshot('${ag.hostname}')" style="flex:1;background:rgba(0,212,255,0.06);border:1px solid var(--panel-border);border-radius:3px;padding:3px 6px;color:var(--cyan);font-family:var(--font-display);font-size:0.48rem;letter-spacing:1px;cursor:pointer">◈ SCREENSHOT</button>
|
||||
<button onclick="event.stopPropagation();agentSysinfo('${ag.hostname}')" style="flex:1;background:rgba(0,212,255,0.06);border:1px solid var(--panel-border);border-radius:3px;padding:3px 6px;color:var(--text-dim);font-family:var(--font-display);font-size:0.48rem;letter-spacing:1px;cursor:pointer">⚡ SYSINFO</button>
|
||||
<button onclick="event.stopPropagation();agentScreenshot('${escJs(ag.hostname)}')" style="flex:1;background:rgba(0,212,255,0.06);border:1px solid var(--panel-border);border-radius:3px;padding:3px 6px;color:var(--cyan);font-family:var(--font-display);font-size:0.48rem;letter-spacing:1px;cursor:pointer">◈ SCREENSHOT</button>
|
||||
<button onclick="event.stopPropagation();agentSysinfo('${escJs(ag.hostname)}')" style="flex:1;background:rgba(0,212,255,0.06);border:1px solid var(--panel-border);border-radius:3px;padding:3px 6px;color:var(--text-dim);font-family:var(--font-display);font-size:0.48rem;letter-spacing:1px;cursor:pointer">⚡ SYSINFO</button>
|
||||
</div>` : ''}
|
||||
</div>`;
|
||||
}).join('');
|
||||
|
||||
Reference in New Issue
Block a user