From d79b31d749ed2603d3b1777e0aadecc32837e8a0 Mon Sep 17 00:00:00 2001 From: anupamme Date: Fri, 2 Oct 2026 21:30:11 +0000 Subject: [PATCH 1/2] fix: multi_agent.cwe-79 security vulnerability Automated security fix generated by OrbisAI Security --- MMM-CalendarExtMiniMonth.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/MMM-CalendarExtMiniMonth.js b/MMM-CalendarExtMiniMonth.js index 1c19873..ed9f359 100644 --- a/MMM-CalendarExtMiniMonth.js +++ b/MMM-CalendarExtMiniMonth.js @@ -52,6 +52,12 @@ function formatPattern(date, pattern, locale) { } } +function escapeHtml(value) { + return String(value).replace(/[&<>"']/g, function (ch) { + return { '&': '&', '<': '<', '>': '>', '"': '"', '\'': ''' }[ch] + }) +} + function toTimestampMs(value) { if (value instanceof Date) { return value.getTime() @@ -179,7 +185,7 @@ Module.register('MMM-CalendarExtMiniMonth', { } return { name: calendarName, - title: event.title, + title: event.title ? escapeHtml(event.title) : event.title, startDate: startDate, endDate: endDate, fullDayEvent: !!event.fullDayEvent, From 0d78f3d0da6aa443fc8b2d1521606d4eda3547a0 Mon Sep 17 00:00:00 2001 From: OrbisAI Security Date: Sat, 10 Oct 2026 14:29:27 +0530 Subject: [PATCH 2/2] fix: revert premature title escaping, add regression test for no-sink guarantee event.title is never inserted into the DOM anywhere in this module (getDom/drawSlot only write innerHTML from Date/config-derived strings via formatPattern, never from event data), so escaping it at ingestion time fixed nothing live and corrupted the stored title (e.g. "&" became "&"). Revert the escapeHtml() call, keep the title field (still required by the pre-existing name/title/date filter), and add a test that proves no DOM sink exists today and will fail if one is ever added unsafely. Co-Authored-By: Claude Sonnet 5 --- MMM-CalendarExtMiniMonth.js | 10 +- package.json | 2 +- test/event-title-no-dom-sink.test.js | 141 +++++++++++++++++++++++++++ 3 files changed, 145 insertions(+), 8 deletions(-) create mode 100644 test/event-title-no-dom-sink.test.js diff --git a/MMM-CalendarExtMiniMonth.js b/MMM-CalendarExtMiniMonth.js index ed9f359..923a7db 100644 --- a/MMM-CalendarExtMiniMonth.js +++ b/MMM-CalendarExtMiniMonth.js @@ -52,12 +52,6 @@ function formatPattern(date, pattern, locale) { } } -function escapeHtml(value) { - return String(value).replace(/[&<>"']/g, function (ch) { - return { '&': '&', '<': '<', '>': '>', '"': '"', '\'': ''' }[ch] - }) -} - function toTimestampMs(value) { if (value instanceof Date) { return value.getTime() @@ -185,7 +179,9 @@ Module.register('MMM-CalendarExtMiniMonth', { } return { name: calendarName, - title: event.title ? escapeHtml(event.title) : event.title, + // Stored raw: never rendered via innerHTML today (see test/event-title-no-dom-sink.test.js). + // If a feature ever displays this, escape at the render sink, not here. + title: event.title, startDate: startDate, endDate: endDate, fullDayEvent: !!event.fullDayEvent, diff --git a/package.json b/package.json index f22003f..3804f75 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "lint": "eslint && prettier . --check", "lint:fix": "eslint --fix && prettier . --write", "release": "commit-and-tag-version --config ./changelog.config.js", - "test": "node --run lint" + "test": "node --run lint && node --test" }, "repository": { "type": "git", diff --git a/test/event-title-no-dom-sink.test.js b/test/event-title-no-dom-sink.test.js new file mode 100644 index 0000000..b792991 --- /dev/null +++ b/test/event-title-no-dom-sink.test.js @@ -0,0 +1,141 @@ +'use strict' + +const test = require('node:test') +const assert = require('node:assert/strict') +const path = require('node:path') + +function createFakeElement() { + return { + _id: '', + className: '', + style: {}, + innerHTML: '', + children: [], + classList: { + add() {}, + contains() { + return false + }, + }, + appendChild(child) { + this.children.push(child) + return child + }, + get id() { + return this._id + }, + set id(value) { + this._id = value + elementsById.set(value, this) + }, + } +} + +const elementsById = new Map() + +function serialize(el) { + let out = el.innerHTML || '' + for (const child of el.children) { + out += serialize(child) + } + return out +} + +function loadModule() { + let captured = null + global.Module = { + register(name, definition) { + captured = definition + }, + } + global.config = {} + global.document = { + createElement: createFakeElement, + getElementById(id) { + return elementsById.get(id) || null + }, + body: createFakeElement(), + } + + delete require.cache[path.join(__dirname, '..', 'MMM-CalendarExtMiniMonth.js')] + require('../MMM-CalendarExtMiniMonth.js') + + const instance = Object.assign(Object.create(captured), captured) + instance.config = Object.assign({}, captured.defaults, { dynamicEventColor: null }) + instance.start() + instance.updateDom = function () {} + instance.refreshScreen = function () { + this.updateDom() + } + return instance +} + +function makeEvent(title) { + return { + calendarName: 'Test Calendar', + title: title, + startDate: Date.now(), + endDate: Date.now() + 60 * 60 * 1000, + } +} + +test('normal event title is stored and rendered without error', () => { + const instance = loadModule() + instance.updateContentFromCalendarEvents([makeEvent('Team Meeting')]) + + assert.equal(instance.events.length, 1) + assert.equal(instance.events[0].title, 'Team Meeting') + + const dom = instance.getDom() + assert.ok(dom.children.length > 0, 'expected rendered DOM to contain header/slot nodes') +}) + +test('' + instance.updateContentFromCalendarEvents([makeEvent(payload)]) + + assert.equal(instance.events[0].title, payload, 'title must be stored raw, not escaped/mangled') + + const dom = instance.getDom() + const rendered = serialize(dom) + assert.ok(!rendered.includes(' { + const instance = loadModule() + const payload = '' + instance.updateContentFromCalendarEvents([makeEvent(payload)]) + + assert.equal(instance.events[0].title, payload) + + const dom = instance.getDom() + const rendered = serialize(dom) + assert.ok(!rendered.includes('onerror'), 'event title must never be inserted into the DOM') +}) + +test('ampersand in title is preserved exactly, not HTML-entity-encoded', () => { + const instance = loadModule() + instance.updateContentFromCalendarEvents([makeEvent('Research & Development')]) + + assert.equal(instance.events[0].title, 'Research & Development') +}) + +test('quotes and apostrophes in title are preserved exactly', () => { + const instance = loadModule() + instance.updateContentFromCalendarEvents([makeEvent('Tom\'s "Meeting"')]) + + assert.equal(instance.events[0].title, 'Tom\'s "Meeting"') +}) + +test('missing or empty title drops the event without throwing', () => { + const instance = loadModule() + assert.doesNotThrow(() => { + instance.updateContentFromCalendarEvents([makeEvent(''), makeEvent(undefined)]) + }) + assert.equal(instance.events.length, 0) + + assert.doesNotThrow(() => { + instance.getDom() + }) +})