VYPR
Medium severity6.1NVD Advisory· Published May 18, 2026· Updated May 18, 2026

CVE-2026-45231

CVE-2026-45231

Description

DumbAssets through 1.0.11 contains a stored cross-site scripting vulnerability in asset fields including name, description, modelNumber, serialNumber, and tags that are stored without server-side sanitization and rendered using innerHTML without client-side escaping. Attackers can create or update assets with HTML or JavaScript payloads via the asset API endpoints to execute arbitrary scripts in the browsers of users viewing the asset list, and with Content-Security-Policy disabled, the injected scripts can make unrestricted connections to internal network services.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

DumbAssets prior to 1.0.12 stores XSS payloads in asset fields via API, executed when users view asset lists via innerHTML.

Vulnerability

DumbAssets through version 1.0.11 contains a stored cross-site scripting (XSS) vulnerability in multiple asset fields: name, description, modelNumber, serialNumber, and tags. These fields are stored without server-side sanitization and are rendered using innerHTML without client-side escaping. The vulnerable code paths include asset and sub-asset rendering in details, info, file grid, maintenance events, sidebar list, file previews, sub-asset lists, nested children, tag manager, and dashboard events table [1], [2].

Exploitation

An attacker can create or update an asset via the asset API endpoints by injecting HTML or JavaScript payloads into any of the unsanitized fields. No authentication is mentioned as a requirement; any user with write access to assets can perform this. When a victim views the asset list or any page that renders the compromised asset fields, the payload executes in their browser. If the browser's Content-Security-Policy is disabled, the injected scripts can make unrestricted connections to internal network services [2].

Impact

Successful exploitation leads to arbitrary JavaScript execution in the context of the victim's browser session. This can result in disclosure of sensitive information, session hijacking, or—if CSP is disabled—potential lateral movement to internal network services accessible from the victim's machine. The attacker gains the same privileges as the victim user within the application [2].

Mitigation

The fix is implemented in pull request #135, which introduces an escapeHtml() and safeUrl() function in src/services/render/escape.js. These are imported by all rendering sites that use innerHTML, neutralizing javascript:, data:, and vbscript: URIs in asset.link and escaping user-controlled fields at the render layer. The fix protects existing entries and prevents re-introduction of payloads via edit/save round-trips [1]. Users should upgrade to version 1.0.12 or later as soon as it is released. As of the publication date, no workaround is documented; disabling CSP may prevent some script execution but is not a recommended mitigation [2].

AI Insight generated on May 21, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected products

1

Patches

1
aed3afa9cc42

Merge 44002383d5114a56dde3465eac3f6758fb1eb775 into 2bacca1e2c4ff8e60d562c8dc300a8dbe91083c3

https://github.com/DumbWareio/DumbAssetsYoanMay 16, 2026via nvd-ref
6 files changed · +102 69
  • public/managers/dashboardManager.js+7 6 modified
    @@ -3,6 +3,7 @@
      * Handles dashboard rendering, events collection, and events display functionality
      */
     import { formatDate } from '../helpers/utils.js';
    +import { escapeHtml } from '/src/services/render/escape.js';
     
     export class DashboardManager {
         constructor({
    @@ -1091,20 +1092,20 @@ export class DashboardManager {
                     </svg>`;
     
                 return `
    -                <div class="event-row ${urgencyClass}" data-type="${event.type}" data-id="${event.id}" data-is-sub-asset="${event.isSubAsset}" style="cursor: pointer;">
    +                <div class="event-row ${urgencyClass}" data-type="${escapeHtml(event.type)}" data-id="${escapeHtml(event.id)}" data-is-sub-asset="${escapeHtml(event.isSubAsset)}" style="cursor: pointer;">
                         <div class="event-type">
                             ${typeIcon}
    -                        <span class="event-type-pill ${event.type}">${event.type === 'warranty' ? 'Warranty' : 'Maintenance'}</span>
    +                        <span class="event-type-pill ${escapeHtml(event.type)}">${event.type === 'warranty' ? 'Warranty' : 'Maintenance'}</span>
                         </div>
                         <div class="event-date">
                             <span class="event-date-text">${this.formatDate(event.date)}</span>
                             <span class="event-days-until">${isPast ? `${Math.abs(daysUntil)} days past` : `${daysUntil} days`}</span>
                         </div>
                         <div class="event-details">
    -                        <div class="event-name">${event.name}</div>
    -                        <div class="event-description">${event.details}</div>
    -                        ${(event.assetType === 'Component' || event.assetType === 'Sub-Component') && event.parentAsset ? `<div class="event-parent">Parent: ${event.parentAsset}</div>` : ''}
    -                        ${event.notes ? `<div class="event-notes">Notes: ${event.notes}</div>` : ''}
    +                        <div class="event-name">${escapeHtml(event.name)}</div>
    +                        <div class="event-description">${escapeHtml(event.details)}</div>
    +                        ${(event.assetType === 'Component' || event.assetType === 'Sub-Component') && event.parentAsset ? `<div class="event-parent">Parent: ${escapeHtml(event.parentAsset)}</div>` : ''}
    +                        ${event.notes ? `<div class="event-notes">Notes: ${escapeHtml(event.notes)}</div>` : ''}
                         </div>
                     </div>
                 `;
    
  • public/script.js+27 26 modified
    @@ -13,6 +13,7 @@ new GlobalHandlers();
     // Import file upload module
     import { initializeFileUploads, handleFileUploads } from '/src/services/fileUpload/index.js';
     import { formatFileSize } from '/src/services/fileUpload/utils.js';
    +import { escapeHtml } from '/src/services/render/escape.js';
     // Import asset renderer module
     import { 
         initRenderer, 
    @@ -792,12 +793,12 @@ document.addEventListener('DOMContentLoaded', () => {
             details.className = 'sub-asset-details';
             details.innerHTML = `
                 ${warrantyDot}
    -            <div class="sub-asset-title">${subAsset.name}</div>
    +            <div class="sub-asset-title">${escapeHtml(subAsset.name)}</div>
                 <div class="sub-asset-actions">
    -                <button class="edit-sub-btn" data-id="${subAsset.id}" title="Edit">
    +                <button class="edit-sub-btn" data-id="${escapeHtml(subAsset.id)}" title="Edit">
                     <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19.5 3 21l1.5-4L16.5 3.5z"/></svg>
                     </button>
    -                <button class="delete-sub-btn" data-id="${subAsset.id}" title="Delete">
    +                <button class="delete-sub-btn" data-id="${escapeHtml(subAsset.id)}" title="Delete">
                     <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2v2"/><line x1="10" y1="11" x2="10" y2="17"/><line x1="14" y1="11" x2="14" y2="17"/></svg>
                     </button>
                 </div>
    @@ -825,12 +826,12 @@ document.addEventListener('DOMContentLoaded', () => {
             // Create model/serial info and tags section
             info.innerHTML = `
                 <div>
    -                ${subAsset.modelNumber ? `<span>${subAsset.modelNumber}</span>` : ''}
    -                ${subAsset.serialNumber ? `<span>#${subAsset.serialNumber}</span>` : ''}
    +                ${subAsset.modelNumber ? `<span>${escapeHtml(subAsset.modelNumber)}</span>` : ''}
    +                ${subAsset.serialNumber ? `<span>#${escapeHtml(subAsset.serialNumber)}</span>` : ''}
                 </div>
                 ${subAsset.tags && subAsset.tags.length > 0 ? `
                 <div class="tag-list">
    -                ${subAsset.tags.map(tag => `<span class="tag" data-tag="${tag}">${tag}</span>`).join('')}
    +                ${subAsset.tags.map(tag => `<span class="tag" data-tag="${escapeHtml(tag)}">${escapeHtml(tag)}</span>`).join('')}
                 </div>`: ''}
             `;
             
    @@ -877,17 +878,17 @@ document.addEventListener('DOMContentLoaded', () => {
                 if (subAsset.photoPath) {
                     files.innerHTML += `
                         <div class="compact-file-item photo">
    -                        <a href="${formatFilePath(subAsset.photoPath)}" target="_blank">
    -                            <img src="${formatFilePath(subAsset.photoPath)}" alt="${subAsset.name}" class="compact-asset-image">
    +                        <a href="${escapeHtml(formatFilePath(subAsset.photoPath))}" target="_blank">
    +                            <img src="${escapeHtml(formatFilePath(subAsset.photoPath))}" alt="${escapeHtml(subAsset.name)}" class="compact-asset-image">
                             </a>
                         </div>
                     `;
                 }
    -            
    +
                 if (subAsset.receiptPath) {
                     files.innerHTML += `
                         <div class="compact-file-item receipt">
    -                        <a href="${formatFilePath(subAsset.receiptPath)}" target="_blank">
    +                        <a href="${escapeHtml(formatFilePath(subAsset.receiptPath))}" target="_blank">
                                 <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
                                     <path stroke="none" d="M0 0h24v24H0z" fill="none"/>
                                     <path d="M5 21v-16a2 2 0 0 1 2 -2h10a2 2 0 0 1 2 2v16l-3 -2l-2 2l-2 -2l-2 2l-2 -2l-3 2"/>
    @@ -899,11 +900,11 @@ document.addEventListener('DOMContentLoaded', () => {
                         </div>
                     `;
                 }
    -            
    +
                 if (subAsset.manualPath) {
                     files.innerHTML += `
                         <div class="compact-file-item manual">
    -                        <a href="${formatFilePath(subAsset.manualPath)}" target="_blank">
    +                        <a href="${escapeHtml(formatFilePath(subAsset.manualPath))}" target="_blank">
                                 <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
                                     <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
                                     <path d="M14 2v6h6"/>
    @@ -961,12 +962,12 @@ document.addEventListener('DOMContentLoaded', () => {
                         childDetails.className = 'sub-asset-details';
                         childDetails.innerHTML = `
                             ${childWarrantyDot}
    -                        <div class="sub-asset-title">${child.name}</div>
    +                        <div class="sub-asset-title">${escapeHtml(child.name)}</div>
                             <div class="sub-asset-actions">
    -                            <button class="edit-sub-btn" data-id="${child.id}" title="Edit">
    +                            <button class="edit-sub-btn" data-id="${escapeHtml(child.id)}" title="Edit">
                                 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19.5 3 21l1.5-4L16.5 3.5z"/></svg>
                                 </button>
    -                            <button class="delete-sub-btn" data-id="${child.id}" title="Delete">
    +                            <button class="delete-sub-btn" data-id="${escapeHtml(child.id)}" title="Delete">
                                 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2v2"/><line x1="10" y1="11" x2="10" y2="17"/><line x1="14" y1="11" x2="14" y2="17"/></svg>
                                 </button>
                             </div>
    @@ -978,12 +979,12 @@ document.addEventListener('DOMContentLoaded', () => {
                         childInfo.className = 'sub-asset-info';
                         childInfo.innerHTML = `
                             <div>
    -                            ${child.modelNumber ? `<span>${child.modelNumber}</span>` : ''}
    -                            ${child.serialNumber ? `<span>#${child.serialNumber}</span>` : ''}
    +                            ${child.modelNumber ? `<span>${escapeHtml(child.modelNumber)}</span>` : ''}
    +                            ${child.serialNumber ? `<span>#${escapeHtml(child.serialNumber)}</span>` : ''}
                             </div>
                             ${child.tags && child.tags.length > 0 ? `
                             <div class="tag-list">
    -                            ${child.tags.map(tag => `<span class="tag" data-tag="${tag}">${tag}</span>`).join('')}
    +                            ${child.tags.map(tag => `<span class="tag" data-tag="${escapeHtml(tag)}">${escapeHtml(tag)}</span>`).join('')}
                             </div>`: ''}
                         `;
                         childElement.appendChild(childInfo);
    @@ -1028,17 +1029,17 @@ document.addEventListener('DOMContentLoaded', () => {
                             if (child.photoPath) {
                                 childFiles.innerHTML += `
                                     <div class="compact-file-item photo">
    -                                    <a href="${formatFilePath(child.photoPath)}" target="_blank">
    -                                        <img src="${formatFilePath(child.photoPath)}" alt="${child.name}" class="compact-asset-image">
    +                                    <a href="${escapeHtml(formatFilePath(child.photoPath))}" target="_blank">
    +                                        <img src="${escapeHtml(formatFilePath(child.photoPath))}" alt="${escapeHtml(child.name)}" class="compact-asset-image">
                                         </a>
                                     </div>
                                 `;
                             }
    -                        
    +
                             if (child.receiptPath) {
                                 childFiles.innerHTML += `
                                     <div class="compact-file-item receipt">
    -                                    <a href="${formatFilePath(child.receiptPath)}" target="_blank">
    +                                    <a href="${escapeHtml(formatFilePath(child.receiptPath))}" target="_blank">
                                             <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
                                                 <path stroke="none" d="M0 0h24v24H0z" fill="none"/>
                                                 <path d="M5 21v-16a2 2 0 0 1 2 -2h10a2 2 0 0 1 2 2v16l-3 -2l-2 2l-2 -2l-2 2l-2 -2l-3 2"/>
    @@ -1050,11 +1051,11 @@ document.addEventListener('DOMContentLoaded', () => {
                                     </div>
                                 `;
                             }
    -                        
    +
                             if (child.manualPath) {
                                 childFiles.innerHTML += `
                                     <div class="compact-file-item manual">
    -                                    <a href="${formatFilePath(child.manualPath)}" target="_blank">
    +                                    <a href="${escapeHtml(formatFilePath(child.manualPath))}" target="_blank">
                                             <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
                                                 <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
                                                 <path d="M14 2v6h6"/>
    @@ -1223,8 +1224,8 @@ document.addEventListener('DOMContentLoaded', () => {
                 if (!container) return;
                 container.innerHTML = Array.from(tags).map(tag => `
                     <span class="tag">
    -                    ${tag}
    -                    <button class="remove-tag" data-tag="${tag}" title="Remove tag">
    +                    ${escapeHtml(tag)}
    +                    <button class="remove-tag" data-tag="${escapeHtml(tag)}" title="Remove tag">
                             <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
                                 <line x1="18" y1="6" x2="6" y2="18"></line>
                                 <line x1="6" y1="6" x2="18" y2="18"></line>
    
  • src/services/render/assetRenderer.js+33 31 modified
    @@ -3,6 +3,8 @@
      * Handles rendering of asset details, sub-assets, and related UI components
      */
     
    +import { escapeHtml, safeUrl } from './escape.js';
    +
     // Import utility functions if needed
     // These will be injected when we use the module
     let formatDate;
    @@ -135,12 +137,12 @@ function generateMaintenanceEventsHTML(maintenanceEvents) {
             return `
                 <div class="maintenance-event-item">
                     <div class="maintenance-event-line">
    -                    <strong>Event: ${event.name}</strong>
    -                    <span class="maintenance-schedule-inline">${typeText} - ${scheduleText}</span>
    +                    <strong>Event: ${escapeHtml(event.name)}</strong>
    +                    <span class="maintenance-schedule-inline">${typeText} - ${escapeHtml(scheduleText)}</span>
                     </div>
                     ${event.notes ? `
                     <div class="maintenance-notes-line">
    -                    <strong>Notes:</strong> ${event.notes}
    +                    <strong>Notes:</strong> ${escapeHtml(event.notes)}
                     </div>
                     ` : ''}
                 </div>
    @@ -166,15 +168,15 @@ function generateAssetInfoHTML(asset) {
         return `
             <div class="info-item">
                 <div class="info-label">Manufacturer</div>
    -            <div>${asset.manufacturer || 'N/A'}</div>
    +            <div>${asset.manufacturer ? escapeHtml(asset.manufacturer) : 'N/A'}</div>
             </div>
             <div class="info-item">
                 <div class="info-label">Model Number</div>
    -            <div>${asset.modelNumber || 'N/A'}</div>
    +            <div>${asset.modelNumber ? escapeHtml(asset.modelNumber) : 'N/A'}</div>
             </div>
             <div class="info-item">
                 <div class="info-label">Serial Number</div>
    -            <div>${asset.serialNumber || 'N/A'}</div>
    +            <div>${asset.serialNumber ? escapeHtml(asset.serialNumber) : 'N/A'}</div>
             </div>
             <div class="info-item">
                 <div class="info-label">Purchase Date</div>
    @@ -197,21 +199,21 @@ function generateAssetInfoHTML(asset) {
             ${asset.warranty?.expirationDate || asset.warranty?.isLifetime ? `
             <div class="info-item">
                 <div class="info-label">Warranty</div>
    -            ${asset.warranty.scope ? `<div>${asset.warranty.scope}</div>` : ''}
    +            ${asset.warranty.scope ? `<div>${escapeHtml(asset.warranty.scope)}</div>` : ''}
                 <div>${asset.warranty.isLifetime ? 'Lifetime' : formatDate(asset.warranty.expirationDate)}</div>
             </div>
             ` : ''}
             ${asset.secondaryWarranty?.expirationDate || asset.secondaryWarranty?.isLifetime ? `
             <div class="info-item">
                 <div class="info-label">Secondary Warranty</div>
    -            ${asset.secondaryWarranty.scope ? `<div>${asset.secondaryWarranty.scope}</div>` : ''}
    +            ${asset.secondaryWarranty.scope ? `<div>${escapeHtml(asset.secondaryWarranty.scope)}</div>` : ''}
                 <div>${asset.secondaryWarranty.isLifetime ? 'Lifetime' : formatDate(asset.secondaryWarranty.expirationDate)}</div>
             </div>
             ` : ''}
             ${asset.link ? `
             <div class="info-item">
                 <div class="info-label">Link</div>
    -            <div><a href="${asset.link}" target="_blank" rel="noopener noreferrer">${asset.link}</a></div>
    +            <div><a href="${escapeHtml(safeUrl(asset.link))}" target="_blank" rel="noopener noreferrer">${escapeHtml(asset.link)}</a></div>
             </div>` : ''}
         `;
     }
    @@ -264,9 +266,9 @@ function generateFileGridHTML(asset) {
                 const fileName = photoInfo.originalName || photoPath.split('/').pop();
                 html += `
                     <div class="file-item photo">
    -                    <a href="${formatFilePath(photoPath)}" target="_blank" class="file-preview">
    -                        <img src="${formatFilePath(photoPath)}" alt="${asset.name}" class="asset-image">
    -                        <div class="file-label">${formatDisplayFileName(fileName)}</div>
    +                    <a href="${escapeHtml(formatFilePath(photoPath))}" target="_blank" class="file-preview">
    +                        <img src="${escapeHtml(formatFilePath(photoPath))}" alt="${escapeHtml(asset.name)}" class="asset-image">
    +                        <div class="file-label">${escapeHtml(formatDisplayFileName(fileName))}</div>
                         </a>
                     </div>
                 `;
    @@ -277,9 +279,9 @@ function generateFileGridHTML(asset) {
             const fileName = photoInfo.originalName || asset.photoPath.split('/').pop();
             html += `
                 <div class="file-item photo">
    -                <a href="${formatFilePath(asset.photoPath)}" target="_blank" class="file-preview">
    -                    <img src="${formatFilePath(asset.photoPath)}" alt="${asset.name}" class="asset-image">
    -                    <div class="file-label">${formatDisplayFileName(fileName)}</div>
    +                <a href="${escapeHtml(formatFilePath(asset.photoPath))}" target="_blank" class="file-preview">
    +                    <img src="${escapeHtml(formatFilePath(asset.photoPath))}" alt="${escapeHtml(asset.name)}" class="asset-image">
    +                    <div class="file-label">${escapeHtml(formatDisplayFileName(fileName))}</div>
                     </a>
                 </div>
             `;
    @@ -292,12 +294,12 @@ function generateFileGridHTML(asset) {
                 const fileName = receiptInfo.originalName || receiptPath.split('/').pop();
                 html += `
                     <div class="file-item receipt">
    -                    <a href="${formatFilePath(receiptPath)}" target="_blank" class="file-preview">
    +                    <a href="${escapeHtml(formatFilePath(receiptPath))}" target="_blank" class="file-preview">
                             <svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
                                 <path stroke="none" d="M0 0h24v24H0z" fill="none"/>
                                 <path d="M5 21v-16a2 2 0 0 1 2 -2h10a2 2 0 0 1 2 2v16l-3 -2l-2 2l-2 -2l-2 2l-2 -2l-3 2m4 -14h6m-6 4h6m-2 4h2" />
                             </svg>
    -                        <div class="file-label">${formatDisplayFileName(fileName)}</div>
    +                        <div class="file-label">${escapeHtml(formatDisplayFileName(fileName))}</div>
                         </a>
                     </div>
                 `;
    @@ -308,12 +310,12 @@ function generateFileGridHTML(asset) {
             const fileName = receiptInfo.originalName || asset.receiptPath.split('/').pop();
             html += `
                 <div class="file-item receipt">
    -                <a href="${formatFilePath(asset.receiptPath)}" target="_blank" class="file-preview">
    +                <a href="${escapeHtml(formatFilePath(asset.receiptPath))}" target="_blank" class="file-preview">
                         <svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
                             <path stroke="none" d="M0 0h24v24H0z" fill="none"/>
                             <path d="M5 21v-16a2 2 0 0 1 2 -2h10a2 2 0 0 1 2 2v16l-3 -2l-2 2l-2 -2l-2 2l-2 -2l-3 2m4 -14h6m-6 4h6m-2 4h2" />
                         </svg>
    -                    <div class="file-label">${formatDisplayFileName(fileName)}</div>
    +                    <div class="file-label">${escapeHtml(formatDisplayFileName(fileName))}</div>
                     </a>
                 </div>
             `;
    @@ -326,15 +328,15 @@ function generateFileGridHTML(asset) {
                 const fileName = manualInfo.originalName || manualPath.split('/').pop();
                 html += `
                     <div class="file-item manual">
    -                    <a href="${formatFilePath(manualPath)}" target="_blank" class="file-preview">
    +                    <a href="${escapeHtml(formatFilePath(manualPath))}" target="_blank" class="file-preview">
                             <svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
                                 <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
                                 <polyline points="14 2 14 8 20 8"></polyline>
                                 <line x1="16" y1="13" x2="8" y2="13"></line>
                                 <line x1="16" y1="17" x2="8" y2="17"></line>
                                 <polyline points="10 9 9 9 8 9"></polyline>
                             </svg>
    -                        <div class="file-label">${formatDisplayFileName(fileName)}</div>
    +                        <div class="file-label">${escapeHtml(formatDisplayFileName(fileName))}</div>
                         </a>
                     </div>
                 `;
    @@ -345,15 +347,15 @@ function generateFileGridHTML(asset) {
             const fileName = manualInfo.originalName || asset.manualPath.split('/').pop();
             html += `
                 <div class="file-item manual">
    -                <a href="${formatFilePath(asset.manualPath)}" target="_blank" class="file-preview">
    +                <a href="${escapeHtml(formatFilePath(asset.manualPath))}" target="_blank" class="file-preview">
                         <svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
                             <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
                             <polyline points="14 2 14 8 20 8"></polyline>
                             <line x1="16" y1="13" x2="8" y2="13"></line>
                             <line x1="16" y1="17" x2="8" y2="17"></line>
                             <polyline points="10 9 9 9 8 9"></polyline>
                         </svg>
    -                    <div class="file-label">${formatDisplayFileName(fileName)}</div>
    +                    <div class="file-label">${escapeHtml(formatDisplayFileName(fileName))}</div>
                     </a>
                 </div>
             `;
    @@ -445,31 +447,31 @@ function renderAssetDetails(assetId, isSubAsset = false) {
                 maintenanceScheduleHtml = `
                     <div class="info-item">
                         <div class="info-label">Maintenance Schedule</div>
    -                    <div>${scheduleText}</div>
    +                    <div>${escapeHtml(scheduleText)}</div>
                     </div>
                 `;
             }
         }
         assetDetails.innerHTML = `
             <fieldset class="dashboard-legend">
    -            <legend class="dashboard-legend-title">${legendTitle}</legend>
    +            <legend class="dashboard-legend-title">${escapeHtml(legendTitle)}</legend>
                 <div class="asset-header">
                     <div class="asset-title">
    -                    <h2>${asset.name}</h2>
    +                    <h2>${escapeHtml(asset.name)}</h2>
                         <div class="asset-meta">
                             Added: ${formatDate(asset.createdAt)}
                             ${asset.updatedAt !== asset.createdAt ? ` • Updated: ${formatDate(asset.updatedAt)}` : ''}
                         </div>
                     </div>
                     <div class="asset-actions">
                         ${isSub ? `<button class="back-to-parent-btn" title="Back to Parent"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 18 9 12 15 6"/></svg></button>` : ''}
    -                    <button class="copy-link-btn" data-id="${asset.id}" data-parent-id="${asset.parentId || ''}" title="Copy Link">
    +                    <button class="copy-link-btn" data-id="${escapeHtml(asset.id)}" data-parent-id="${escapeHtml(asset.parentId || '')}" title="Copy Link">
                           <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg>
                         </button>
    -                    <button class="edit-asset-btn" data-id="${asset.id}" title="Edit">
    +                    <button class="edit-asset-btn" data-id="${escapeHtml(asset.id)}" title="Edit">
                           <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19.5 3 21l1.5-4L16.5 3.5z"/></svg>
                         </button>
    -                    <button class="delete-asset-btn" data-id="${asset.id}" title="Delete">
    +                    <button class="delete-asset-btn" data-id="${escapeHtml(asset.id)}" title="Delete">
                           <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2v2"/><line x1="10" y1="11" x2="10" y2="17"/><line x1="14" y1="11" x2="14" y2="17"/></svg>
                         </button>
                     </div>
    @@ -482,14 +484,14 @@ function renderAssetDetails(assetId, isSubAsset = false) {
                 ${(asset.description || asset.notes) ? `
                 <div class="asset-description">
                     <strong>Description:</strong>
    -                <p>${asset.description || asset.notes}</p>
    +                <p>${escapeHtml(asset.description || asset.notes)}</p>
                 </div>
                 ` : ''}
                 ${asset.tags && asset.tags.length > 0 ? `
                 <div class="info-item" style="margin-bottom: 1rem;">
                     <div class="info-label">Tags</div>
                     <div class="tag-list">
    -                    ${asset.tags.map(tag => `<span class="tag" data-tag="${tag}" style="cursor: pointer;">${tag}</span>`).join('')}
    +                    ${asset.tags.map(tag => `<span class="tag" data-tag="${escapeHtml(tag)}" style="cursor: pointer;">${escapeHtml(tag)}</span>`).join('')}
                     </div>
                 </div>` : ''}
                 <div class="asset-files">
    
  • src/services/render/escape.js+25 0 added
    @@ -0,0 +1,25 @@
    +/**
    + * HTML escaping helpers for rendering user-controlled data into innerHTML.
    + *
    + * Always escape values interpolated into HTML strings — both text content
    + * and attribute values. For URLs used in href/src, also pass them through
    + * safeUrl() to neutralize javascript:/data:/vbscript: schemes.
    + */
    +
    +export function escapeHtml(value) {
    +    if (value === null || value === undefined) return '';
    +    return String(value)
    +        .replace(/&/g, '&amp;')
    +        .replace(/</g, '&lt;')
    +        .replace(/>/g, '&gt;')
    +        .replace(/"/g, '&quot;')
    +        .replace(/'/g, '&#039;');
    +}
    +
    +export function safeUrl(value) {
    +    if (value === null || value === undefined) return '#';
    +    const s = String(value).trim();
    +    if (!s) return '#';
    +    if (/^(javascript|data|vbscript):/i.test(s)) return '#';
    +    return s;
    +}
    
  • src/services/render/listRenderer.js+5 3 modified
    @@ -3,6 +3,8 @@
      * Handles rendering of the asset list sidebar with search and filter functionality
      */
     
    +import { escapeHtml } from './escape.js';
    +
     // These functions from other modules will be injected
     let updateSelectedIds;
     let renderAssetDetails;
    @@ -330,11 +332,11 @@ function renderAssetList(searchQuery = '') {
             
             // Format asset item with name, model, and tags
             assetItem.innerHTML += `
    -            <div class="asset-item-name">${asset.name || 'Unnamed Asset'}</div>
    -            ${asset.modelNumber ? `<div class="asset-item-model">${asset.modelNumber}</div>` : ''}
    +            <div class="asset-item-name">${asset.name ? escapeHtml(asset.name) : 'Unnamed Asset'}</div>
    +            ${asset.modelNumber ? `<div class="asset-item-model">${escapeHtml(asset.modelNumber)}</div>` : ''}
                 ${asset.tags && asset.tags.length > 0 ? `
                     <div class="asset-item-tags">
    -                    ${asset.tags.map(tag => `<span class="asset-tag" data-tag="${tag}">${tag}</span>`).join('')}
    +                    ${asset.tags.map(tag => `<span class="asset-tag" data-tag="${escapeHtml(tag)}">${escapeHtml(tag)}</span>`).join('')}
                     </div>
                 ` : ''}
             `;
    
  • src/services/render/previewRenderer.js+5 3 modified
    @@ -3,6 +3,8 @@
      * Provides centralized functions for rendering file previews consistently across the application
      */
     
    +import { escapeHtml } from './escape.js';
    +
     /**
      * Create a photo preview element
      * 
    @@ -22,7 +24,7 @@ export function createPhotoPreview(filePath, onDeleteCallback, fileName = null,
         previewItem.innerHTML = `
             <div class="file-preview">
                 <div class="preview-content">
    -                <img src="${filePath}" alt="Photo Preview">
    +                <img src="${escapeHtml(filePath)}" alt="Photo Preview">
                 </div>
             </div>
             <button type="button" class="delete-preview-btn" title="Delete Image">
    @@ -34,7 +36,7 @@ export function createPhotoPreview(filePath, onDeleteCallback, fileName = null,
                 </svg>
             </button>
             <div class="file-info-pill">
    -            <span class="file-name">${fileName}</span>
    +            <span class="file-name">${escapeHtml(fileName)}</span>
             </div>
         `;
         
    @@ -108,7 +110,7 @@ export function createDocumentPreview(type, filePath, onDeleteCallback, fileName
                 </svg>
             </button>
             <div class="file-info-pill">
    -            <span class="file-name">${fileName || 'Document'}</span>
    +            <span class="file-name">${fileName ? escapeHtml(fileName) : 'Document'}</span>
             </div>
         `;
         
    

Vulnerability mechanics

Root cause

"Asset fields (name, description, modelNumber, serialNumber, tags, and others) are stored server-side without sanitization and rendered into the DOM via innerHTML without HTML-escaping, enabling stored cross-site scripting."

Attack vector

An attacker with network access to the DumbAssets application can create or update an asset via the asset API endpoints, injecting HTML or JavaScript payloads into fields such as name, description, modelNumber, serialNumber, or tags [CWE-79]. Because the server stores these values without sanitization and the client renders them using innerHTML without escaping, any user who views the asset list or asset detail page will execute the injected script in their browser. If the application's Content-Security-Policy is disabled, the injected script can make unrestricted connections to internal network services, expanding the impact beyond simple cross-site scripting.

Affected code

The vulnerability spans two files: `src/services/render/assetRenderer.js` and `public/script.js`. In `assetRenderer.js`, the functions `generateAssetInfoHTML`, `generateMaintenanceEventsHTML`, `generateFileGridHTML`, and `renderAssetDetails` all interpolate asset fields (name, description, modelNumber, serialNumber, tags, warranty scope, link, etc.) directly into HTML template literals without escaping. In `public/script.js`, the same pattern appears when rendering sub-asset details, child asset details, and file grid items.

What the fix does

The patch introduces an `escapeHtml` function (imported from `./escape.js`) and applies it to every user-controlled value that is interpolated into HTML strings in `assetRenderer.js` and `public/script.js` [patch_id=424432]. For example, `asset.name` becomes `${escapeHtml(asset.name)}`, `asset.modelNumber` becomes `${escapeHtml(asset.modelNumber)}`, and tag values are escaped both in the `data-tag` attribute and the visible text. The `asset.link` field is additionally passed through a `safeUrl` function to prevent `javascript:` or other dangerous URL schemes. These changes ensure that any HTML metacharacters in user-supplied data are converted to their safe entity equivalents, preventing script injection via innerHTML rendering.

Preconditions

  • networkAttacker must have network access to the DumbAssets application's API endpoints.
  • inputAttacker must be able to create or update an asset via the asset API, supplying payloads in name, description, modelNumber, serialNumber, or tags fields.

Generated on May 19, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

2

News mentions

0

No linked articles in our index yet.