[object Object]

← back to Costa Rica

cycle 24 docs: YOLO_NOTES ledger — stored-XSS fix on place.html + ingest audit findings

6564e86aa3d6735efd30eb80b9e8854f9621226f · 2026-09-24 04:05:40 -0700 · Steve

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TouFkmUGKHtwqpZwgReVic

Files touched

Diff

commit 6564e86aa3d6735efd30eb80b9e8854f9621226f
Author: Steve <steve@designerwallcoverings.com>
Date:   Thu Sep 24 04:05:40 2026 -0700

    cycle 24 docs: YOLO_NOTES ledger — stored-XSS fix on place.html + ingest audit findings
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01TouFkmUGKHtwqpZwgReVic
---
 YOLO_NOTES.md | 24 ++++++++++++++++++++++++
 1 file changed, 24 insertions(+)

diff --git a/YOLO_NOTES.md b/YOLO_NOTES.md
index d80aabf..6dceaf0 100644
--- a/YOLO_NOTES.md
+++ b/YOLO_NOTES.md
@@ -518,3 +518,27 @@ Canonical apply order now: `004_marketplace → 005_apple → 006_contacts → 0
 **Cost:** $0 (local + one Cody pass that ran an actual negative-test verification, not a positive-only rubber-stamp).
 
 **Backlog:** THREE gated items now on Steve's desk — host-claim approval (unanswered), reconciler cron (unanswered), and the new payout-visibility decision. Plus live-only preflight (provider idempotency key + sig-encoding) and the processorFee schema prerequisite. Concrete-deferred code backlog is empty again — next cycle: another cold audit on a fresh surface (ingest/scraper scripts, App Store submission checklist, or a refund-after-payout state-machine guard).
+
+---
+
+## Cycle 24 — cold Cody audit of the INGEST layer: live stored-XSS closed (TK-10346)
+
+**Surface:** the data-ingest layer (scripts/ingest/*.js + scripts/cr-*.js) — DB-writing, external-data-driven, never audited across 23 money-path cycles. (createPayoutForBooking still has no caller, so the refund-after-payout guard stays moot — skipped.)
+
+**Headline finding (CRITICAL, REAL, live):** SQL injection = NONE (every ingest query parameterizes — genuinely clean). But the scraped fields feed a **stored XSS** on the live consumer page `public/place.html`: `name`/`website`/`email`/`source`/`image_url`/credit (all attacker-editable via Google Business Profile, OSM tags, portal listings) were concatenated RAW into `innerHTML` + Leaflet `bindPopup`, and the page had **no `esc()` at all** (index.html has one; place.html didn't). A listing named `X"><img src=x onerror=...>` executed on every `/p/<slug>` visitor — cookie/session theft, no auth.
+
+**Fix (escape at output + allow-list URL protocols):**
+- Ported `esc()` (same as index.html) + added `safeUrl()` (http/mailto/tel only) + `safeImg()` (http(s) + root-relative `/img/…`, blocks script + protocol-relative).
+- Wrapped all sinks: credit, d-email, d-website, d-source, bindPopup(name), website CTA href, hero imgEl.src.
+- Rebuilt the siblings block via DOM (createElement/textContent + DOM-set encodeURI'd background-image) — `image_url` landed in a CSS `url()` inside a style attribute, uncloseable by HTML-escaping.
+- `test/place-page-xss.test.js`: evals the **shipped** esc/safeUrl/safeImg against payloads + asserts every named-field sink routes through them (regression guard).
+
+**Two Cody passes — the gate earned its keep:** the diff-gate caught that the hero `imgEl.src` was the ONE named field (`image_url`) I initially skipped. I fixed it with `safeImg` (NOT Cody's literal `safeUrl` suggestion, which would have blanked every localized `/img/…` image — verified the actual stored URL shapes before choosing). Suite **205 → 209**, serial green. Commit `d79ae5e`.
+
+**GATED:** the fix is committed + tested locally, but the XSS is LIVE on prod until deployed — external deploy to a customer-facing site is a hard gate. Drafted `2026-09-24-TK-10346-costa-place-xss-DEPLOY-GATED.md` (surgical single-file scp; security-urgent). Did NOT touch prod.
+
+**Cost:** $0 (local + two Cody passes; the 2nd was a genuine diff-gate that found a real missed sink, not a rubber-stamp).
+
+**Queued (Cody's 3 lower-sev ingest items, no live exploit path — next cycles, reversible/no-gate):** (1) slug-collision dedup in meic-pymes.js silently MERGES two different businesses (missing-cedula fallback slugs on name+canton — common Spanish trade names collide → ON CONFLICT grafts B's fields onto A); (2) one poison record aborts the WHOLE run in meic-pymes.js + ict-cst.js (no per-row try/catch — google-places.js + local-portals.js got it right); (3) no fetch timeout in scripts/ingest/_lib.js (a hung gov site stalls run-all.js forever; cr-portal-crawl.js has a correct 20s AbortController to copy).
+
+**Backlog:** FOUR gated items on Steve's desk — host-claim approval, reconciler cron, payout-visibility decision, and now the XSS deploy (security-urgent). Next cycle: ingest hardening items (2)+(3) above (per-record isolation + fetch timeout — cheapest, reversible), then (1) the dedup key.

← d79ae5e costa-rica: fix stored XSS on public/place.html — escape eve  ·  back to Costa Rica  ·  costa-rica: ingest layer resilience — fetch timeout + per-re f10bf96 →