Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 7 additions & 6 deletions public/managers/dashboardManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down Expand Up @@ -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>
`;
Expand Down
53 changes: 27 additions & 26 deletions public/script.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== formatFilePath definition(s) =="
rg -n --type=js -C3 '\bformatFilePath\b'

echo
echo "== URL sinks using formatFilePath =="
rg -n --type=js -C2 'href="\$\{[^}]*formatFilePath\(|src="\$\{[^}]*formatFilePath\('

echo
echo "== Existing safeUrl usage with formatFilePath =="
rg -n --type=js -C2 'safeUrl\(\s*formatFilePath\('

Repository: DumbWareio/DumbAssets

Length of output: 29096


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Get full formatFilePath implementation
echo "== Full formatFilePath implementation =="
sed -n '97,120p' src/services/render/assetRenderer.js

echo
echo "== escape.js exports and safeUrl check =="
cat src/services/render/escape.js | head -100

Repository: DumbWareio/DumbAssets

Length of output: 1750


Use safeUrl() for URL sinks (href/src), not escapeHtml().

Your code pipes escapeHtml(formatFilePath(...)) into href and src attributes. Problem: escapeHtml() only escapes HTML characters; it doesn't block dangerous URL schemes. If formatFilePath() returns a URL starting with http:// or https://, it passes through unmodified. A malicious URL like https://example.com/javascript:alert(1) bypasses escapeHtml entirely and reaches the browser's URL parser.

The safeUrl() function already exists in your escape module and explicitly neutralizes javascript:, data:, and vbscript: schemes. Use it for all URL attributes.

Fix
-import { escapeHtml } from '/src/services/render/escape.js';
+import { escapeHtml, safeUrl } from '/src/services/render/escape.js';

Then replace escapeHtml(formatFilePath(...)) with safeUrl(formatFilePath(...)) in these URL sinks:

  • Lines 881-882: href and src for subAsset.photoPath
  • Line 891: href for subAsset.receiptPath
  • Line 907: href for subAsset.manualPath
  • Lines 1032-1033: href and src for child.photoPath
  • Line 1042: href for child.receiptPath
  • Line 1058: href for child.manualPath
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@public/script.js` at line 16, Replace usage of escapeHtml(...) for URL sinks
with safeUrl(...) and import safeUrl from the escape module: update the import
line to include safeUrl (e.g., import { escapeHtml, safeUrl } from
'/src/services/render/escape.js') and then change
escapeHtml(formatFilePath(...)) to safeUrl(formatFilePath(...)) for the URL
attributes referencing subAsset.photoPath (href and src), subAsset.receiptPath
(href), subAsset.manualPath (href), child.photoPath (href and src),
child.receiptPath (href), and child.manualPath (href); keep escapeHtml for
non-URL HTML text sinks.

// Import asset renderer module
import {
initRenderer,
Expand Down Expand Up @@ -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>
Expand Down Expand Up @@ -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>`: ''}
`;

Expand Down Expand Up @@ -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"/>
Expand All @@ -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"/>
Expand Down Expand Up @@ -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>
Expand All @@ -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);
Expand Down Expand Up @@ -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"/>
Expand All @@ -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"/>
Expand Down Expand Up @@ -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>
Expand Down
Loading