From 72b443d54100e3fadc9339363d1d66697721f644 Mon Sep 17 00:00:00 2001 From: Chuck Date: Sat, 11 Jul 2026 12:30:17 -0400 Subject: [PATCH] fix(dev-server): allowlist plugin_id before any path lookup CodeQL (py/path-injection): plugin_id arrives in request input and flows into filesystem paths via find_plugin_dir. Gate it with the same ^[a-zA-Z0-9_-]{1,64}$ allowlist the web UI's pages_v3 uses, at the single choke point every route resolves through. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01FqzC1nzTWL4kaqgMaQZFam --- scripts/dev_server.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/scripts/dev_server.py b/scripts/dev_server.py index 86195533..b3a450a9 100644 --- a/scripts/dev_server.py +++ b/scripts/dev_server.py @@ -16,6 +16,7 @@ Opens at http://localhost:5001 import sys import os import json +import re import time import argparse import logging @@ -44,6 +45,10 @@ MAX_HEIGHT = 512 MIN_WIDTH = 1 MIN_HEIGHT = 1 +# plugin_id arrives in request input and is used to build filesystem paths — +# allowlist it (same pattern the web UI's pages_v3 uses) +_SAFE_PLUGIN_ID_RE = re.compile(r'^[a-zA-Z0-9_-]{1,64}$') + # -------------------------------------------------------------------------- # Plugin discovery @@ -106,7 +111,13 @@ def discover_plugins() -> List[Dict[str, Any]]: def find_plugin_dir(plugin_id: str) -> Optional[Path]: - """Find a plugin directory by ID.""" + """Find a plugin directory by ID. + + plugin_id comes from request input; the allowlist match keeps it from + ever naming a path outside the plugin search dirs. + """ + if not isinstance(plugin_id, str) or not _SAFE_PLUGIN_ID_RE.match(plugin_id): + return None from src.plugin_system.plugin_loader import PluginLoader loader = PluginLoader() for search_dir in get_search_dirs():