← back to Wallco Ai
SECURITY: fix reflected XSS in htmlHead (<head> now esc()'d at chokepoint — kills /search ?q=</title><script> vector) + gate /api/generator/batch with isAdmin (was the only ungated generator route; anon could spawn 50 SDXL jobs). claude-codex findings #2/#3
c2f49d0d31f1f1ccf557d7e9a6dfa71c04a7a310 · 2026-06-01 14:13:36 -0700 · Steve Abrams
Files touched
Diff
commit c2f49d0d31f1f1ccf557d7e9a6dfa71c04a7a310
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Mon Jun 1 14:13:36 2026 -0700
SECURITY: fix reflected XSS in htmlHead (<head> now esc()'d at chokepoint — kills /search ?q=</title><script> vector) + gate /api/generator/batch with isAdmin (was the only ungated generator route; anon could spawn 50 SDXL jobs). claude-codex findings #2/#3
---
server.js | 16 ++++++++++++++++
1 file changed, 16 insertions(+)
diff --git a/server.js b/server.js
index 44b5dd8..60fc6d1 100644
--- a/server.js
+++ b/server.js
@@ -5960,6 +5960,17 @@ const GTAG = GA4_ID ? `
</script>` : '';
function htmlHead({ title, description, canonical, ogImage = '', ogType = 'website', ogImageAlt = '', productOG = null, preloadImages = [] }) {
+ // SECURITY (2026-06-01, claude-codex finding): HTML-escape every dynamic value
+ // interpolated into <head> below, at this single chokepoint. The <body> esc()'d
+ // but the <head> did not — and /search reflects the user query as the page title
+ // (`Search: ${qRaw}`), so `?q=</title><script>…` was live reflected XSS. Escaping
+ // &<>"' is safe for both element-text (<title>) and attribute (content="…") contexts.
+ const _escHead = (s) => String(s == null ? '' : s).replace(/[<>&"']/g, c => ({ '<': '<', '>': '>', '&': '&', '"': '"', "'": ''' }[c]));
+ title = _escHead(title);
+ description = _escHead(description);
+ canonical = _escHead(canonical);
+ ogImage = ogImage ? _escHead(ogImage) : ogImage;
+ ogImageAlt = _escHead(ogImageAlt);
const siteUrl = `https://${SITE}`;
const ogImg = ogImage || `${siteUrl}/og-default.png`;
const imgAlt = ogImageAlt || title;
@@ -21552,6 +21563,11 @@ app.delete('/api/generator/recipes/:id', (req, res) => {
// runs to completion regardless of the tab. Progress shows up in the Recent
// Runs list, which the page already polls every 30s.
app.post('/api/generator/batch', (req, res) => {
+ // SECURITY (2026-06-01, claude-codex finding): this spawns up to 50 background
+ // SDXL renders. It was the ONLY /api/generator/* route missing the admin gate
+ // its siblings (palette/cooldowns/digest/recipes) all carry — so an anonymous
+ // POST could burn the houses' GPU at will. Gate it consistently.
+ if (!isAdmin(req)) return res.status(404).json({ error: 'not found' });
const { colors = '', elements = [], styles = [], patterns = [], extra_text = '' } = req.body || {};
// Clamp the requested count to a sane 1–50 so a fat-fingered number can't
// spawn a runaway render job.
← c76ad29 deploy: orphaned-entrypoint guard — warn if a pm2 process po
·
back to Wallco Ai
·
SECURITY: SQL-escape interpolated paths/category in elements b3db6de →