mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-03-13 09:57:24 +00:00
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:
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ export default function PluginTabContent({
|
||||
}, [isDarkMode, selectedProject, selectedSession]);
|
||||
|
||||
useEffect(() => {
|
||||
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 <div ref={containerRef} className="h-full w-full overflow-auto" />;
|
||||
}
|
||||
|
||||
@@ -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".
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -104,7 +104,8 @@
|
||||
"appearance": "Appearance",
|
||||
"git": "Git",
|
||||
"apiTokens": "API & Tokens",
|
||||
"tasks": "Tasks"
|
||||
"tasks": "Tasks",
|
||||
"plugins": "Plugins"
|
||||
},
|
||||
"appearanceSettings": {
|
||||
"darkMode": {
|
||||
|
||||
@@ -104,7 +104,8 @@
|
||||
"appearance": "外観",
|
||||
"git": "Git",
|
||||
"apiTokens": "API & トークン",
|
||||
"tasks": "タスク"
|
||||
"tasks": "タスク",
|
||||
"plugins": "プラグイン"
|
||||
},
|
||||
"appearanceSettings": {
|
||||
"darkMode": {
|
||||
|
||||
@@ -104,7 +104,8 @@
|
||||
"appearance": "외관",
|
||||
"git": "Git",
|
||||
"apiTokens": "API & 토큰",
|
||||
"tasks": "작업"
|
||||
"tasks": "작업",
|
||||
"plugins": "플러그인"
|
||||
},
|
||||
"appearanceSettings": {
|
||||
"darkMode": {
|
||||
|
||||
@@ -104,7 +104,8 @@
|
||||
"appearance": "外观",
|
||||
"git": "Git",
|
||||
"apiTokens": "API 和令牌",
|
||||
"tasks": "任务"
|
||||
"tasks": "任务",
|
||||
"plugins": "插件"
|
||||
},
|
||||
"appearanceSettings": {
|
||||
"darkMode": {
|
||||
|
||||
Reference in New Issue
Block a user