diff --git a/server/utils/plugin-loader.js b/server/utils/plugin-loader.js index 9400d4b7..2f961526 100644 --- a/server/utils/plugin-loader.js +++ b/server/utils/plugin-loader.js @@ -143,14 +143,16 @@ export function resolvePluginAssetPath(name, assetPath) { const resolved = path.resolve(pluginDir, assetPath); - // Prevent path traversal — resolved path must be within plugin directory - if (!resolved.startsWith(pluginDir + path.sep) && resolved !== pluginDir) { + // Prevent path traversal — canonicalize via realpath to defeat symlink bypasses + if (!fs.existsSync(resolved)) return null; + + const realResolved = fs.realpathSync(resolved); + const realPluginDir = fs.realpathSync(pluginDir); + if (!realResolved.startsWith(realPluginDir + path.sep) && realResolved !== realPluginDir) { return null; } - if (!fs.existsSync(resolved)) return null; - - return resolved; + return realResolved; } export function installPluginFromGit(url) { diff --git a/src/components/plugins/PluginIcon.tsx b/src/components/plugins/PluginIcon.tsx index 38e7127a..2ab5a635 100644 --- a/src/components/plugins/PluginIcon.tsx +++ b/src/components/plugins/PluginIcon.tsx @@ -11,15 +11,20 @@ type Props = { const svgCache = new Map(); export default function PluginIcon({ pluginName, iconFile, className }: Props) { - const url = `/api/plugins/${encodeURIComponent(pluginName)}/assets/${encodeURIComponent(iconFile)}`; - const [svg, setSvg] = useState(svgCache.get(url) ?? null); + const url = iconFile + ? `/api/plugins/${encodeURIComponent(pluginName)}/assets/${encodeURIComponent(iconFile)}` + : ''; + const [svg, setSvg] = useState(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(' { - if (!containerRef.current) return; + if (!containerRef.current || !plugin?.enabled) return; let active = true; const container = containerRef.current; @@ -120,7 +120,7 @@ export default function PluginTabContent({ contextCallbacksRef.current.clear(); moduleRef.current = null; }; - }, [pluginName, plugin?.entry]); // re-mount only when the plugin itself changes + }, [pluginName, plugin?.entry, plugin?.enabled]); // re-mount when plugin or enabled state changes return
; } diff --git a/src/components/settings/hooks/useSettingsController.ts b/src/components/settings/hooks/useSettingsController.ts index b0f8ab86..c6e66adc 100644 --- a/src/components/settings/hooks/useSettingsController.ts +++ b/src/components/settings/hooks/useSettingsController.ts @@ -98,7 +98,7 @@ type CodexSettingsStorage = { type ActiveLoginProvider = AgentProvider | ''; -const KNOWN_MAIN_TABS: SettingsMainTab[] = ['agents', 'appearance', 'git', 'api', 'tasks']; +const KNOWN_MAIN_TABS: SettingsMainTab[] = ['agents', 'appearance', 'git', 'api', 'tasks', 'plugins']; const normalizeMainTab = (tab: string): SettingsMainTab => { // Keep backwards compatibility with older callers that still pass "tools". diff --git a/src/components/settings/view/SettingsMainTabs.tsx b/src/components/settings/view/SettingsMainTabs.tsx index 44cb12c4..dd9bf60f 100644 --- a/src/components/settings/view/SettingsMainTabs.tsx +++ b/src/components/settings/view/SettingsMainTabs.tsx @@ -20,7 +20,7 @@ const TAB_CONFIG: MainTabConfig[] = [ { id: 'git', labelKey: 'mainTabs.git', icon: GitBranch }, { id: 'api', labelKey: 'mainTabs.apiTokens', icon: Key }, { id: 'tasks', labelKey: 'mainTabs.tasks' }, - { id: 'plugins', label: 'Plugins', icon: Puzzle }, + { id: 'plugins', labelKey: 'mainTabs.plugins', icon: Puzzle }, ]; export default function SettingsMainTabs({ activeTab, onChange }: SettingsMainTabsProps) { diff --git a/src/i18n/locales/en/settings.json b/src/i18n/locales/en/settings.json index 2c6a99e1..00da3136 100644 --- a/src/i18n/locales/en/settings.json +++ b/src/i18n/locales/en/settings.json @@ -104,7 +104,8 @@ "appearance": "Appearance", "git": "Git", "apiTokens": "API & Tokens", - "tasks": "Tasks" + "tasks": "Tasks", + "plugins": "Plugins" }, "appearanceSettings": { "darkMode": { diff --git a/src/i18n/locales/ja/settings.json b/src/i18n/locales/ja/settings.json index 4fd82ec8..812ce1b9 100644 --- a/src/i18n/locales/ja/settings.json +++ b/src/i18n/locales/ja/settings.json @@ -104,7 +104,8 @@ "appearance": "外観", "git": "Git", "apiTokens": "API & トークン", - "tasks": "タスク" + "tasks": "タスク", + "plugins": "プラグイン" }, "appearanceSettings": { "darkMode": { diff --git a/src/i18n/locales/ko/settings.json b/src/i18n/locales/ko/settings.json index f452291f..aa0af1e1 100644 --- a/src/i18n/locales/ko/settings.json +++ b/src/i18n/locales/ko/settings.json @@ -104,7 +104,8 @@ "appearance": "외관", "git": "Git", "apiTokens": "API & 토큰", - "tasks": "작업" + "tasks": "작업", + "plugins": "플러그인" }, "appearanceSettings": { "darkMode": { diff --git a/src/i18n/locales/zh-CN/settings.json b/src/i18n/locales/zh-CN/settings.json index cdfb5497..a80ab555 100644 --- a/src/i18n/locales/zh-CN/settings.json +++ b/src/i18n/locales/zh-CN/settings.json @@ -104,7 +104,8 @@ "appearance": "外观", "git": "Git", "apiTokens": "API 和令牌", - "tasks": "任务" + "tasks": "任务", + "plugins": "插件" }, "appearanceSettings": { "darkMode": {