From e709a158e6ab8537dbf24133c6ce628e953b7b92 Mon Sep 17 00:00:00 2001 From: Vito Sansevero Date: Wed, 7 Oct 2026 19:42:21 -0700 Subject: [PATCH] fix: escape image dimensions and workflow JSON before rendering openGalleryModal interpolated width, height and file_size (stored from a POST body, so possibly strings) straight into data-caption and the card text. showWorkflowData wrote JSON.stringify(workflow) raw into a blank same-origin window, so a workflow containing " }); + assert.ok(html.includes("</pre><script>"), html); + assert.ok(!html.includes(" + diff --git a/web/gallery.html b/web/gallery.html index ea388e6..4185bf9 100644 --- a/web/gallery.html +++ b/web/gallery.html @@ -6,6 +6,7 @@ PromptManager - Gallery + diff --git a/web/js/admin.js b/web/js/admin.js index eb716bd..a9ded6b 100644 --- a/web/js/admin.js +++ b/web/js/admin.js @@ -1434,14 +1434,21 @@ content.classList.remove("hidden"); empty.classList.add("hidden"); - content.innerHTML = images.map((image, index) => ` + const formatFileSize = (bytes) => this.formatFileSize(bytes); + content.innerHTML = images.map((image, index) => { + const generated = new Date(image.generation_time); + const when = escapeHtml(`${generated.toLocaleDateString()} ${generated.toLocaleTimeString()}`); + const fileUrl = `/prompt_manager/images/${escapeHtml(image.id)}/file`; + const caption = ImageHelpers.formatImageCaption(image, { formatFileSize }); + const details = ImageHelpers.formatImageCaption(image, { formatFileSize, separator: ' • ' }); + return `
- Generated image ${index + 1}
@@ -1451,15 +1458,15 @@
- ${new Date(image.generation_time).toLocaleDateString()} ${new Date(image.generation_time).toLocaleTimeString()} + ${when}
- ${image.width && image.height ? `${image.width}×${image.height}` : 'Unknown size'} - ${image.file_size ? ` • ${this.formatFileSize(image.file_size)}` : ''} + ${details}
- `).join(''); + `; + }).join(''); // Store images for navigation this.currentGalleryImages = images; @@ -2856,15 +2863,12 @@ showWorkflowData() { if (this.currentMetadata && this.currentMetadata.workflow) { const newWindow = window.open('', '_blank'); - newWindow.document.write(` - - ComfyUI Workflow Data - -

ComfyUI Workflow JSON

-
${JSON.stringify(this.currentMetadata.workflow, null, 2)}
- - - `); + if (!newWindow) { + this.showNotification('Popup blocked: allow popups to view the workflow', 'warning'); + return; + } + newWindow.document.write(ImageHelpers.workflowDocumentHtml(this.currentMetadata.workflow)); + newWindow.document.close(); } } diff --git a/web/js/gallery.js b/web/js/gallery.js index 2a00939..bc926ac 100644 --- a/web/js/gallery.js +++ b/web/js/gallery.js @@ -1461,15 +1461,12 @@ showWorkflowData() { if (this.currentMetadata && this.currentMetadata.workflow) { const newWindow = window.open('', '_blank'); - newWindow.document.write(` - - ComfyUI Workflow Data - -

ComfyUI Workflow JSON

-
${JSON.stringify(this.currentMetadata.workflow, null, 2)}
- - - `); + if (!newWindow) { + this.showNotification('Popup blocked: allow popups to view the workflow', 'warning'); + return; + } + newWindow.document.write(ImageHelpers.workflowDocumentHtml(this.currentMetadata.workflow)); + newWindow.document.close(); } } diff --git a/web/js/image-helpers.js b/web/js/image-helpers.js new file mode 100644 index 0000000..9c29786 --- /dev/null +++ b/web/js/image-helpers.js @@ -0,0 +1,68 @@ +/** + * Pure helpers for image entries rendered by the admin and gallery pages. + * + * Image rows come from the database (`width`, `height`, `file_size` are stored + * from a POST body and may be strings) or from the output-folder scan (`id` is + * a path digest, `path` is the file). Everything these helpers return for HTML + * is already escaped, so callers can drop it into text or a quoted attribute. + * + * Loadable in the browser (window.ImageHelpers) and in Node for tests. + */ +(function (root) { + "use strict"; + + const escapeHtml = + typeof module !== "undefined" && module.exports + ? require("./html-escape.js").escapeHtml + : root.escapeHtml; + + const UNKNOWN_SIZE = "Unknown size"; + + /** + * "640×480 | 1.2 MB" for a card body or a viewer caption, escaped. + * + * @param {{width?: unknown, height?: unknown, file_size?: unknown}|null|undefined} image + * @param {{formatFileSize?: (bytes: number) => string, separator?: string}} [options] + * @returns {string} escaped text; "Unknown size" when a dimension is missing + */ + function formatImageCaption(image, { formatFileSize, separator = " | " } = {}) { + const entry = image || {}; + const parts = []; + if (entry.width && entry.height) { + parts.push(`${escapeHtml(entry.width)}×${escapeHtml(entry.height)}`); + } else { + parts.push(UNKNOWN_SIZE); + } + const bytes = Number(entry.file_size); + if (formatFileSize && Number.isFinite(bytes) && bytes > 0) { + parts.push(escapeHtml(formatFileSize(bytes))); + } + return parts.join(escapeHtml(separator)); + } + + /** + * A complete standalone page showing a workflow as pretty-printed JSON. + * Written into a blank same-origin window, so the JSON must not be able + * to close the
 and inject markup.
+     *
+     * @param {unknown} workflow
+     * @returns {string} HTML document
+     */
+    function workflowDocumentHtml(workflow) {
+        const json = escapeHtml(JSON.stringify(workflow, null, 2));
+        return [
+            "",
+            "",
+            "ComfyUI Workflow Data",
+            "",
+            "

ComfyUI Workflow JSON

", + `
${json}
`, + "", + "", + ].join("\n"); + } + + const api = { formatImageCaption, workflowDocumentHtml }; + if (typeof module !== "undefined" && module.exports) module.exports = api; + else root.ImageHelpers = api; +})(typeof window !== "undefined" ? window : globalThis);