fix(plugins): harden path traversal and respect enabled state

Use realpathSync to canonicalize paths before the plugin asset
boundary check, preventing symlink-based traversal bypasses that
could escape the plugin directory.

PluginTabContent now guards on plugin.enabled before mounting the
plugin module, and re-mounts when the enabled state changes so
toggling a plugin takes effect without a page reload.

PluginIcon safely handles a missing iconFile prop and skips
processing non-OK fetch responses instead of attempting to parse
error bodies as SVG.

Register 'plugins' as a known main tab so the settings router
preserves the tab on navigation.
This commit is contained in:
simosmik
2026-03-09 06:49:51 +00:00
parent a7e8b12ef4
commit efdee162c9
9 changed files with 29 additions and 18 deletions

View File

@@ -11,15 +11,20 @@ type Props = {
const svgCache = new Map<string, string>();
export default function PluginIcon({ pluginName, iconFile, className }: Props) {
const url = `/api/plugins/${encodeURIComponent(pluginName)}/assets/${encodeURIComponent(iconFile)}`;
const [svg, setSvg] = useState<string | null>(svgCache.get(url) ?? null);
const url = iconFile
? `/api/plugins/${encodeURIComponent(pluginName)}/assets/${encodeURIComponent(iconFile)}`
: '';
const [svg, setSvg] = useState<string | null>(url ? (svgCache.get(url) ?? null) : null);
useEffect(() => {
if (svgCache.has(url)) return;
if (!url || svgCache.has(url)) return;
authenticatedFetch(url)
.then((r) => r.text())
.then((r) => {
if (!r.ok) return;
return r.text();
})
.then((text) => {
if (text.trimStart().startsWith('<svg')) {
if (text && text.trimStart().startsWith('<svg')) {
svgCache.set(url, text);
setSvg(text);
}