[object Object]

← back to Ventura Corridor

fix(news): hardenings caught by codex-2way round 1 audit

840b1a4cc96619a9bcb300b1282681daa3a6bf25 · 2026-05-07 17:47:17 -0700 · SteveStudio2

Two real findings (PROSECUTOR-only, DEFENDER stayed silent both times):

1. /business/:id/news LIMIT 200
   The news_items query had no LIMIT, so a single business with
   thousands of articles would be a DoS vector (slow query + huge
   payload). Capped at 200; existing data tops out at 4 per biz so
   this is purely future-proofing.

2. baseUrl XML-escape in /sitemap.xml and /news/feed.xml
   PUBLIC_BASE_URL is env-controlled but not validated. If it ever
   contained ampersand/quotes/angle brackets, the emitted XML would
   be invalid and could let an operator-supplied env value inject
   attributes into <link>/<loc>. Defense in depth - shared escape
   helper applied at the top of each route.

Both routes still validate via xmllint; smoke confirms /business/49/news
returns 200 with the LIMIT clause in place.

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

Files touched

Diff

commit 840b1a4cc96619a9bcb300b1282681daa3a6bf25
Author: SteveStudio2 <stevestudio2@SteveStacStudio.lan>
Date:   Thu May 7 17:47:17 2026 -0700

    fix(news): hardenings caught by codex-2way round 1 audit
    
    Two real findings (PROSECUTOR-only, DEFENDER stayed silent both times):
    
    1. /business/:id/news LIMIT 200
       The news_items query had no LIMIT, so a single business with
       thousands of articles would be a DoS vector (slow query + huge
       payload). Capped at 200; existing data tops out at 4 per biz so
       this is purely future-proofing.
    
    2. baseUrl XML-escape in /sitemap.xml and /news/feed.xml
       PUBLIC_BASE_URL is env-controlled but not validated. If it ever
       contained ampersand/quotes/angle brackets, the emitted XML would
       be invalid and could let an operator-supplied env value inject
       attributes into <link>/<loc>. Defense in depth - shared escape
       helper applied at the top of each route.
    
    Both routes still validate via xmllint; smoke confirms /business/49/news
    returns 200 with the LIMIT clause in place.
    
    Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---
 src/server/index.ts | 7 +++++--
 1 file changed, 5 insertions(+), 2 deletions(-)

diff --git a/src/server/index.ts b/src/server/index.ts
index 995fb77..0fb8bb8 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -3042,7 +3042,8 @@ rows.map((f: any) => `
 // /sitemap.xml — every published feature + every per-vertical issue page
 app.get('/sitemap.xml', async (_req, res) => {
   try {
-    const baseUrl = process.env.PUBLIC_BASE_URL || 'http://127.0.0.1:9780';
+    const _esc0 = (s: any) => String(s ?? '').replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]!));
+    const baseUrl = _esc0(process.env.PUBLIC_BASE_URL || 'http://127.0.0.1:9780');
     const r = await query(`
       SELECT id, COALESCE(published_at, generated_at) AS ts
       FROM magazine_features
@@ -5116,6 +5117,7 @@ app.get('/business/:id/news', async (req, res) => {
         FROM news_items WHERE business_id = $1
        ORDER BY COALESCE(published_guess, fetched_at::date) DESC,
                 fetched_at DESC
+       LIMIT 200
     `, [id]);
     const esc = (s: any) => String(s ?? '').replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]!));
     const safeUrl = (raw: string) => {
@@ -5196,7 +5198,8 @@ main{max-width:920px;margin:0 auto;padding:32px}
 // description prefers the qwen3 summary when available, else the body excerpt.
 app.get('/news/feed.xml', async (_req, res) => {
   try {
-    const baseUrl = process.env.PUBLIC_BASE_URL || 'http://127.0.0.1:9780';
+    const _esc0 = (s: any) => String(s ?? '').replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]!));
+    const baseUrl = _esc0(process.env.PUBLIC_BASE_URL || 'http://127.0.0.1:9780');
     const r = await query(`
       SELECT n.id, n.business_id, n.source_url, n.title, n.excerpt, n.summary,
              n.published_guess, n.fetched_at,

← 9027832 feat(seo): include news pages in /sitemap.xml  ·  back to Ventura Corridor  ·  feat(news): per-vertical RSS feeds via /news/feed.xml?vertic 52178df →