diff --git a/src/content/content.js b/src/content/content.js index 35b4915..0641d10 100644 --- a/src/content/content.js +++ b/src/content/content.js @@ -10,6 +10,13 @@ (function () { 'use strict'; + // --- Safe innerHTML replacement (AMO-compliant, page world) --- + function safeHTML(el, html) { + const template = document.createElement('template'); + template.innerHTML = html; + el.replaceChildren(...template.content.childNodes); + } + // ============================================================ // Load config from the injector script's data attribute // ============================================================ @@ -514,7 +521,7 @@ const more = warnings.length > 5 ? `
+${warnings.length - 5} more
` : ''; - warningEl.innerHTML = ` + safeHTML(warningEl, `
Silent Send detected potential PPI that may not be substituted: @@ -522,7 +529,7 @@ ${items} ${more} - `; + `); warningEl.classList.add('visible'); @@ -1165,7 +1172,7 @@
` ).join(''); - docPreviewEl.innerHTML = ` + safeHTML(docPreviewEl, `
PPI found in ${esc(filename)} ${preview.replacementCount} item(s) @@ -1176,7 +1183,7 @@
- `; + `); docPreviewEl.classList.add('visible'); const confirm = docPreviewEl.querySelector('.ss-dp-confirm'); const cancel = docPreviewEl.querySelector('.ss-dp-cancel'); @@ -1657,7 +1664,7 @@ const more = warnings.length > 8 ? `
+${warnings.length - 8} more
` : ''; - preSendWarningEl.innerHTML = ` + safeHTML(preSendWarningEl, `
Potential PPI detected — not yet configured: @@ -1668,7 +1675,7 @@ ${settings.autoRedactDetected !== false ? 'Auto-redacted with standard placeholders.' : 'These were sent as-is.'} ${settings.autoAddDetected !== false ? ' Click + to add a permanent mapping.' : ''}
- `; + `); preSendWarningEl.classList.add('visible'); diff --git a/src/options/options.js b/src/options/options.js index 3e5d010..5f6eaa6 100644 --- a/src/options/options.js +++ b/src/options/options.js @@ -14,6 +14,12 @@ let passwordsRevealed = false; const $ = (sel) => document.querySelector(sel); +// --- Safe innerHTML replacement (AMO-compliant) --- +function safeHTML(el, html) { + const doc = new DOMParser().parseFromString(html, 'text/html'); + el.replaceChildren(...doc.body.childNodes); +} + document.addEventListener('DOMContentLoaded', async () => { mappings = await Storage.getMappings(); settings = await Storage.getSettings(); @@ -405,11 +411,11 @@ function renderMappings() { const nonPasswordMappings = mappings.filter(m => m.category !== 'password'); if (nonPasswordMappings.length === 0) { - tbody.innerHTML = 'No mappings configured'; + safeHTML(tbody, 'No mappings configured'); return; } - tbody.innerHTML = nonPasswordMappings + safeHTML(tbody, nonPasswordMappings .map( (m) => ` @@ -427,7 +433,7 @@ function renderMappings() { ` ) - .join(''); + .join('')); // Bind tbody.querySelectorAll('.btn-delete').forEach((btn) => { @@ -457,14 +463,14 @@ function renderPasswords() { const noMsg = $('#noPasswordsMsg'); if (passwordMappings.length === 0) { - tbody.innerHTML = ''; + tbody.replaceChildren(); noMsg.style.display = 'block'; return; } noMsg.style.display = 'none'; - tbody.innerHTML = passwordMappings.map(m => { + safeHTML(tbody, passwordMappings.map(m => { const displayReal = passwordsRevealed ? escapeHtml(m.real) : '••••••••'; @@ -481,7 +487,7 @@ function renderPasswords() { `; - }).join(''); + }).join('')); // Bind delete tbody.querySelectorAll('.btn-delete-pw').forEach(btn => { @@ -518,11 +524,11 @@ async function renderLog() { const list = $('#logList'); if (log.length === 0) { - list.innerHTML = '
No activity logged
'; + safeHTML(list, '
No activity logged
'); return; } - list.innerHTML = log + safeHTML(list, log .slice(0, 100) .map((entry) => { const time = new Date(entry.timestamp).toLocaleString(); @@ -535,7 +541,7 @@ async function renderLog() { `; }) - .join(''); + .join('')); } // --- Custom Domains --- @@ -582,18 +588,18 @@ function renderDomains() { const domains = settings.customDomains || []; if (domains.length === 0) { - list.innerHTML = '
No custom domains. Built-in sites (Claude, ChatGPT, Grok, Gemini, localhost) are always active.
'; + safeHTML(list, '
No custom domains. Built-in sites (Claude, ChatGPT, Grok, Gemini, localhost) are always active.
'); return; } - list.innerHTML = domains + safeHTML(list, domains .map((d, i) => `
${escapeHtml(d)}
`) - .join(''); + .join('')); list.querySelectorAll('.btn-remove-domain').forEach((btn) => { btn.addEventListener('click', async () => { @@ -1239,11 +1245,11 @@ async function renderVersionHistory() { const snapshots = await VersionHistory.getSnapshots(); if (snapshots.length === 0) { - list.innerHTML = '
No snapshots yet. Snapshots are created on each sync.
'; + safeHTML(list, '
No snapshots yet. Snapshots are created on each sync.
'); return; } - list.innerHTML = snapshots.map(s => { + safeHTML(list, snapshots.map(s => { const time = new Date(s.timestamp).toLocaleString(); const mappingCount = (s.data?.mappings || []).length; return `
@@ -1254,7 +1260,7 @@ async function renderVersionHistory() {
`; - }).join(''); + }).join('')); list.querySelectorAll('.btn-restore-snapshot').forEach(btn => { btn.addEventListener('click', async () => { @@ -1299,13 +1305,13 @@ async function renderDevices() { const entries = Object.values(devices); if (entries.length === 0) { - list.innerHTML = '
No devices synced yet. Push or pull to register this device.
'; + safeHTML(list, '
No devices synced yet. Push or pull to register this device.
'); return; } entries.sort((a, b) => (b.lastSync || 0) - (a.lastSync || 0)); - list.innerHTML = ` + safeHTML(list, `
@@ -1322,7 +1328,7 @@ async function renderDevices() { `; }).join('')} -
Device Browser${!isCurrent ? `` : ''}
`; + `); list.querySelectorAll('.btn-remove-device').forEach(btn => { btn.addEventListener('click', async () => { @@ -1399,9 +1405,9 @@ async function showOrgJoined() { const compliance = await OrgPolicy.checkCompliance(); const statusEl = $('#orgComplianceStatus'); if (compliance.compliant) { - statusEl.innerHTML = '✓ Compliant — all required fields configured'; + safeHTML(statusEl, '✓ Compliant — all required fields configured'); } else { - statusEl.innerHTML = `Missing: ${compliance.missing.join(', ')}`; + safeHTML(statusEl, `Missing: ${compliance.missing.join(', ')}`); } const reqMappings = policy?.requiredMappings || []; @@ -1553,7 +1559,7 @@ async function checkConflicts() { function renderConflicts(conflicts) { const list = $('#conflictList'); - list.innerHTML = conflicts.map(c => ` + safeHTML(list, conflicts.map(c => `
${escapeHtml(c.path)}
@@ -1571,7 +1577,7 @@ function renderConflicts(conflicts) {
- `).join(''); + `).join('')); list.querySelectorAll('.btn-resolve').forEach(btn => { btn.addEventListener('click', async () => { @@ -1632,10 +1638,10 @@ async function handleBulkImport(e) { result.identity.usernames.filter(u => !u.substitute).length + result.identity.phones.filter(p => !p.substitute).length; - $('#bulkImportSummary').innerHTML = ` + safeHTML($('#bulkImportSummary'), ` Found: ${parts.join(', ')}. ${needsMapping > 0 ? `${needsMapping} item(s) need substitutes — you can add them after import.` : ''} - `; + `); // Build preview list const items = []; @@ -1655,8 +1661,8 @@ async function handleBulkImport(e) { items.push(`
${escapeHtml(m.category)}: ${escapeHtml(m.real)}${m.substitute ? ' → ' + escapeHtml(m.substitute) : ' needs substitute'}
`); } - $('#bulkImportItems').innerHTML = items.slice(0, 50).join('') + - (items.length > 50 ? `
+${items.length - 50} more...
` : ''); + safeHTML($('#bulkImportItems'), items.slice(0, 50).join('') + + (items.length > 50 ? `
+${items.length - 50} more...
` : '')); $('#bulkImportPreview').style.display = 'block'; diff --git a/src/popup/popup.js b/src/popup/popup.js index 4fdcf25..76de432 100644 --- a/src/popup/popup.js +++ b/src/popup/popup.js @@ -18,6 +18,12 @@ let settings = {}; const $ = (sel) => document.querySelector(sel); const $$ = (sel) => document.querySelectorAll(sel); +// --- Safe innerHTML replacement (AMO-compliant) --- +function safeHTML(el, html) { + const doc = new DOMParser().parseFromString(html, 'text/html'); + el.replaceChildren(...doc.body.childNodes); +} + // --- Init --- document.addEventListener('DOMContentLoaded', async () => { // Check if locked BEFORE trying to read sensitive data @@ -302,11 +308,11 @@ async function showLockedUI() { // --- Profiles --- function renderProfileSelector() { const select = $('#profileSelect'); - select.innerHTML = profiles.map(p => + safeHTML(select, profiles.map(p => `` - ).join(''); + ).join('')); const profile = profiles.find(p => p.id === currentProfileId); $('#profileActive').checked = profile?.active ?? true; @@ -355,7 +361,7 @@ function renderFieldList(fieldName, items) { items = [{ real: '', substitute: '', type: config.defaultType || '' }]; } - container.innerHTML = items.map((item, i) => { + safeHTML(container, items.map((item, i) => { let typeHtml = ''; if (config.typeOptions) { typeHtml = ` `; - }).join(''); + }).join('')); // Bind remove buttons container.querySelectorAll('.btn-remove').forEach(btn => { @@ -420,13 +426,13 @@ function loadIdentityForm() { ).join('') + ``; } - tempDiv.innerHTML = `
+ safeHTML(tempDiv, `
${typeHtml} -
`; +
`); const row = tempDiv.firstElementChild; container.appendChild(row); row.querySelector('.btn-remove').addEventListener('click', () => { @@ -576,11 +582,11 @@ function renderMappings() { const list = $('#mappingList'); if (mappings.length === 0) { - list.innerHTML = '
No mappings yet. Add your first one above.
'; + safeHTML(list, '
No mappings yet. Add your first one above.
'); return; } - list.innerHTML = mappings + safeHTML(list, mappings .map( (m) => `
@@ -597,7 +603,7 @@ function renderMappings() {
` ) - .join(''); + .join('')); // Bind actions list.querySelectorAll('.btn-delete').forEach((btn) => { @@ -631,11 +637,11 @@ async function renderActivity() { countEl.textContent = `${log.length} substitution${log.length !== 1 ? 's' : ''} logged`; if (log.length === 0) { - list.innerHTML = '
No activity yet.
'; + safeHTML(list, '
No activity yet.
'); return; } - list.innerHTML = log + safeHTML(list, log .slice(0, 50) .map((entry) => { const time = new Date(entry.timestamp).toLocaleTimeString([], { @@ -653,7 +659,7 @@ async function renderActivity() { `; }) - .join(''); + .join('')); } // --- Test Diff (Strip: real → fake) --- @@ -663,7 +669,7 @@ function renderTestDiff() { const stats = $('#diffStats'); if (!input) { - output.innerHTML = ''; + output.replaceChildren(); stats.textContent = ''; return; } @@ -703,7 +709,7 @@ function renderTestDiff() { `${escapedReplaced}` ); } - output.innerHTML = html; + safeHTML(output, html); const smartCount = smartResult.replacements.length; const explicitCount = explicitResult.replacements.length; @@ -723,10 +729,12 @@ function renderTestDiff() { // Show PPI warnings below stats if (ppiWarnings.length > 0) { - stats.innerHTML += `
+ const ppiDiv = document.createElement('div'); + safeHTML(ppiDiv, `
Unconfigured PPI detected: ${ppiWarnings.map(w => `
${escapeHtml(w.value)} — ${w.hint}
`).join('')} -
`; +
`); + stats.appendChild(ppiDiv); } } @@ -737,7 +745,7 @@ function renderRevealDiff() { const stats = $('#revealStats'); if (!input) { - output.innerHTML = ''; + output.replaceChildren(); stats.textContent = ''; return; } @@ -783,7 +791,7 @@ function renderRevealDiff() { `${escapedReal}` ); } - output.innerHTML = html; + safeHTML(output, html); stats.textContent = `${totalCount} value${totalCount !== 1 ? 's' : ''} revealed`; }