Resolve remaining Codacy findings in settings search

- Validate plugin ids against a strict allowlist (mirroring the server's
  _SAFE_PLUGIN_ID_RE) before they can appear in a fetch path, so the request
  URL is never built from unvalidated input (Codacy: user-controlled URL).
- Document that the fetched HTML is parsed into an inert document (scripts
  never run, never inserted into the live DOM) purely to read field text for
  the search index.
- Declare block-scoped locals with const instead of var where they were
  nested inside conditionals (Codacy: var not at function root).

No behavior change; re-verified with ESLint and the headless-Chromium test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014gZxznuxw8L92FUMBN3Nqz
This commit is contained in:
Claude
2026-07-08 15:40:22 +00:00
parent 6e3f334e10
commit c0d785967b
+20 -9
View File
@@ -35,6 +35,11 @@
var MAX_RESULTS = 25; var MAX_RESULTS = 25;
// Plugin ids are constrained to this allowlist (mirrors the server's
// _SAFE_PLUGIN_ID_RE in pages_v3.py) before they can appear in a request
// path, so a fetched URL can never be influenced by untrusted input.
var PLUGIN_ID_RE = /^[a-zA-Z0-9_-]{1,64}$/;
function debounce(fn, ms) { function debounce(fn, ms) {
var t; var t;
return function () { return function () {
@@ -88,10 +93,15 @@
} }
function fetchAndScan(url, tab, tabLabel) { function fetchAndScan(url, tab, tabLabel) {
// `url` is always one of our own same-origin settings partial paths
// (CORE_TABS literals or a plugin path built from an allowlisted id).
return fetch(url, { headers: { 'X-Requested-With': 'settings-search' } }) return fetch(url, { headers: { 'X-Requested-With': 'settings-search' } })
.then(function (r) { return r.ok ? r.text() : ''; }) .then(function (r) { return r.ok ? r.text() : ''; })
.then(function (html) { .then(function (html) {
if (!html) return []; if (!html) return [];
// Parsed into an inert document: scripts do not run and it is
// never inserted into the live DOM — we only read labels and
// tooltip text from it to build the search index.
var doc = new DOMParser().parseFromString(html, 'text/html'); var doc = new DOMParser().parseFromString(html, 'text/html');
return scanDoc(doc, tab, tabLabel); return scanDoc(doc, tab, tabLabel);
}) })
@@ -107,9 +117,10 @@
var plugins = (window.installedPlugins || []); var plugins = (window.installedPlugins || []);
plugins.forEach(function (p) { plugins.forEach(function (p) {
if (!p || !p.id) return; // Skip ids that don't match the strict allowlist so the request
jobs.push(fetchAndScan('/v3/partials/plugin-config/' + encodeURIComponent(p.id), // path is always built from safe, validated components.
p.id, p.name || p.id)); if (!p || !p.id || !PLUGIN_ID_RE.test(p.id)) return;
jobs.push(fetchAndScan('/v3/partials/plugin-config/' + p.id, p.id, p.name || p.id));
}); });
buildPromise = Promise.all(jobs).then(function (lists) { buildPromise = Promise.all(jobs).then(function (lists) {
@@ -158,7 +169,7 @@
var lastTab = null; var lastTab = null;
results.forEach(function (r, i) { results.forEach(function (r, i) {
if (r.tabLabel !== lastTab) { if (r.tabLabel !== lastTab) {
var group = document.createElement('div'); const group = document.createElement('div');
group.className = 'ssr-group'; group.className = 'ssr-group';
group.textContent = r.tabLabel; group.textContent = r.tabLabel;
resultsBox.appendChild(group); resultsBox.appendChild(group);
@@ -329,7 +340,7 @@
e.preventDefault(); e.preventDefault();
highlight(Math.max(activeIndex - 1, 0)); highlight(Math.max(activeIndex - 1, 0));
} else if (e.key === 'Enter') { } else if (e.key === 'Enter') {
var chosen = currentResults.at(activeIndex >= 0 ? activeIndex : 0); const chosen = currentResults.at(activeIndex >= 0 ? activeIndex : 0);
if (chosen) { if (chosen) {
e.preventDefault(); e.preventDefault();
navigateToSetting(chosen); navigateToSetting(chosen);
@@ -345,8 +356,8 @@
var opt = e.target.closest('.ssr-option'); var opt = e.target.closest('.ssr-option');
if (!opt) return; if (!opt) return;
e.preventDefault(); e.preventDefault();
var idx = parseInt(opt.getAttribute('data-idx'), 10); const idx = parseInt(opt.getAttribute('data-idx'), 10);
var chosen = currentResults.at(idx); const chosen = currentResults.at(idx);
if (chosen) navigateToSetting(chosen); if (chosen) navigateToSetting(chosen);
}); });
@@ -405,9 +416,9 @@
}); });
// Toggle the "no matches" note if the filter box provides one. // Toggle the "no matches" note if the filter box provides one.
var wrap = scope.querySelector('.settings-filter-wrap'); const wrap = scope.querySelector('.settings-filter-wrap');
if (wrap) { if (wrap) {
var empty = wrap.querySelector('.settings-filter-empty'); const empty = wrap.querySelector('.settings-filter-empty');
if (empty) empty.classList.toggle('hidden', !(terms.length && !anyVisible)); if (empty) empty.classList.toggle('hidden', !(terms.length && !anyVisible));
} }
} }