From bf8842e1d7f8ed0d2f1830f5c41237a000e6fb66 Mon Sep 17 00:00:00 2001 From: cproudlock Date: Wed, 29 Jul 2026 10:06:18 -0400 Subject: [PATCH] applications: render Application Notes as sanitized HTML The notes field is authored as HTML (the form says "HTML supported") but the detail page interpolated it with {{ }}, so tags like
showed as literal text. Render via v-html through a DOMPurify sanitizer (utils/sanitizeHtml): allow-list of formatting tags + links only, forces target=_blank rel=noopener on links, strips scripts/handlers. Promote dompurify to a direct dependency (was transitive via jspdf). --- frontend/package.json | 1 + frontend/src/utils/sanitizeHtml.js | 31 +++++++++++++++++++ .../frontend/views/ApplicationDetail.vue | 22 +++++++++++-- 3 files changed, 51 insertions(+), 3 deletions(-) create mode 100644 frontend/src/utils/sanitizeHtml.js diff --git a/frontend/package.json b/frontend/package.json index 49b9aca..37f0dfb 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -20,6 +20,7 @@ "@fullcalendar/daygrid": "^6.1.20", "@fullcalendar/vue3": "^6.1.20", "axios": "^1.6.0", + "dompurify": "^3.4.11", "jsbarcode": "^3.12.3", "jspdf": "^4.2.1", "leaflet": "^1.9.4", diff --git a/frontend/src/utils/sanitizeHtml.js b/frontend/src/utils/sanitizeHtml.js new file mode 100644 index 0000000..8ad887f --- /dev/null +++ b/frontend/src/utils/sanitizeHtml.js @@ -0,0 +1,31 @@ +// Safe rendering of user-authored notes HTML (e.g. Application Notes, which the +// form advertises as "HTML supported"). DOMPurify strips scripts, event +// handlers, and any active/unsafe content; we allow only basic formatting + +// links. Never v-html raw notes without this. +import DOMPurify from 'dompurify' + +// Force every surviving link to open safely: new tab + no window.opener handle. +DOMPurify.addHook('afterSanitizeAttributes', (node) => { + if (node.tagName === 'A' && node.getAttribute('href')) { + node.setAttribute('target', '_blank') + node.setAttribute('rel', 'noopener noreferrer') + } +}) + +const ALLOWED_TAGS = [ + 'p', 'br', 'hr', 'b', 'strong', 'i', 'em', 'u', 's', 'span', 'div', + 'a', 'ul', 'ol', 'li', 'blockquote', 'code', 'pre', + 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', + 'table', 'thead', 'tbody', 'tr', 'th', 'td', +] +const ALLOWED_ATTR = ['href', 'title', 'target', 'rel'] + +// Return a sanitized HTML string safe to bind with v-html. Empty in -> empty out. +export function sanitizeNotesHtml(html) { + if (!html) return '' + return DOMPurify.sanitize(String(html), { + ALLOWED_TAGS, + ALLOWED_ATTR, + ALLOW_DATA_ATTR: false, + }) +} diff --git a/plugins/applications/frontend/views/ApplicationDetail.vue b/plugins/applications/frontend/views/ApplicationDetail.vue index 91de7f6..aa5da35 100644 --- a/plugins/applications/frontend/views/ApplicationDetail.vue +++ b/plugins/applications/frontend/views/ApplicationDetail.vue @@ -79,7 +79,9 @@

Application Notes

-
{{ app.applicationnotes }}
+ +
@@ -160,10 +162,11 @@