[object Object]

← 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

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 &amp; 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 →