diff --git a/src/web-server/routes/claude-extension-routes.ts b/src/web-server/routes/claude-extension-routes.ts index 76c66225..e89b22ad 100644 --- a/src/web-server/routes/claude-extension-routes.ts +++ b/src/web-server/routes/claude-extension-routes.ts @@ -26,9 +26,12 @@ import { type ClaudeExtensionActionTarget, verifyClaudeExtensionBinding, } from '../services/claude-extension-settings-service'; +import { requireLocalAccessWhenAuthDisabled } from '../middleware/auth-middleware'; const router = Router(); const VALID_HOSTS = new Set(CLAUDE_EXTENSION_HOSTS.map((host) => host.id)); +const SETUP_LOCAL_ACCESS_ERROR = + 'Claude extension setup requires localhost access when dashboard auth is disabled.'; const VALID_TARGETS = new Set(['shared', 'ide', 'all']); function getHostFromRequest(req: Request): ClaudeExtensionHost { @@ -79,6 +82,10 @@ router.get('/profiles', (_req: Request, res: Response): void => { }); router.get('/setup', async (req: Request, res: Response): Promise => { + if (!requireLocalAccessWhenAuthDisabled(req, res, SETUP_LOCAL_ACCESS_ERROR)) { + return; + } + const rawProfile = typeof req.query.profile === 'string' ? req.query.profile.trim() : ''; if (!rawProfile) { res.status(400).json({ error: 'Missing required query parameter: profile' }); diff --git a/tests/unit/web-server/claude-extension-routes.test.ts b/tests/unit/web-server/claude-extension-routes.test.ts index 753e40fd..a37372e5 100644 --- a/tests/unit/web-server/claude-extension-routes.test.ts +++ b/tests/unit/web-server/claude-extension-routes.test.ts @@ -34,9 +34,8 @@ describe('web-server claude-extension-routes', () => { ({ default: SharedManager } = await import('../../../src/management/shared-manager')); ({ createEmptyUnifiedConfig } = await import('../../../src/config/unified-config-types')); ({ saveUnifiedConfig } = await import('../../../src/config/unified-config-loader')); - ({ default: claudeExtensionRoutes } = await import( - '../../../src/web-server/routes/claude-extension-routes' - )); + ({ default: claudeExtensionRoutes } = + await import('../../../src/web-server/routes/claude-extension-routes')); const app = express(); app.use(express.json()); @@ -68,7 +67,8 @@ describe('web-server claude-extension-routes', () => { if (originalCcsHome !== undefined) process.env.CCS_HOME = originalCcsHome; else delete process.env.CCS_HOME; - if (originalClaudeConfigDir !== undefined) process.env.CLAUDE_CONFIG_DIR = originalClaudeConfigDir; + if (originalClaudeConfigDir !== undefined) + process.env.CLAUDE_CONFIG_DIR = originalClaudeConfigDir; else delete process.env.CLAUDE_CONFIG_DIR; }); @@ -178,6 +178,47 @@ describe('web-server claude-extension-routes', () => { expect(payload.sharedSettings.json).toContain('"env"'); }); + it('blocks non-local setup requests when dashboard auth is disabled', async () => { + const app = express(); + app.use((_req, _res, next) => { + Object.defineProperty(_req.socket, 'remoteAddress', { + configurable: true, + value: '10.0.0.25', + }); + next(); + }); + app.use('/api/claude-extension', claudeExtensionRoutes); + + const remoteServer = await new Promise((resolve, reject) => { + const instance = app.listen(0, '127.0.0.1'); + const handleError = (error: Error) => reject(error); + instance.once('error', handleError); + instance.once('listening', () => { + instance.off('error', handleError); + resolve(instance); + }); + }); + + try { + const address = remoteServer.address(); + if (!address || typeof address === 'string') { + throw new Error('Unable to resolve remote test server port'); + } + + const response = await fetch( + `http://127.0.0.1:${address.port}/api/claude-extension/setup?profile=glm&host=vscode` + ); + expect(response.status).toBe(403); + + const payload = (await response.json()) as { error: string }; + expect(payload.error).toBe( + 'Claude extension setup requires localhost access when dashboard auth is disabled.' + ); + } finally { + await new Promise((resolve) => remoteServer.close(() => resolve())); + } + }); + it('normalizes the effective profile CLAUDE_CONFIG_DIR for extension setup', async () => { const explicitConfigDir = path.join(tempHome, '.claude-profiles', 'glm'); const glmSettingsPath = path.join(tempHome, '.ccs', 'glm.settings.json');