From c0b79873b57a7a3e9a2d25736c4a7730240b5428 Mon Sep 17 00:00:00 2001 From: Michael Neale Date: Wed, 7 May 2025 23:44:52 +1000 Subject: [PATCH] allowlist blocks and shift SSE to warning (#2445) --- Justfile | 5 ++ crates/goose-server/ALLOWLIST.md | 6 +- ui/desktop/src/App.tsx | 99 +++++++++++++++++++++++++------- ui/desktop/src/main.ts | 2 + 4 files changed, 90 insertions(+), 22 deletions(-) diff --git a/Justfile b/Justfile index b5b95114..9cf349b9 100644 --- a/Justfile +++ b/Justfile @@ -79,6 +79,11 @@ run-ui: @echo "Running UI..." cd ui/desktop && npm install && npm run start-gui +run-ui-only: + @echo "Running UI..." + cd ui/desktop && npm install && npm run start-gui + + # Run UI with alpha changes run-ui-alpha: @just release-binary diff --git a/crates/goose-server/ALLOWLIST.md b/crates/goose-server/ALLOWLIST.md index a3b7079d..e9ac0386 100644 --- a/crates/goose-server/ALLOWLIST.md +++ b/crates/goose-server/ALLOWLIST.md @@ -1,3 +1,5 @@ +IMPORTANT: currently GOOSE_ALLOWLIST is used in main.ts in ui/desktop, and not in goose-server. The following is for reference in case it is used on the server side for launch time enforcement. + # Goose Extension Allowlist The allowlist feature provides a security mechanism for controlling which MCP commands can be used by goose. @@ -24,9 +26,11 @@ If this environment variable is not set, no allowlist restrictions will be appli In certain development or testing scenarios, you may need to bypass the allowlist restrictions. You can do this by setting the `GOOSE_ALLOWLIST_BYPASS` environment variable to `true`: ```bash -export GOOSE_ALLOWLIST_BYPASS=true +# For the GUI, you can have it show a warning instead of blocking (but it will always show a warning): +export GOOSE_ALLOWLIST_WARNING=true ``` + When this environment variable is set to `true` (case insensitive), the allowlist check will be bypassed and all commands will be allowed, even if the `GOOSE_ALLOWLIST` environment variable is set. ## Allowlist File Format diff --git a/ui/desktop/src/App.tsx b/ui/desktop/src/App.tsx index 890702de..405c7bbe 100644 --- a/ui/desktop/src/App.tsx +++ b/ui/desktop/src/App.tsx @@ -498,7 +498,15 @@ export default function App() { } }, [view]); - // TODO: modify + // Configuration for extension security + const config = window.electron.getConfig(); + // If GOOSE_ALLOWLIST_WARNING is true, use warning-only mode (STRICT_ALLOWLIST=false) + // If GOOSE_ALLOWLIST_WARNING is not set or false, use strict blocking mode (STRICT_ALLOWLIST=true) + const STRICT_ALLOWLIST = config.GOOSE_ALLOWLIST_WARNING === true ? false : true; + console.log( + `Extension security mode: ${STRICT_ALLOWLIST ? 'Strict' : 'Warning-only'} (GOOSE_ALLOWLIST_WARNING=${config.GOOSE_ALLOWLIST_WARNING})` + ); + useEffect(() => { console.log('Setting up extension handler'); const handleAddExtension = async (_event: IpcRendererEvent, link: string) => { @@ -510,36 +518,81 @@ export default function App() { window.electron.logInfo(`Adding extension from deep link ${link}`); setPendingLink(link); - // Fetch the allowlist and check if the command is allowed + // Default values for confirmation dialog let warningMessage = ''; let label = 'OK'; let title = 'Confirm Extension Installation'; - try { - const allowedCommands = await window.electron.getAllowedExtensions(); + let isBlocked = false; + let useDetailedMessage = false; - // Only check and show warning if we have a non-empty allowlist - if (allowedCommands && allowedCommands.length > 0) { - const isCommandAllowed = allowedCommands.some((allowedCmd) => - command.startsWith(allowedCmd) - ); + // For SSE extensions (with remoteUrl), always use detailed message + if (remoteUrl) { + useDetailedMessage = true; + } else { + // For command-based extensions, check against allowlist + try { + const allowedCommands = await window.electron.getAllowedExtensions(); - if (!isCommandAllowed) { - title = '⛔️ Untrusted Extension ⛔️'; - label = 'Override and install'; - warningMessage = - '\n\n⚠️ WARNING: This extension command is not in the allowed list. Installing extensions from untrusted sources may pose security risks. Please contact and admin if you are unsusure or want to allow this extension.'; + // Only check and show warning if we have a non-empty allowlist + if (allowedCommands && allowedCommands.length > 0) { + const isCommandAllowed = allowedCommands.some((allowedCmd) => + command.startsWith(allowedCmd) + ); + + if (!isCommandAllowed) { + // Not in allowlist - use detailed message and show warning/block + useDetailedMessage = true; + title = '⛔️ Untrusted Extension ⛔️'; + + if (STRICT_ALLOWLIST) { + // Block installation completely unless override is active + isBlocked = true; + label = 'Extension Blocked'; + warningMessage = + '\n\n⛔️ BLOCKED: This extension command is not in the allowed list. ' + + 'Installation is blocked by your administrator. ' + + 'Please contact your administrator if you need this extension.'; + } else { + // Allow override (either because STRICT_ALLOWLIST is false or secret key combo was used) + label = 'Override and install'; + warningMessage = + '\n\n⚠️ WARNING: This extension command is not in the allowed list. ' + + 'Installing extensions from untrusted sources may pose security risks. ' + + 'Please contact an admin if you are unsure or want to allow this extension.'; + } + } + // If in allowlist, use simple message (useDetailedMessage remains false) } + // If no allowlist, use simple message (useDetailedMessage remains false) + } catch (error) { + console.error('Error checking allowlist:', error); } - } catch (error) { - console.error('Error checking allowlist:', error); } - const messageDetails = remoteUrl ? `Remote URL: ${remoteUrl}` : `Command: ${command}`; - setModalMessage( - `Are you sure you want to install the ${extName} extension?\n\n${messageDetails}${warningMessage}` - ); + // Set the appropriate message based on the extension type and allowlist status + if (useDetailedMessage) { + // Detailed message for SSE extensions or non-allowlisted command extensions + const detailedMessage = remoteUrl + ? `You are about to install the ${extName} extension which connects to:\n\n${remoteUrl}\n\nThis extension will be able to access your conversations and provide additional functionality.` + : `You are about to install the ${extName} extension which runs the command:\n\n${command}\n\nThis extension will be able to access your conversations and provide additional functionality.`; + + setModalMessage(`${detailedMessage}${warningMessage}`); + } else { + // Simple message for allowlisted command extensions or when no allowlist exists + const messageDetails = `Command: ${command}`; + setModalMessage( + `Are you sure you want to install the ${extName} extension?\n\n${messageDetails}` + ); + } + setExtensionConfirmLabel(label); setExtensionConfirmTitle(title); + + // If blocked, disable the confirmation button functionality by setting a special flag + if (isBlocked) { + setPendingLink(null); // Clear the pending link so confirmation does nothing + } + setModalVisible(true); } catch (error) { console.error('Error handling add-extension event:', error); @@ -550,7 +603,7 @@ export default function App() { return () => { window.electron.off('add-extension', handleAddExtension); }; - }, []); + }, [STRICT_ALLOWLIST]); // Focus the first found input field useEffect(() => { @@ -584,6 +637,10 @@ export default function App() { } finally { setPendingLink(null); } + } else { + // This case happens when pendingLink was cleared due to blocking + console.log('Extension installation blocked by allowlist restrictions'); + setModalVisible(false); } }; diff --git a/ui/desktop/src/main.ts b/ui/desktop/src/main.ts index c89a3f9a..7d806f89 100644 --- a/ui/desktop/src/main.ts +++ b/ui/desktop/src/main.ts @@ -272,6 +272,8 @@ let appConfig = { GOOSE_API_HOST: 'http://127.0.0.1', GOOSE_PORT: 0, GOOSE_WORKING_DIR: '', + // If GOOSE_ALLOWLIST_WARNING env var is not set, defaults to false (strict blocking mode) + GOOSE_ALLOWLIST_WARNING: process.env.GOOSE_ALLOWLIST_WARNING === 'true', secretKey: generateSecretKey(), };