diff --git a/plugin.json b/plugin.json index efb9d07..9730456 100644 --- a/plugin.json +++ b/plugin.json @@ -1,7 +1,7 @@ { "id": "section_map", "name": "Section Map", - "version": "1.1.0", + "version": "1.1.1", "private": false, "script": "screen.js", "category": "practice", diff --git a/screen.js b/screen.js index db330b8..e6b028f 100644 --- a/screen.js +++ b/screen.js @@ -165,9 +165,11 @@ function _smRender() { let label = sec.name.replace(/\d+$/, '').trim(); label = label.charAt(0).toUpperCase() + label.slice(1); + const safeLabel = _smEscapeHtml(label); + const safeTitle = _smEscapeHtml(`${label} (${_smFmt(sec.time)})`); html += `
`; } @@ -182,12 +184,21 @@ function _smFmt(s) { return Math.floor(s / 60) + ':' + String(Math.floor(s % 60)).padStart(2, '0'); } +function _smEscapeHtml(value) { + return String(value) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); +} + // Node-only export hook for tests; browsers fall through to the side-effect // IIFE below (poller + playSong/showScreen wrapping). if (typeof module !== 'undefined' && module.exports) { module.exports = { _smGetColor, _smFmt, _smCreate, _smRemove, _smUpdate, _smRender, - _smOnClick, _smOnWheel, + _smOnClick, _smOnWheel, _smEscapeHtml, _getState: () => ({ bar: _smBar, sections: _smSections, duration: _smDuration }), _setState(next) { if ('sections' in next) _smSections = next.sections; diff --git a/tests/screen.test.js b/tests/screen.test.js index f9dd39f..3fb50e6 100644 --- a/tests/screen.test.js +++ b/tests/screen.test.js @@ -58,6 +58,14 @@ test('_smFmt formats seconds as m:ss with zero-padded seconds', () => { assert.equal(mod._smFmt(600), '10:00'); }); +test('_smEscapeHtml neutralizes section-name markup in text and attributes', () => { + const mod = freshPlugin(); + assert.equal( + mod._smEscapeHtml('