[object Object]

← back to Marketing Command Center

vendors: harden fetchLiOg — linkedin-host guard, redirect:manual, content-type check (codex-check fold-in)

be836bbe4e2c92c3df58d7b8dd6f4e62fbaf78de · 2026-08-31 12:27:01 -0700 · Steve Abrams

Files touched

Diff

commit be836bbe4e2c92c3df58d7b8dd6f4e62fbaf78de
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Mon Aug 31 12:27:01 2026 -0700

    vendors: harden fetchLiOg — linkedin-host guard, redirect:manual, content-type check (codex-check fold-in)
---
 modules/vendors/index.js | 8 +++++++-
 1 file changed, 7 insertions(+), 1 deletion(-)

diff --git a/modules/vendors/index.js b/modules/vendors/index.js
index bf39bab..000e1df 100644
--- a/modules/vendors/index.js
+++ b/modules/vendors/index.js
@@ -93,12 +93,18 @@ async function fetchPosts(igId, token, username) {
 // shares a link WITH attribution back to the vendor.
 const LI_UA = 'facebookexternalhit/1.1 (+http://www.facebook.com/externalhit_uatext.php)';
 async function fetchLiOg(url) {
+  // Defense-in-depth (the slug is always from our own JSON, never user input, so
+  // this is belt-and-suspenders): only ever fetch a linkedin.com company page.
+  try { const h = new URL(url).hostname; if (!/(^|\.)linkedin\.com$/i.test(h)) return { ok: false, error: 'refusing non-linkedin host' }; } catch { return { ok: false, error: 'bad url' }; }
   const ctrl = new AbortController();
   const timer = setTimeout(() => ctrl.abort(), 8000);
   try {
-    const r = await fetch(url, { headers: { 'User-Agent': LI_UA, 'Accept-Language': 'en-US,en;q=0.9' }, redirect: 'follow', signal: ctrl.signal });
+    // redirect:'manual' so a crafted redirect can't bounce this fetch off-host.
+    const r = await fetch(url, { headers: { 'User-Agent': LI_UA, 'Accept-Language': 'en-US,en;q=0.9' }, redirect: 'manual', signal: ctrl.signal });
     const status = r.status;
+    if (status >= 300 && status < 400) return { ok: false, status, error: 'redirected (auth wall)' };
     if (!r.ok) return { ok: false, status, error: 'HTTP ' + status + (status === 999 || status === 429 ? ' (LinkedIn rate-limited / auth-walled this fetch)' : '') };
+    if (!/text\/html/i.test(r.headers.get('content-type') || '')) return { ok: false, status, error: 'non-html response' };
     const html = (await r.text()).slice(0, 400000);
     // Decode HTML entities (LinkedIn double-encodes &amp;amp; → run &amp;→& twice).
     const dec = s => String(s || '').replace(/&amp;/g, '&').replace(/&amp;/g, '&').replace(/&#39;/g, "'").replace(/&#x27;/g, "'").replace(/&quot;/g, '"').replace(/&lt;/g, '<').replace(/&gt;/g, '>');

← b8fe12e vendors panel: add 'Pull ALL vendor accounts' + IG coverage  ·  back to Marketing Command Center  ·  auto-data-snapshot: 2026-08-31T12:34:16 (10 data files) — da 4bbd244 →