← back to NationalPaperHangers
claude-codex r3: fix null-element crash in 4 templates, session-save race in OAuth callbacks, hostname-spoof in social-embed, missing Vimeo CSP
4ecde9cb860e77b8ece61304f23c7fbe651b66f1 · 2026-05-06 11:34:03 -0700 · Steve
Files touched
M lib/social-embed.jsM routes/auth-google.jsM routes/auth-linkedin.jsM server.jsM views/public/installer-tpl-bilingue.ejsM views/public/installer-tpl-editorial.ejsM views/public/installer-tpl-studio.ejsM views/public/installer-tpl-trade-pro.ejs
Diff
commit 4ecde9cb860e77b8ece61304f23c7fbe651b66f1
Author: Steve <steve@designerwallcoverings.com>
Date: Wed May 6 11:34:03 2026 -0700
claude-codex r3: fix null-element crash in 4 templates, session-save race in OAuth callbacks, hostname-spoof in social-embed, missing Vimeo CSP
---
lib/social-embed.js | 17 +++++++++++------
routes/auth-google.js | 4 +++-
routes/auth-linkedin.js | 2 +-
server.js | 5 ++++-
views/public/installer-tpl-bilingue.ejs | 4 ++--
views/public/installer-tpl-editorial.ejs | 4 ++--
views/public/installer-tpl-studio.ejs | 2 +-
views/public/installer-tpl-trade-pro.ejs | 4 ++--
8 files changed, 26 insertions(+), 16 deletions(-)
diff --git a/lib/social-embed.js b/lib/social-embed.js
index e5c8d60..d416539 100644
--- a/lib/social-embed.js
+++ b/lib/social-embed.js
@@ -4,11 +4,18 @@
// Returns null if URL is unsupported or malformed — caller should fall through
// to a generic link card.
+// Strict-domain check: host must equal the apex OR be a true subdomain.
+// `.endsWith('youtube.com')` alone matches `evilyoutube.com` — we explicitly
+// reject the no-leading-dot case. (claude-codex r3 finding HIGH#4)
+function isHost(host, apex) {
+ return host === apex || host.endsWith('.' + apex);
+}
+
function parseYouTube(u) {
try {
const url = new URL(u);
let id = null;
- if (url.hostname.endsWith('youtube.com') || url.hostname === 'www.youtube.com' || url.hostname === 'youtube.com') {
+ if (isHost(url.hostname, 'youtube.com')) {
if (url.pathname === '/watch') id = url.searchParams.get('v');
else if (url.pathname.startsWith('/embed/')) id = url.pathname.split('/')[2];
else if (url.pathname.startsWith('/shorts/')) id = url.pathname.split('/')[2];
@@ -23,8 +30,7 @@ function parseYouTube(u) {
function parseInstagram(u) {
try {
const url = new URL(u);
- if (!url.hostname.endsWith('instagram.com')) return null;
- // /p/<id>/, /reel/<id>/, /tv/<id>/
+ if (!isHost(url.hostname, 'instagram.com')) return null;
const m = url.pathname.match(/^\/(p|reel|tv)\/([A-Za-z0-9_-]+)/);
if (!m) return null;
return { type: m[1], id: m[2] };
@@ -34,8 +40,7 @@ function parseInstagram(u) {
function parseTikTok(u) {
try {
const url = new URL(u);
- if (!url.hostname.endsWith('tiktok.com')) return null;
- // /@user/video/<id> or /v/<id>.html
+ if (!isHost(url.hostname, 'tiktok.com')) return null;
let m = url.pathname.match(/\/video\/(\d{10,25})/);
if (m) return m[1];
m = url.pathname.match(/\/v\/(\d{10,25})/);
@@ -47,7 +52,7 @@ function parseTikTok(u) {
function parseVimeo(u) {
try {
const url = new URL(u);
- if (!url.hostname.endsWith('vimeo.com')) return null;
+ if (!isHost(url.hostname, 'vimeo.com')) return null;
const m = url.pathname.match(/^\/(\d+)/);
return m ? m[1] : null;
} catch { return null; }
diff --git a/routes/auth-google.js b/routes/auth-google.js
index 845f345..8e94d87 100644
--- a/routes/auth-google.js
+++ b/routes/auth-google.js
@@ -178,7 +178,9 @@ router.get('/auth/google/callback', async (req, res, next) => {
await regenerateSession(req);
req.session.consumerAccountId = id;
req.session.consumer = { id, email: profile.email, name: profile.name || null, picture_url: profile.picture || null };
- res.redirect(sessionNext);
+ // Explicit save: under load the PG session-store write can lag the 302,
+ // landing the client on /book pre-persistence and looking signed-out.
+ req.session.save(err => err ? next(err) : res.redirect(sessionNext));
} catch (err) { next(err); }
});
diff --git a/routes/auth-linkedin.js b/routes/auth-linkedin.js
index 9a3f2f4..4db483c 100644
--- a/routes/auth-linkedin.js
+++ b/routes/auth-linkedin.js
@@ -174,7 +174,7 @@ router.get('/auth/linkedin/callback', async (req, res, next) => {
await regenerateSession(req);
req.session.consumerAccountId = id;
req.session.consumer = { id, email: profile.email, name: profile.name || null, picture_url: profile.picture || null, provider: 'linkedin' };
- res.redirect(sessionNext);
+ req.session.save(err => err ? next(err) : res.redirect(sessionNext));
} catch (err) { next(err); }
});
diff --git a/server.js b/server.js
index e96c649..46f15b7 100644
--- a/server.js
+++ b/server.js
@@ -72,7 +72,10 @@ app.use(helmet({
'https://js.stripe.com', 'https://hooks.stripe.com',
'https://www.youtube.com', 'https://www.youtube-nocookie.com',
'https://www.instagram.com', 'https://instagram.com',
- 'https://www.tiktok.com'
+ 'https://www.tiktok.com',
+ // lib/social-embed.js emits player.vimeo.com iframes; without this CSP
+ // silently strips them. (claude-codex r3 finding HIGH#2)
+ 'https://player.vimeo.com'
],
frameAncestors: ["'none'"],
formAction: ["'self'"],
diff --git a/views/public/installer-tpl-bilingue.ejs b/views/public/installer-tpl-bilingue.ejs
index 504b4f4..4def657 100644
--- a/views/public/installer-tpl-bilingue.ejs
+++ b/views/public/installer-tpl-bilingue.ejs
@@ -53,8 +53,8 @@
<section class="bl-section">
<div class="bl-callout">
- <span data-lang="en">Specialist in <%= (installer.materials || ['wallcoverings']).slice(0,3).map(function(m){return m.replace(/_/g,' ');}).join(', ') %> installations.</span>
- <span data-lang="es">Especialista en instalaciones de <%= (installer.materials || ['empapelado']).slice(0,3).map(function(m){return m.replace(/_/g,' ');}).join(', ') %>.</span>
+ <span data-lang="en">Specialist in <%= (installer.materials || ['wallcoverings']).slice(0,3).map(function(m){return String(m||'').replace(/_/g,' ');}).filter(Boolean).join(', ') %> installations.</span>
+ <span data-lang="es">Especialista en instalaciones de <%= (installer.materials || ['empapelado']).slice(0,3).map(function(m){return String(m||'').replace(/_/g,' ');}).filter(Boolean).join(', ') %>.</span>
</div>
<h2><span data-lang="en">About the studio</span><span data-lang="es">Sobre el estudio</span></h2>
<% if (installer.bio) { %>
diff --git a/views/public/installer-tpl-editorial.ejs b/views/public/installer-tpl-editorial.ejs
index e8b5b30..2b8c5a2 100644
--- a/views/public/installer-tpl-editorial.ejs
+++ b/views/public/installer-tpl-editorial.ejs
@@ -31,11 +31,11 @@
<% if (installer.bio) { %><p><%= installer.bio %></p><% } else { %><p><em>This studio has not yet shared a long-form profile. Their work below speaks first.</em></p><% } %>
<% if ((installer.materials || []).length) { %>
<h2>Materials & methods</h2>
- <p><%= installer.business_name %> works in <%= installer.materials.map(function(m){return m.replace(/_/g,' ');}).join(', ') %><% if ((installer.brands_handled||[]).length) { %>, with installations of <%= installer.brands_handled.slice(0,5).join(', ') %><% } %>.</p>
+ <p><%= installer.business_name %> works in <%= installer.materials.map(function(m){return String(m||'').replace(/_/g,' ');}).filter(Boolean).join(', ') %><% if ((installer.brands_handled||[]).length) { %>, with installations of <%= installer.brands_handled.slice(0,5).join(', ') %><% } %>.</p>
<% } %>
<% if ((installer.market_segments || []).length) { %>
<h2>Project mix</h2>
- <p>Active in <%= installer.market_segments.map(function(s){return s.replace(/_/g,' ');}).join(', ') %><% if (installer.travel_available) { %>; available to travel for projects<% } else if (installer.service_radius_miles) { %>; <%= installer.service_radius_miles %>-mile service radius from <%= installer.city || 'home base' %><% } %>.</p>
+ <p>Active in <%= installer.market_segments.map(function(s){return String(s||'').replace(/_/g,' ');}).filter(Boolean).join(', ') %><% if (installer.travel_available) { %>; available to travel for projects<% } else if (installer.service_radius_miles) { %>; <%= installer.service_radius_miles %>-mile service radius from <%= installer.city || 'home base' %><% } %>.</p>
<% } %>
</div>
<aside class="ed-side">
diff --git a/views/public/installer-tpl-studio.ejs b/views/public/installer-tpl-studio.ejs
index 970853f..a15472b 100644
--- a/views/public/installer-tpl-studio.ejs
+++ b/views/public/installer-tpl-studio.ejs
@@ -45,7 +45,7 @@
<div>
<p><%= installer.bio || 'Profile not yet completed by the studio.' %></p>
<% if ((installer.materials || []).length) { %>
- <p style="margin-top:18px;color:#666;font-size:14px"><strong>Materials</strong> — <%= installer.materials.map(function(m){return m.replace(/_/g,' ');}).join(', ') %></p>
+ <p style="margin-top:18px;color:#666;font-size:14px"><strong>Materials</strong> — <%= installer.materials.map(function(m){return String(m||'').replace(/_/g,' ');}).filter(Boolean).join(', ') %></p>
<% } %>
<% if ((installer.brands_handled || []).length) { %>
<p style="margin-top:6px;color:#666;font-size:14px"><strong>Brands installed</strong> — <%= installer.brands_handled.join(', ') %></p>
diff --git a/views/public/installer-tpl-trade-pro.ejs b/views/public/installer-tpl-trade-pro.ejs
index 745dba6..a2a2111 100644
--- a/views/public/installer-tpl-trade-pro.ejs
+++ b/views/public/installer-tpl-trade-pro.ejs
@@ -59,8 +59,8 @@
<tr><th>Pasting</th><td><%= eq.paper_table ? eq.paper_table.replace(/_/g,' ') : '—' %></td></tr>
<tr><th>Vehicle</th><td><%= eq.vehicle ? eq.vehicle.replace(/_/g,' ') : '—' %></td></tr>
<tr><th>HEPA dust</th><td><%= eq.dust_extraction ? 'Yes — on-site' : '—' %></td></tr>
- <tr><th>Materials</th><td><%= (installer.materials || []).map(function(m){return m.replace(/_/g,' ');}).join(', ') || '—' %></td></tr>
- <tr><th>Segments</th><td><%= (installer.market_segments || []).map(function(s){return s.replace(/_/g,' ');}).join(', ') || '—' %></td></tr>
+ <tr><th>Materials</th><td><%= (installer.materials || []).map(function(m){return String(m||'').replace(/_/g,' ');}).filter(Boolean).join(', ') || '—' %></td></tr>
+ <tr><th>Segments</th><td><%= (installer.market_segments || []).map(function(s){return String(s||'').replace(/_/g,' ');}).filter(Boolean).join(', ') || '—' %></td></tr>
<% if ((credentials || []).length) { %>
<tr><th>Credentials</th><td><% credentials.slice(0,5).forEach(function(c, i){ %><%= i ? ' · ' : '' %><%= c.brand %><% }); %></td></tr>
<% } %>
← a9f49d0 watch the work: YT + IG reels + TikTok embeds via partial ac
·
back to NationalPaperHangers
·
claude-codex r3 (cont): guard installer in social-videos par 350a7bb →