mirror of
https://github.com/tiennm99/ccs.git
synced 2026-09-03 00:17:47 +00:00
fix(security): require localhost for Claude extension /setup when dashboard auth is disabled (#1270)
* fix(security): restrict Claude extension setup endpoint * style: apply prettier formatting
This commit is contained in:
@@ -26,9 +26,12 @@ import {
|
|||||||
type ClaudeExtensionActionTarget,
|
type ClaudeExtensionActionTarget,
|
||||||
verifyClaudeExtensionBinding,
|
verifyClaudeExtensionBinding,
|
||||||
} from '../services/claude-extension-settings-service';
|
} from '../services/claude-extension-settings-service';
|
||||||
|
import { requireLocalAccessWhenAuthDisabled } from '../middleware/auth-middleware';
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
const VALID_HOSTS = new Set(CLAUDE_EXTENSION_HOSTS.map((host) => host.id));
|
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<ClaudeExtensionActionTarget>(['shared', 'ide', 'all']);
|
const VALID_TARGETS = new Set<ClaudeExtensionActionTarget>(['shared', 'ide', 'all']);
|
||||||
|
|
||||||
function getHostFromRequest(req: Request): ClaudeExtensionHost {
|
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<void> => {
|
router.get('/setup', async (req: Request, res: Response): Promise<void> => {
|
||||||
|
if (!requireLocalAccessWhenAuthDisabled(req, res, SETUP_LOCAL_ACCESS_ERROR)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const rawProfile = typeof req.query.profile === 'string' ? req.query.profile.trim() : '';
|
const rawProfile = typeof req.query.profile === 'string' ? req.query.profile.trim() : '';
|
||||||
if (!rawProfile) {
|
if (!rawProfile) {
|
||||||
res.status(400).json({ error: 'Missing required query parameter: profile' });
|
res.status(400).json({ error: 'Missing required query parameter: profile' });
|
||||||
|
|||||||
@@ -34,9 +34,8 @@ describe('web-server claude-extension-routes', () => {
|
|||||||
({ default: SharedManager } = await import('../../../src/management/shared-manager'));
|
({ default: SharedManager } = await import('../../../src/management/shared-manager'));
|
||||||
({ createEmptyUnifiedConfig } = await import('../../../src/config/unified-config-types'));
|
({ createEmptyUnifiedConfig } = await import('../../../src/config/unified-config-types'));
|
||||||
({ saveUnifiedConfig } = await import('../../../src/config/unified-config-loader'));
|
({ saveUnifiedConfig } = await import('../../../src/config/unified-config-loader'));
|
||||||
({ default: claudeExtensionRoutes } = await import(
|
({ default: claudeExtensionRoutes } =
|
||||||
'../../../src/web-server/routes/claude-extension-routes'
|
await import('../../../src/web-server/routes/claude-extension-routes'));
|
||||||
));
|
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
@@ -68,7 +67,8 @@ describe('web-server claude-extension-routes', () => {
|
|||||||
if (originalCcsHome !== undefined) process.env.CCS_HOME = originalCcsHome;
|
if (originalCcsHome !== undefined) process.env.CCS_HOME = originalCcsHome;
|
||||||
else delete process.env.CCS_HOME;
|
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;
|
else delete process.env.CLAUDE_CONFIG_DIR;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -178,6 +178,47 @@ describe('web-server claude-extension-routes', () => {
|
|||||||
expect(payload.sharedSettings.json).toContain('"env"');
|
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<Server>((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<void>((resolve) => remoteServer.close(() => resolve()));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it('normalizes the effective profile CLAUDE_CONFIG_DIR for extension setup', async () => {
|
it('normalizes the effective profile CLAUDE_CONFIG_DIR for extension setup', async () => {
|
||||||
const explicitConfigDir = path.join(tempHome, '.claude-profiles', 'glm');
|
const explicitConfigDir = path.join(tempHome, '.claude-profiles', 'glm');
|
||||||
const glmSettingsPath = path.join(tempHome, '.ccs', 'glm.settings.json');
|
const glmSettingsPath = path.join(tempHome, '.ccs', 'glm.settings.json');
|
||||||
|
|||||||
Reference in New Issue
Block a user