Skip to content

Commit 80188da

Browse files
committed
fix(security): replace createContextualFragment and innerHTML with DOMParser
Eliminates AMO linter warnings for unsafe DOM manipulation by using DOMParser with text/html mode. Extract shared parseSVG helper in icons.js and remove the innerHTML path from showSyncStatus.
1 parent ebe1c5a commit 80188da

2 files changed

Lines changed: 30 additions & 26 deletions

File tree

src/lib/icons.js

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -76,8 +76,17 @@ export function getIconSVG(type, state, merged, conclusion, stateReason) {
7676
}
7777

7878
/**
79-
* Parse a static SVG string from ICON_SVGS into a live SVGElement.
80-
* Uses createContextualFragment to avoid innerHTML, so the AMO linter does not flag it.
79+
* Parse an SVG markup string into a live SVGElement.
80+
* @param {string} svgString - SVG markup
81+
* @returns {SVGElement}
82+
*/
83+
export function parseSVG(svgString) {
84+
const doc = new DOMParser().parseFromString(svgString, "text/html");
85+
return doc.body.firstElementChild;
86+
}
87+
88+
/**
89+
* Get a notification icon as a live SVGElement.
8190
* @param {string} type - Icon key (e.g. 'pull_request', 'issue', 'comment_bubble')
8291
* @param {string} [state]
8392
* @param {boolean} [merged]
@@ -86,10 +95,5 @@ export function getIconSVG(type, state, merged, conclusion, stateReason) {
8695
* @returns {SVGElement}
8796
*/
8897
export function getIconSVGElement(type, state, merged, conclusion, stateReason) {
89-
const svgString = getIconSVG(type, state, merged, conclusion, stateReason);
90-
// createContextualFragment parses in the current document's HTML context,
91-
// correctly handling SVG namespaces without an innerHTML assignment.
92-
const range = document.createRange();
93-
const fragment = range.createContextualFragment(svgString);
94-
return fragment.firstElementChild;
98+
return parseSVG(getIconSVG(type, state, merged, conclusion, stateReason));
9599
}

src/popup/popup.js

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
buildKeywordNotificationsUrl,
2222
} from "../lib/url-builder.js";
2323
import { classifyError } from "../lib/format-utils.js";
24+
import { parseSVG } from "../lib/icons.js";
2425
import {
2526
initRenderer,
2627
renderNotifications,
@@ -720,11 +721,9 @@ async function markAllAsRead() {
720721
// Immediate visual feedback
721722
const originalNodes = Array.from(markAllBtn.childNodes).map((n) => n.cloneNode(true));
722723
markAllBtn.disabled = true;
723-
const spinner = document
724-
.createRange()
725-
.createContextualFragment(
726-
'<svg viewBox="0 0 16 16" width="16" height="16" class="spinner-icon"><circle cx="8" cy="8" r="6" stroke="currentColor" stroke-width="2" fill="none" stroke-dasharray="30" stroke-dashoffset="0"><animateTransform attributeName="transform" type="rotate" from="0 8 8" to="360 8 8" dur="1s" repeatCount="indefinite"/></circle></svg>',
727-
);
724+
const spinner = parseSVG(
725+
'<svg viewBox="0 0 16 16" width="16" height="16" class="spinner-icon"><circle cx="8" cy="8" r="6" stroke="currentColor" stroke-width="2" fill="none" stroke-dasharray="30" stroke-dashoffset="0"><animateTransform attributeName="transform" type="rotate" from="0 8 8" to="360 8 8" dur="1s" repeatCount="indefinite"/></circle></svg>',
726+
);
728727
markAllBtn.replaceChildren(spinner);
729728

730729
// Immediate visual feedback: start overlay animation with stagger
@@ -1067,7 +1066,7 @@ function createFilterRuleActionButton({
10671066
button.title = title;
10681067
button.setAttribute("aria-label", ariaLabel);
10691068
button.disabled = disabled;
1070-
button.appendChild(document.createRange().createContextualFragment(svgMarkup));
1069+
button.appendChild(parseSVG(svgMarkup));
10711070
button.addEventListener("click", onClick);
10721071
return button;
10731072
}
@@ -1172,10 +1171,9 @@ function renderRuleRows(rules, stats = []) {
11721171
if (rules.length === 0) {
11731172
const empty = document.createElement("div");
11741173
empty.className = "empty-state";
1175-
// Reuse the filter funnel icon from the header (createContextualFragment avoids innerHTML)
11761174
const svgMarkup =
11771175
'<svg viewBox="0 0 16 16" width="24" height="24"><path fill="currentColor" d="M.75 3h14.5a.75.75 0 0 1 0 1.5H.75a.75.75 0 0 1 0-1.5ZM3 7.75A.75.75 0 0 1 3.75 7h8.5a.75.75 0 0 1 0 1.5h-8.5A.75.75 0 0 1 3 7.75Zm3 4a.75.75 0 0 1 .75-.75h2.5a.75.75 0 0 1 0 1.5h-2.5a.75.75 0 0 1-.75-.75Z"/></svg>';
1178-
empty.appendChild(document.createRange().createContextualFragment(svgMarkup));
1176+
empty.appendChild(parseSVG(svgMarkup));
11791177
const text = document.createElement("p");
11801178
text.textContent = "No rules yet";
11811179
empty.appendChild(text);
@@ -1472,13 +1470,9 @@ function updateFilterCreatorLabel() {
14721470
filterCreatorLabel.textContent = editingRuleIndex >= 0 ? "Edit Rule" : "New Rule";
14731471
}
14741472

1475-
function showSyncStatus(text, isError = false, useHtml = false) {
1473+
function showSyncStatus(text, isError = false) {
14761474
if (!syncStatus) return;
1477-
if (useHtml) {
1478-
syncStatus.innerHTML = text;
1479-
} else {
1480-
syncStatus.textContent = text;
1481-
}
1475+
syncStatus.textContent = text;
14821476
syncStatus.classList.toggle("error", isError);
14831477
syncStatus.hidden = false;
14841478
}
@@ -1571,10 +1565,16 @@ async function handleSyncToggle() {
15711565
if (authMethod === "oauth") {
15721566
showSyncStatus("Re-login via OAuth to grant the gist scope.", true);
15731567
} else {
1574-
showSyncStatus(
1575-
'Add the <a href="https://github.com/settings/tokens" target="_blank" rel="noopener noreferrer">gist scope</a> to your token on GitHub.',
1576-
true,
1577-
true,
1568+
const link = document.createElement("a");
1569+
link.href = "https://github.com/settings/tokens";
1570+
link.target = "_blank";
1571+
link.rel = "noopener noreferrer";
1572+
link.textContent = "gist scope";
1573+
showSyncStatus("", true);
1574+
syncStatus.replaceChildren(
1575+
document.createTextNode("Add the "),
1576+
link,
1577+
document.createTextNode(" to your token on GitHub."),
15781578
);
15791579
}
15801580
} else {

0 commit comments

Comments
 (0)