[object Object]

← back to Factory Floor

fix broken URL-strip regex (was a parse error) + escape untrusted strings + add rel=noopener to service links

21889053a582c741eded8fbe50347ed29261c986 · 2026-05-18 20:08:22 -0700 · SteveStudio2

Files touched

Diff

commit 21889053a582c741eded8fbe50347ed29261c986
Author: SteveStudio2 <steve@designerwallcoverings.com>
Date:   Mon May 18 20:08:22 2026 -0700

    fix broken URL-strip regex (was a parse error) + escape untrusted strings + add rel=noopener to service links
---
 public/index.html | 27 ++++++++++++++++-----------
 1 file changed, 16 insertions(+), 11 deletions(-)

diff --git a/public/index.html b/public/index.html
index 537c7fb..3d79f7a 100644
--- a/public/index.html
+++ b/public/index.html
@@ -88,6 +88,11 @@
 <script>
 const $ = (id) => document.getElementById(id);
 
+// Escape text before injecting into innerHTML — names/messages come from
+// local processes (pm2, ollama, git) but are still untrusted strings.
+const esc = (v) => String(v ?? '').replace(/[&<>"']/g, (c) =>
+  ({ '&':'&amp;', '<':'&lt;', '>':'&gt;', '"':'&quot;', "'":'&#39;' }[c]));
+
 const BRAIN_SVG = `
 <svg viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
   <defs>
@@ -154,7 +159,7 @@ function bigRedCard(state) {
         <div class="model-row">
           <label>brain</label>
           <select id="brModel" onchange="setBigRedModel(this.value)">
-            ${allModels.map(m => `<option ${m===current?'selected':''} value="${m}">${m}</option>`).join('')}
+            ${allModels.map(m => `<option ${m===current?'selected':''} value="${esc(m)}">${esc(m)}</option>`).join('')}
           </select>
           <span class="saved">saved ✓</span>
         </div>
@@ -193,7 +198,7 @@ async function tick() {
   const memPct = Math.min(100, ((h.mem_gb_total - h.mem_gb_free) / h.mem_gb_total) * 100);
   const loadColor = h.load?.[0] > 100 ? 'var(--bad)' : (h.load?.[0] > 20 ? 'var(--warn)' : 'var(--ok)');
   html += `<div class="card third"><h2>Host</h2>
-    <div class="row"><span class="l">${h.host || '—'}</span><span class="r">${h.cores} cores</span></div>
+    <div class="row"><span class="l">${esc(h.host || '—')}</span><span class="r">${h.cores} cores</span></div>
     <div class="row"><span class="l">Load (1m)</span><span class="r" style="color:${loadColor}">${h.load?.[0] ?? '—'}</span></div>
     <div class="row"><span class="l">Memory</span><span class="r">${memUsed} / ${h.mem_gb_total} GB</span></div>
     <div class="bar"><span style="width:${memPct}%"></span></div>
@@ -209,8 +214,8 @@ async function tick() {
     <div class="tiles">${(ol.installed || []).map(m => {
       const loaded = (ol.loaded || []).some(L => L.name === m.name);
       return `<div class="tile">
-        <div class="name">${m.name}${loaded ? '<span class="pill ok">HOT</span>' : ''}</div>
-        <div class="sub">${m.params || ''} · ${m.q || ''} · ${m.size_gb}GB</div>
+        <div class="name">${esc(m.name)}${loaded ? '<span class="pill ok">HOT</span>' : ''}</div>
+        <div class="sub">${esc(m.params || '')} · ${esc(m.q || '')} · ${m.size_gb}GB</div>
       </div>`;
     }).join('') || '<div class="empty">no models</div>'}</div>
   </div>`;
@@ -222,9 +227,9 @@ async function tick() {
     const list = groups[g] || [];
     if (!list.length) continue;
     html += `<div class="group-head">${g.replace('-', ' ')}</div><div class="tiles">${
-      list.map(s => `<a class="tile" href="${s.url}" target="_blank">
-        <div class="name">${s.name} ${fmt.pill(s.ok)}</div>
-        <div class="sub">${s.url.replace(/^https?:\\/\\//, '')} · ${s.ok ? `${s.status} · ${s.ms}ms` : (s.error || s.status)}</div>
+      list.map(s => `<a class="tile" href="${esc(s.url)}" target="_blank" rel="noopener noreferrer">
+        <div class="name">${esc(s.name)} ${fmt.pill(s.ok)}</div>
+        <div class="sub">${esc(s.url.replace(/^https?:\/\//, ''))} · ${s.ok ? `${s.status} · ${s.ms}ms` : esc(s.error || String(s.status))}</div>
       </a>`).join('')
     }</div>`;
   }
@@ -233,7 +238,7 @@ async function tick() {
   // GIT
   html += `<div class="card third"><h2>Recent commits <span class="count">across ~/Projects</span></h2>
     <div style="max-height:280px;overflow-y:auto">${(s.git || []).slice(0,12).map(g =>
-      `<div class="row"><span class="l" style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:60%">${g.project}: <span style="color:var(--ink)">${(g.msg||'').slice(0,50)}</span></span><span class="r">${fmt.age(g.age_s)}</span></div>`
+      `<div class="row"><span class="l" style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:60%">${esc(g.project)}: <span style="color:var(--ink)">${esc((g.msg||'').slice(0,50))}</span></span><span class="r">${fmt.age(g.age_s)}</span></div>`
     ).join('') || '<div class="empty">no repos</div>'}</div>
   </div>`;
 
@@ -244,9 +249,9 @@ async function tick() {
     <div style="max-height:380px;overflow-y:auto"><table>
       <thead><tr><th>name</th><th>state</th><th>port</th><th class="r">mem</th><th class="r">cpu</th><th class="r">↻</th></tr></thead>
       <tbody>${pm2items.map(p => `<tr>
-        <td class="mono">${p.name}</td>
-        <td class="s ${p.status !== 'online' ? p.status : ''}">${p.status}</td>
-        <td class="mono" style="color:var(--mute)">${p.port || ''}</td>
+        <td class="mono">${esc(p.name)}</td>
+        <td class="s ${p.status !== 'online' ? esc(p.status) : ''}">${esc(p.status)}</td>
+        <td class="mono" style="color:var(--mute)">${esc(p.port || '')}</td>
         <td class="r">${p.mem_mb}M</td>
         <td class="r">${p.cpu}%</td>
         <td class="r">${p.restarts}</td>

← a09f471 snapshot: 4 file(s) changed, +3 new, ~1 modified  ·  back to Factory Floor  ·  Add per-site favicon (kills /favicon.ico 404) a2e466c →