← 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 => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[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 => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[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 => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[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 →