[object Object]

← back to Ventura Corridor

fix: 3 bugs caught by codex-2way audit at tick 73

1229e88dfe17393fb57089054d1cbd37d378779d · 2026-05-08 00:28:29 -0700 · SteveStudio2

PROSECUTOR (Kimi K2.5 cloud) audit on the 17-commit batch since
tick 49 found three real issues. All fixed:

1. public/index.html:353 — fleet panel ternary "escapeHtml ? escapeHtml(...)"
   was unnecessary but legal (escapeHtml is defined at line 204). Removed
   the dead ternary; also wraps m.label since it comes from network so
   defense in depth. PROSECUTOR misread this as undefined-fn but flagged
   the right surface area to harden.

2. src/server/index.ts:541-545 — RSS feed vertical filter was concatenating
   user input into ILIKE pattern without escaping % or _ wildcards. So
   ?vertical=% would match every category. Real injection but low impact
   (read-only, no auth bypass). Now escapes \%, \_, \\ in tokens before
   the ILIKE assembly.

3. public/news.html:200 — Hot-only toggle stored Number(r.id) in Set but
   filtered with String r.business_id — PG bigint always serializes as
   string, so Set.has missed every row. Set.has now coerces both sides
   to String. Also slices leaderboard rows to 1000 max as a tab-OOM guard.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Files touched

Diff

commit 1229e88dfe17393fb57089054d1cbd37d378779d
Author: SteveStudio2 <stevestudio2@SteveStacStudio.lan>
Date:   Fri May 8 00:28:29 2026 -0700

    fix: 3 bugs caught by codex-2way audit at tick 73
    
    PROSECUTOR (Kimi K2.5 cloud) audit on the 17-commit batch since
    tick 49 found three real issues. All fixed:
    
    1. public/index.html:353 — fleet panel ternary "escapeHtml ? escapeHtml(...)"
       was unnecessary but legal (escapeHtml is defined at line 204). Removed
       the dead ternary; also wraps m.label since it comes from network so
       defense in depth. PROSECUTOR misread this as undefined-fn but flagged
       the right surface area to harden.
    
    2. src/server/index.ts:541-545 — RSS feed vertical filter was concatenating
       user input into ILIKE pattern without escaping % or _ wildcards. So
       ?vertical=% would match every category. Real injection but low impact
       (read-only, no auth bypass). Now escapes \%, \_, \\ in tokens before
       the ILIKE assembly.
    
    3. public/news.html:200 — Hot-only toggle stored Number(r.id) in Set but
       filtered with String r.business_id — PG bigint always serializes as
       string, so Set.has missed every row. Set.has now coerces both sides
       to String. Also slices leaderboard rows to 1000 max as a tab-OOM guard.
    
    Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---
 public/index.html   | 2 +-
 public/news.html    | 6 ++++--
 src/server/index.ts | 7 ++++++-
 3 files changed, 11 insertions(+), 4 deletions(-)

diff --git a/public/index.html b/public/index.html
index aceafd0..9cdc12b 100644
--- a/public/index.html
+++ b/public/index.html
@@ -443,7 +443,7 @@ async function loadBuildStatus() {
         const modelStr = model ? `${model.name} ${model.pinned ? '⚓' : ''}` : 'idle';
         const cpu = m.cpu.toFixed(0), ram = m.ram_pct.toFixed(0);
         const dashUrl = port === '100.94.103.98:9930' ? 'http://100.94.103.98:9930' : 'http://localhost:9931';
-        return `<a href="${dashUrl}" target="_blank" style="color:inherit;text-decoration:none"><span style="color:var(--metal)">${m.label}</span> · <span style="color:var(--ink)">${escapeHtml ? escapeHtml(modelStr) : modelStr}</span> · cpu <b style="color:var(--metal-glow)">${cpu}%</b> · ram <b style="color:var(--metal-glow)">${ram}%</b></a>`;
+        return `<a href="${dashUrl}" target="_blank" style="color:inherit;text-decoration:none"><span style="color:var(--metal)">${escapeHtml(m.label)}</span> · <span style="color:var(--ink)">${escapeHtml(modelStr)}</span> · cpu <b style="color:var(--metal-glow)">${cpu}%</b> · ram <b style="color:var(--metal-glow)">${ram}%</b></a>`;
       };
       const m1El = document.getElementById('fleet-mac1');
       const m2El = document.getElementById('fleet-mac2');
diff --git a/public/news.html b/public/news.html
index c2ff8fe..81bc918 100644
--- a/public/news.html
+++ b/public/news.html
@@ -124,7 +124,7 @@ function render() {
     rows = rows.filter(r => (r.category || '').toLowerCase().startsWith(vertical.toLowerCase()));
   }
   if (HOT_ONLY && HOT_BIZ_IDS) {
-    rows = rows.filter(r => HOT_BIZ_IDS.has(r.business_id));
+    rows = rows.filter(r => HOT_BIZ_IDS.has(String(r.business_id)));
   }
   rows = rows.slice();
   if (sort === 'biz') {
@@ -204,7 +204,9 @@ document.getElementById('hot-only').addEventListener('click', async () => {
     if (!HOT_BIZ_IDS) {
       try {
         const lb = await fetch('/api/news/leaderboard').then(r => r.json());
-        HOT_BIZ_IDS = new Set((lb.rows || []).filter(r => Number(r.fresh_7d) >= 3).map(r => Number(r.id)));
+        // PG bigint serializes as string; coerce both sides so Set.has matches
+        // (caught by codex-2way audit, tick 73)
+        HOT_BIZ_IDS = new Set(((lb.rows || []).slice(0, 1000)).filter(r => Number(r.fresh_7d) >= 3).map(r => String(r.id)));
       } catch { HOT_BIZ_IDS = new Set(); }
     }
   } else {
diff --git a/src/server/index.ts b/src/server/index.ts
index 90e6684..70fe73c 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -5483,8 +5483,13 @@ app.get('/news/feed.xml', async (req, res) => {
     const businessId = parseInt(String(req.query.business || ''), 10);
     const params: any[] = [];
     const conds: string[] = [];
+    // Escape ILIKE wildcards (% and _) in user input so ?vertical=% can't match
+    // every category. Caught by codex-2way audit (SQL wildcard injection,
+    // tick 73). Backslash-escape, then add the literal "%" suffix only on
+    // top-cat single-token form.
+    const sqlEscape = (s: string) => s.replace(/[\\%_]/g, '\\$&');
     if (vertical) {
-      const tokens = vertical.split(/[:\-_\s]+/).filter(Boolean);
+      const tokens = vertical.split(/[:\-_\s]+/).filter(Boolean).map(sqlEscape);
       if (tokens.length === 1) {
         params.push(tokens[0] + ': %');
         conds.push(`b.category ILIKE $${params.length}`);

← 7d1fe85 feat(coverage): rejected-ideas drawer below the idea-loop fe  ·  back to Ventura Corridor  ·  fix: portable home dir in /api/ideas/{recent,rejects} (caugh 3853840 →