fix(plugins): harden input validation and scan reliability

- Validate plugin names against [a-zA-Z0-9_-] allowlist in
  manifest and asset routes to prevent path traversal via URL
- Strip embedded credentials (user:pass@) from git remote URLs
  before exposing them to the client
- Skip .tmp-* directories during scan to avoid partial installs
  from in-progress updates appearing as broken plugins
- Deduplicate plugins sharing the same manifest name to prevent
  ambiguous state
- Guard RPC proxy error handler against writing to an already-sent
  response, preventing uncaught exceptions on aborted requests
This commit is contained in:
simosmik
2026-03-09 07:59:46 +00:00
parent ca247cddae
commit ca16342a20
4 changed files with 51 additions and 6 deletions

View File

@@ -312,8 +312,13 @@ export default function PluginSettingsTab() {
setConfirmUninstall(name);
return;
}
await uninstallPlugin(name);
setConfirmUninstall(null);
const result = await uninstallPlugin(name);
if (result.success) {
setConfirmUninstall(null);
} else {
setInstallError(result.error || 'Uninstall failed');
setConfirmUninstall(null);
}
};
const hasStarterInstalled = plugins.some((p) => p.name === 'project-stats');
@@ -350,6 +355,7 @@ export default function PluginSettingsTab() {
setInstallError(null);
}}
placeholder="https://github.com/user/my-plugin"
aria-label="Plugin git repository URL"
className="flex-1 bg-transparent px-2 py-2.5 text-sm text-foreground placeholder:text-muted-foreground/40 focus:outline-none"
onKeyDown={(e) => {
if (e.key === 'Enter') void handleInstall();
@@ -399,7 +405,7 @@ export default function PluginSettingsTab() {
key={plugin.name}
plugin={plugin}
index={index}
onToggle={(enabled) => void togglePlugin(plugin.name, enabled)}
onToggle={(enabled) => void togglePlugin(plugin.name, enabled).then(r => { if (!r.success) setInstallError(r.error || 'Toggle failed'); })}
onUpdate={() => void handleUpdate(plugin.name)}
onUninstall={() => void handleUninstall(plugin.name)}
updating={updatingPlugins.has(plugin.name)}

View File

@@ -70,7 +70,7 @@ export default function PluginTabContent({
try {
// Fetch the plugin JS with auth headers (Cloudflare Worker requires auth on all routes).
// Then import it via a Blob URL so the browser never makes an unauthenticated request.
const assetUrl = `/api/plugins/${encodeURIComponent(pluginName)}/assets/${entryFile}`;
const assetUrl = `/api/plugins/${encodeURIComponent(pluginName)}/assets/${encodeURIComponent(entryFile)}`;
const res = await authenticatedFetch(assetUrl);
if (!res.ok) throw new Error(`Failed to fetch plugin (HTTP ${res.status})`);
const jsText = await res.text();
@@ -114,7 +114,10 @@ export default function PluginTabContent({
if (!active) return;
console.error(`[Plugin:${pluginName}] Failed to load:`, err);
if (containerRef.current) {
containerRef.current.innerHTML = `<div style="padding:16px;font-size:13px;color:#dc2626">Plugin failed to load: ${String(err)}</div>`;
const errDiv = document.createElement('div');
errDiv.style.cssText = 'padding:16px;font-size:13px;color:#dc2626';
errDiv.textContent = `Plugin failed to load: ${String(err)}`;
containerRef.current.replaceChildren(errDiv);
}
}
})();