harden(#2622): DOM-bound plugin handlers + tab.path validation (Codex round-2)

Codex found two more once the config-guard bug was fixed (Opus concurred on #2):
1. panels.js _buildPluginCard built the Open button + enable toggle with inline
   onclick/onchange that interpolated tab.path / plugin.key into a JS-string-in-
   attribute context — HTML-escaping is insufficient there (quote breakout).
   Now rendered inert + bound via addEventListener with RAW closure values.
2. tab.path was unvalidated. Added _VALID_PLUGIN_TAB_PATH (^/[A-Za-z0-9._~/-]{0,255}$)
   in load_plugins() — absolute, no quotes/query/fragment/control chars.
Also Opus nit: deep-merge now coerces dashboard_plugins values to bool + str keys.
Regression tests added for both.
This commit is contained in:
nesquena-hermes
2026-06-02 03:39:41 +00:00
parent 944e739b40
commit 816a4a93f9
4 changed files with 62 additions and 4 deletions

View File

@@ -24,6 +24,11 @@ logger = logging.getLogger(__name__)
# a settings key). Lowercase alnum + - / _, 1-64 chars, must start with a letter.
_VALID_PLUGIN_NAME = re.compile(r"^[a-z][a-z0-9_-]{0,63}$")
# Valid tab.path: a clean same-origin absolute path. Must start with '/', then
# only safe path chars — no quotes, whitespace, control chars, query ('?') or
# fragment ('#') so it can't break out of a JS-string nav arg or shadow routes.
_VALID_PLUGIN_TAB_PATH = re.compile(r"^/[A-Za-z0-9._~/-]{0,255}$")
# plugin_name -> manifest dict (as loaded from manifest.json)
PLUGIN_MANIFESTS: dict[str, dict] = {}
@@ -67,6 +72,14 @@ def load_plugins() -> None:
tab = manifest.get("tab", {})
tab_path = tab.get("path", f"/{name}")
# Validate tab.path: it's a same-origin route the plugin page is served
# at AND a value passed into client-side navigation. Require a clean
# absolute path — no quotes/control chars/query/fragment — so a hostile
# manifest can't shadow odd routes or inject via the path.
if not _VALID_PLUGIN_TAB_PATH.match(str(tab_path)):
logger.warning("Skipping plugin %s with invalid tab.path %r (must match %s)", name, tab_path, _VALID_PLUGIN_TAB_PATH.pattern)
continue
if name in PLUGIN_MANIFESTS:
logger.warning("Duplicate plugin name skipped: %s (already loaded)", name)
continue