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, `
`
).join('');
- docPreviewEl.innerHTML = `
+ safeHTML(docPreviewEl, `
- `;
+ `);
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, `
- `;
+ `);
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() {
Restore
`;
- }).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, `
Device
Browser
@@ -1322,7 +1328,7 @@ async function renderDevices() {
${!isCurrent ? `× ` : ''}
`;
}).join('')}
-
`;
+
`);
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) {
Keep Remote
- `).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 =>
`` +
`${escapeHtml(p.name)}${p.active ? '' : ' (off)'}` +
` `
- ).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 = `` +
@@ -371,7 +377,7 @@ function renderFieldList(fieldName, items) {
×
`;
- }).join('');
+ }).join(''));
// Bind remove buttons
container.querySelectorAll('.btn-remove').forEach(btn => {
@@ -420,13 +426,13 @@ function loadIdentityForm() {
).join('') +
` `;
}
- tempDiv.innerHTML = ``);
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`;
}