← 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
M modules/vendors/index.js
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; → run &→& twice).
const dec = s => String(s || '').replace(/&/g, '&').replace(/&/g, '&').replace(/'/g, "'").replace(/'/g, "'").replace(/"/g, '"').replace(/</g, '<').replace(/>/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 →