mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-03-14 18:37:22 +00:00
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:
@@ -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)}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user