diff --git a/server/utils/plugin-loader.js b/server/utils/plugin-loader.js index 2f961526..9b8cf244 100644 --- a/server/utils/plugin-loader.js +++ b/server/utils/plugin-loader.js @@ -31,9 +31,9 @@ export function getPluginsConfig() { export function savePluginsConfig(config) { const dir = path.dirname(PLUGINS_CONFIG_PATH); if (!fs.existsSync(dir)) { - fs.mkdirSync(dir, { recursive: true }); + fs.mkdirSync(dir, { recursive: true, mode: 0o700 }); } - fs.writeFileSync(PLUGINS_CONFIG_PATH, JSON.stringify(config, null, 2)); + fs.writeFileSync(PLUGINS_CONFIG_PATH, JSON.stringify(config, null, 2), { mode: 0o600 }); } export function validateManifest(manifest) { @@ -60,6 +60,23 @@ export function validateManifest(manifest) { return { valid: false, error: `Invalid plugin slot: ${manifest.slot}. Must be one of: ${ALLOWED_SLOTS.join(', ')}` }; } + // Validate entry is a relative path without traversal + if (manifest.entry.includes('..') || path.isAbsolute(manifest.entry)) { + return { valid: false, error: 'Entry must be a relative path without ".."' }; + } + + if (manifest.server !== undefined && manifest.server !== null) { + if (typeof manifest.server !== 'string' || manifest.server.includes('..') || path.isAbsolute(manifest.server)) { + return { valid: false, error: 'Server entry must be a relative path string without ".."' }; + } + } + + if (manifest.permissions !== undefined) { + if (!Array.isArray(manifest.permissions) || !manifest.permissions.every(p => typeof p === 'string')) { + return { valid: false, error: 'Permissions must be an array of strings' }; + } + } + return { valid: true }; } @@ -235,6 +252,13 @@ export function installPluginFromGit(url) { return reject(new Error(`Invalid manifest: ${validation.error}`)); } + // Reject if another installed plugin already uses this name + const existing = scanPlugins().find(p => p.name === manifest.name); + if (existing) { + cleanupTemp(); + return reject(new Error(`A plugin named "${manifest.name}" is already installed (in "${existing.dirName}")`)); + } + // Run npm install if package.json exists. // --ignore-scripts prevents postinstall hooks from executing arbitrary code. const packageJsonPath = path.join(tempDir, 'package.json'); diff --git a/src/components/plugins/PluginSettingsTab.tsx b/src/components/plugins/PluginSettingsTab.tsx index 65aafe17..28d4df0b 100644 --- a/src/components/plugins/PluginSettingsTab.tsx +++ b/src/components/plugins/PluginSettingsTab.tsx @@ -7,7 +7,7 @@ import PluginIcon from './PluginIcon'; const STARTER_PLUGIN_URL = 'https://github.com/cloudcli-ai/cloudcli-plugin-starter'; /* ─── Toggle Switch ─────────────────────────────────────────────────────── */ -function ToggleSwitch({ checked, onChange }: { checked: boolean; onChange: (v: boolean) => void }) { +function ToggleSwitch({ checked, onChange, ariaLabel }: { checked: boolean; onChange: (v: boolean) => void; ariaLabel: string }) { return (