Fix SSRF, XSS, dedup, force-post, and DB hot-path issues

- Add internal/safehttp: hardened HTTP client (DNS-resolved dial guard
  blocking loopback/RFC1918/CGNAT/link-local, redirect re-validation,
  body-size cap) and rewire article/feed/thumb clients through it
- Cap goquery body at 5 MiB so a hostile origin can't OOM the process
- search.js: reject non-http(s) hrefs to block stored XSS via javascript:
- dedup: tracking-param key "CMP" was unreachable (lookup lowercases);
  fixed to "cmp" so CMP= is actually stripped from canonical URLs
- ForcePost: postItem now returns bool; on dedup-skip ForcePost returns
  false so !post falls back to DB lookup instead of silently consuming
- Bound reaction callbacks behind an 8-slot semaphore; drop overflow
- Add stories indexes on (channel, classified, seen_at DESC),
  (classified, seen_at DESC), and partial image_url to kill full scans
  in IsKnownImageURL and ORDER BY seen_at hot paths
- Surface FTS5 probe Scan error instead of swallowing it
This commit is contained in:
prosolis
2026-05-25 12:23:37 -07:00
parent e428f37c9e
commit 9e5ba5aafc
12 changed files with 234 additions and 19 deletions

View File

@@ -40,6 +40,15 @@
.replace(/'/g, "'");
}
// safeURL rejects any href whose scheme isn't http(s). Stored article_url
// comes from feeds we don't control; a hostile feed publishing
// `javascript:fetch(...)` would otherwise execute on click.
function safeURL(s) {
const raw = String(s == null ? "" : s).trim();
if (/^https?:\/\//i.test(raw)) return raw;
return "";
}
function render(results) {
items = results || [];
activeIndex = items.length > 0 ? 0 : -1;
@@ -55,8 +64,10 @@
</div>`
: `<div class="hidden sm:flex w-20 h-20 shrink-0 items-center justify-center rounded-2xl bg-[color:var(--ink)]/5 text-2xl">${escapeHTML(r.channel_emoji || "·")}</div>`;
const postedRing = r.posted ? ` border-theme-${escapeHTML(r.channel_theme)}` : "";
const href = safeURL(r.article_url);
if (!href) return "";
return `
<a href="${escapeHTML(r.article_url)}" target="_blank" rel="noopener noreferrer"
<a href="${escapeHTML(href)}" target="_blank" rel="noopener noreferrer"
data-idx="${i}"
class="search-result group flex items-start gap-4 rounded-2xl p-3 transition border-2 border-transparent hover:bg-[color:var(--ink)]/5${postedRing}">
${thumb}
@@ -138,7 +149,8 @@
} else if (e.key === "Enter") {
if (activeIndex >= 0 && items[activeIndex]) {
e.preventDefault();
window.open(items[activeIndex].article_url, "_blank", "noopener");
const href = safeURL(items[activeIndex].article_url);
if (href) window.open(href, "_blank", "noopener");
}
}
});

View File

@@ -23,6 +23,7 @@ import (
"golang.org/x/image/draw"
_ "golang.org/x/image/webp"
"pete/internal/safehttp"
"pete/internal/storage"
)
@@ -36,7 +37,7 @@ const (
)
var (
thumbClient = &http.Client{Timeout: thumbFetchTimeout}
thumbClient = safehttp.NewClient(thumbFetchTimeout)
thumbMu sync.Mutex
thumbInFlight = map[string]*sync.WaitGroup{}