max / goingson
6 files changed,
+28 insertions,
-8 deletions
| @@ -7,6 +7,7 @@ | |||
| 7 | 7 | 'use strict'; | |
| 8 | 8 | const esc = GoingsOn.utils.escapeHtml; | |
| 9 | 9 | const escAttr = GoingsOn.utils.escapeAttr; | |
| 10 | + | const escAttrVal = GoingsOn.utils.escapeAttrValue; | |
| 10 | 11 | ||
| 11 | 12 | // MIME type to icon mapping | |
| 12 | 13 | const MIME_ICONS = { | |
| @@ -60,7 +61,7 @@ | |||
| 60 | 61 | <span class="attachment-icon">${getIcon(a.mimeType)}</span> | |
| 61 | 62 | <div class="attachment-info"> | |
| 62 | 63 | <div class="attachment-filename" | |
| 63 | - | title="${escAttr(a.filename)}">${esc(a.filename)}</div> | |
| 64 | + | title="${escAttrVal(a.filename)}">${esc(a.filename)}</div> | |
| 64 | 65 | <div class="attachment-meta">${esc(a.fileSizeFormatted)}</div> | |
| 65 | 66 | </div> | |
| 66 | 67 | <div class="attachment-actions"> |
| @@ -7,6 +7,8 @@ | |||
| 7 | 7 | 'use strict'; | |
| 8 | 8 | const esc = GoingsOn.utils.escapeHtml; | |
| 9 | 9 | const escAttr = GoingsOn.utils.escapeAttr; | |
| 10 | + | const escAttrVal = GoingsOn.utils.escapeAttrValue; | |
| 11 | + | const safeUrl = GoingsOn.utils.safeUrl; | |
| 10 | 12 | ||
| 11 | 13 | // ============ Helpers ============ | |
| 12 | 14 | ||
| @@ -98,7 +100,7 @@ | |||
| 98 | 100 | // Social handles | |
| 99 | 101 | const socialRows = (contact.socialHandles || []).map(s => ` | |
| 100 | 102 | <div class="sub-item"> | |
| 101 | - | <span><strong>${esc(s.platform)}:</strong> ${s.url ? `<a href="${escAttr(s.url)}" target="_blank">${esc(s.handle)}</a>` : esc(s.handle)}</span> | |
| 103 | + | <span><strong>${esc(s.platform)}:</strong> ${s.url ? `<a href="${escAttrVal(safeUrl(s.url))}" target="_blank">${esc(s.handle)}</a>` : esc(s.handle)}</span> | |
| 102 | 104 | <button class="btn btn-sm btn-danger" onclick="GoingsOn.contacts.removeSocialHandle('${escAttr(contact.id)}', '${escAttr(s.id)}')" title="Remove">x</button> | |
| 103 | 105 | </div> | |
| 104 | 106 | `).join(''); | |
| @@ -106,7 +108,7 @@ | |||
| 106 | 108 | // Custom fields | |
| 107 | 109 | const customFieldRows = (contact.customFields || []).map(f => ` | |
| 108 | 110 | <div class="sub-item"> | |
| 109 | - | <span><strong>${esc(f.label)}:</strong> ${f.url ? `<a href="${escAttr(f.url)}" target="_blank">${esc(f.value)}</a>` : esc(f.value)}</span> | |
| 111 | + | <span><strong>${esc(f.label)}:</strong> ${f.url ? `<a href="${escAttrVal(safeUrl(f.url))}" target="_blank">${esc(f.value)}</a>` : esc(f.value)}</span> | |
| 110 | 112 | <button class="btn btn-sm btn-danger" onclick="GoingsOn.contacts.removeCustomField('${escAttr(contact.id)}', '${escAttr(f.id)}')" title="Remove">x</button> | |
| 111 | 113 | </div> | |
| 112 | 114 | `).join(''); |
| @@ -7,6 +7,7 @@ | |||
| 7 | 7 | 'use strict'; | |
| 8 | 8 | const esc = GoingsOn.utils.escapeHtml; | |
| 9 | 9 | const escAttr = GoingsOn.utils.escapeAttr; | |
| 10 | + | const escAttrVal = GoingsOn.utils.escapeAttrValue; | |
| 10 | 11 | ||
| 11 | 12 | let currentMonthDate = new Date(); | |
| 12 | 13 | let currentWeekDate = new Date(); | |
| @@ -126,7 +127,7 @@ | |||
| 126 | 127 | const maxShow = 3; | |
| 127 | 128 | dayEvents.slice(0, maxShow).forEach(e => { | |
| 128 | 129 | const blockClass = e.blockType ? `block-${e.blockType}` : ''; | |
| 129 | - | html += `<div class="cal-event-chip ${blockClass}" onclick="event.stopPropagation(); GoingsOn.events.open('${escAttr(e.id)}')" title="${escAttr(e.title)}">${esc(truncate(e.title, 18))}</div>`; | |
| 130 | + | html += `<div class="cal-event-chip ${blockClass}" onclick="event.stopPropagation(); GoingsOn.events.open('${escAttr(e.id)}')" title="${escAttrVal(e.title)}">${esc(truncate(e.title, 18))}</div>`; | |
| 130 | 131 | }); | |
| 131 | 132 | if (dayEvents.length > maxShow) { | |
| 132 | 133 | html += `<div class="cal-event-more">+${dayEvents.length - maxShow} more</div>`; | |
| @@ -254,7 +255,7 @@ | |||
| 254 | 255 | html += '<div class="cal-week-allday-cell">'; | |
| 255 | 256 | dayEvts.forEach(e => { | |
| 256 | 257 | const blockClass = e.blockType ? `block-${e.blockType}` : ''; | |
| 257 | - | html += `<div class="cal-event-chip ${blockClass}" onclick="GoingsOn.events.open('${escAttr(e.id)}')" title="${escAttr(e.title)}">${esc(truncate(e.title, 14))}</div>`; | |
| 258 | + | html += `<div class="cal-event-chip ${blockClass}" onclick="GoingsOn.events.open('${escAttr(e.id)}')" title="${escAttrVal(e.title)}">${esc(truncate(e.title, 14))}</div>`; | |
| 258 | 259 | }); | |
| 259 | 260 | html += '</div>'; | |
| 260 | 261 | adCursor.setDate(adCursor.getDate() + 1); | |
| @@ -298,7 +299,7 @@ | |||
| 298 | 299 | const heightPx = Math.max((durationMinutes / 15) * SLOT_HEIGHT, SLOT_HEIGHT); | |
| 299 | 300 | const blockClass = e.blockType ? `block-${e.blockType}` : ''; | |
| 300 | 301 | ||
| 301 | - | html += `<div class="cal-week-event ${blockClass}" style="top: ${topPx}px; height: ${heightPx}px;" onclick="GoingsOn.events.open('${escAttr(e.id)}')" title="${escAttr(e.title + ' ' + e.timeFormatted)}"> | |
| 302 | + | html += `<div class="cal-week-event ${blockClass}" style="top: ${topPx}px; height: ${heightPx}px;" onclick="GoingsOn.events.open('${escAttr(e.id)}')" title="${escAttrVal(e.title + ' ' + e.timeFormatted)}"> | |
| 302 | 303 | <div class="cal-week-event-title">${esc(truncate(e.title, 20))}</div> | |
| 303 | 304 | <div class="cal-week-event-time">${esc(e.timeFormatted)}</div> | |
| 304 | 305 | </div>`; |
| @@ -7,6 +7,7 @@ | |||
| 7 | 7 | 'use strict'; | |
| 8 | 8 | const esc = GoingsOn.utils.escapeHtml; | |
| 9 | 9 | const escAttr = GoingsOn.utils.escapeAttr; | |
| 10 | + | const escAttrVal = GoingsOn.utils.escapeAttrValue; | |
| 10 | 11 | ||
| 11 | 12 | // ============ State ============ | |
| 12 | 13 | ||
| @@ -268,7 +269,7 @@ | |||
| 268 | 269 | <tr> | |
| 269 | 270 | ${columns.map(c => { | |
| 270 | 271 | const value = data[c.key] || ''; | |
| 271 | - | return `<td title="${escAttr(value)}">${esc(truncate(value, 50))}</td>`; | |
| 272 | + | return `<td title="${escAttrVal(value)}">${esc(truncate(value, 50))}</td>`; | |
| 272 | 273 | }).join('')} | |
| 273 | 274 | </tr> | |
| 274 | 275 | `; |
| @@ -7,6 +7,7 @@ | |||
| 7 | 7 | 'use strict'; | |
| 8 | 8 | const esc = GoingsOn.utils.escapeHtml; | |
| 9 | 9 | const escAttr = GoingsOn.utils.escapeAttr; | |
| 10 | + | const escAttrVal = GoingsOn.utils.escapeAttrValue; | |
| 10 | 11 | ||
| 11 | 12 | // ============ Task Rendering Helpers ============ | |
| 12 | 13 | ||
| @@ -96,7 +97,7 @@ | |||
| 96 | 97 | <span class="task-description-text">${esc(displayDesc)}</span> | |
| 97 | 98 | ${renderTimeBadge(t)} | |
| 98 | 99 | ${renderTaskBadges(t)} | |
| 99 | - | ${t.contactName ? `<span class="contact-badge" title="${escAttr(t.contactName)}">${esc(t.contactName)}</span>` : ''} | |
| 100 | + | ${t.contactName ? `<span class="contact-badge" title="${escAttrVal(t.contactName)}">${esc(t.contactName)}</span>` : ''} | |
| 100 | 101 | ${t.isSnoozed ? `<span class="snooze-badge" title="Snoozed until ${escAttr(t.snoozedUntilFormatted || '')}" aria-label="Snoozed until ${escAttr(t.snoozedUntilFormatted || '')}">Snoozed</span>` : ''} | |
| 101 | 102 | </div> | |
| 102 | 103 | <div class="task-cell task-project">${esc(t.projectName) || '-'}</div> |
| @@ -55,6 +55,19 @@ function escapeAttrValue(str) { | |||
| 55 | 55 | } | |
| 56 | 56 | ||
| 57 | 57 | /** | |
| 58 | + | * Return a URL only if it uses a safe scheme (http/https/mailto), else "#". | |
| 59 | + | * Blocks `javascript:`, `data:`, and custom-handler schemes in user/sync-supplied | |
| 60 | + | * links (e.g. contact social URLs) before they reach an href (ultra-fuzz Run #28). | |
| 61 | + | * Still pass the result through escapeAttrValue for the attribute itself. | |
| 62 | + | * @param {string} url | |
| 63 | + | * @returns {string} | |
| 64 | + | */ | |
| 65 | + | function safeUrl(url) { | |
| 66 | + | const s = String(url ?? '').trim(); | |
| 67 | + | return /^(https?:|mailto:)/i.test(s) ? s : '#'; | |
| 68 | + | } | |
| 69 | + | ||
| 70 | + | /** | |
| 58 | 71 | * Human-friendly prefixes for machine-readable API error codes. | |
| 59 | 72 | * Backend sends structured ApiError { code, message, details }. | |
| 60 | 73 | */ | |
| @@ -649,6 +662,7 @@ GoingsOn.utils = { | |||
| 649 | 662 | escapeHtml, | |
| 650 | 663 | escapeAttr, | |
| 651 | 664 | escapeAttrValue, | |
| 665 | + | safeUrl, | |
| 652 | 666 | getErrorMessage, | |
| 653 | 667 | showError, | |
| 654 | 668 |