personal memory agent
0

Configure Feed

Select the types of activity you want to include in your feed.

fix(convey): escape the notification badge

_createCard interpolated n.badge raw into innerHTML while every neighbouring field, including n.app, n.title, and n.message, was escaped, so a caller could inject markup through the badge. The value is caller-controlled end to end: sol notify --badge is free-form and websocket.js forwards raw callosum payloads into show().

_updateCard already assigned the badge with textContent, so initial render and update render disagreed. This aligns them on the safe behaviour.

Found while auditing the icon-contract change. It is the same class of defect that change closes for icon, in the same renderer, but a different field, hence a separate commit.

The test helper assertNoPayload was tightened to match event-handler markup only inside a real tag, because correctly escaped output legitimately contains the literal text onerror.

+15 -3
+1 -1
solstone/convey/static/app.js
··· 2442 2442 <div class="notification-body"> 2443 2443 <div class="notification-title">${window.AppServices.escapeHtml(n.title)}</div> 2444 2444 ${n.message ? `<div class="notification-message">${window.AppServices.escapeHtml(n.message)}</div>` : ''} 2445 - ${n.badge ? `<span class="notification-badge">${n.badge}</span>` : ''} 2445 + ${n.badge ? `<span class="notification-badge">${window.AppServices.escapeHtml(n.badge)}</span>` : ''} 2446 2446 </div> 2447 2447 <div class="notification-footer"> 2448 2448 <span class="notification-time">${relativeTime}</span>
+14 -2
tests/test_notifications_js.py
··· 98 98 "const notifications = vm.runInContext('(' + notificationsSource + ')', context, { filename: 'notifications-object.js' });\n" 99 99 "window.AppServices.notifications = notifications;\n" 100 100 "function assertNoPayload(html) {\n" 101 - " assert(!String(html).includes('<img'), 'raw image tag should not render');\n" 102 - " assert(!String(html).includes('onerror'), 'raw event handler should not render');\n" 101 + " const text = String(html);\n" 102 + " assert(!text.includes('<img'), 'raw image tag should not render');\n" 103 + " assert(!/<[^>]*\\sonerror\\s*=/.test(text), 'raw event handler should not render');\n" 103 104 "}\n" 104 105 "function makeElement(tagName = 'div') {\n" 105 106 " const el = {\n" ··· 196 197 "assert(browserCalls[0].options.body === 'Browser body', 'browser body should be delivered');\n" 197 198 "assert(browserCalls[0].options.tag === 'system-2', 'browser tag should be delivered');\n" 198 199 "assert(!Object.prototype.hasOwnProperty.call(browserCalls[0].options, 'icon'), 'browser notification should not receive icon');\n" 200 + ) 201 + 202 + 203 + def test_notification_badge_escapes_initial_card_markup(): 204 + _run_notifications_script( 205 + "context.document = { createElement: makeElement };\n" 206 + "const payload = '<img src=x onerror=alert(1)>';\n" 207 + "const card = notifications._createCard({id: 7, app: 'system', icon: 'mailbox', title: 'Badge', message: '', action: null, facet: null, dismissible: true, badge: payload, timestamp: Date.now(), lastSeen: Date.now(), autoDismiss: null, buttons: []});\n" 208 + "assertNoPayload(card.innerHTML);\n" 209 + "assert(!card.innerHTML.includes(payload), 'raw badge payload should not render');\n" 210 + "assert(card.innerHTML.includes('&lt;img src=x onerror=alert(1)&gt;'), 'badge should render escaped payload');\n" 199 211 ) 200 212 201 213