fix(core): prevent server name spoofing in policy engine (#12511)

This commit is contained in:
Allen Hutchison
2025-11-05 10:10:23 -08:00
committed by GitHub
parent 16113647de
commit f5bd474e51
8 changed files with 411 additions and 121 deletions

View File

@@ -30,18 +30,22 @@ describe('Policy Engine Integration Tests', () => {
const engine = new PolicyEngine(config);
// Allowed tool should be allowed
expect(engine.check({ name: 'run_shell_command' })).toBe(
expect(engine.check({ name: 'run_shell_command' }, undefined)).toBe(
PolicyDecision.ALLOW,
);
// Excluded tool should be denied
expect(engine.check({ name: 'write_file' })).toBe(PolicyDecision.DENY);
expect(engine.check({ name: 'write_file' }, undefined)).toBe(
PolicyDecision.DENY,
);
// Other write tools should ask user
expect(engine.check({ name: 'replace' })).toBe(PolicyDecision.ASK_USER);
expect(engine.check({ name: 'replace' }, undefined)).toBe(
PolicyDecision.ASK_USER,
);
// Unknown tools should use default
expect(engine.check({ name: 'unknown_tool' })).toBe(
expect(engine.check({ name: 'unknown_tool' }, undefined)).toBe(
PolicyDecision.ASK_USER,
);
});
@@ -68,31 +72,31 @@ describe('Policy Engine Integration Tests', () => {
const engine = new PolicyEngine(config);
// Tools from allowed server should be allowed
expect(engine.check({ name: 'allowed-server__tool1' })).toBe(
PolicyDecision.ALLOW,
);
expect(engine.check({ name: 'allowed-server__another_tool' })).toBe(
expect(engine.check({ name: 'allowed-server__tool1' }, undefined)).toBe(
PolicyDecision.ALLOW,
);
expect(
engine.check({ name: 'allowed-server__another_tool' }, undefined),
).toBe(PolicyDecision.ALLOW);
// Tools from trusted server should be allowed
expect(engine.check({ name: 'trusted-server__tool1' })).toBe(
PolicyDecision.ALLOW,
);
expect(engine.check({ name: 'trusted-server__special_tool' })).toBe(
expect(engine.check({ name: 'trusted-server__tool1' }, undefined)).toBe(
PolicyDecision.ALLOW,
);
expect(
engine.check({ name: 'trusted-server__special_tool' }, undefined),
).toBe(PolicyDecision.ALLOW);
// Tools from blocked server should be denied
expect(engine.check({ name: 'blocked-server__tool1' })).toBe(
PolicyDecision.DENY,
);
expect(engine.check({ name: 'blocked-server__any_tool' })).toBe(
expect(engine.check({ name: 'blocked-server__tool1' }, undefined)).toBe(
PolicyDecision.DENY,
);
expect(
engine.check({ name: 'blocked-server__any_tool' }, undefined),
).toBe(PolicyDecision.DENY);
// Tools from unknown servers should use default
expect(engine.check({ name: 'unknown-server__tool' })).toBe(
expect(engine.check({ name: 'unknown-server__tool' }, undefined)).toBe(
PolicyDecision.ASK_USER,
);
});
@@ -114,13 +118,13 @@ describe('Policy Engine Integration Tests', () => {
const engine = new PolicyEngine(config);
// MCP server allowed (priority 2.1) provides general allow for server
expect(engine.check({ name: 'my-server__safe-tool' })).toBe(
expect(engine.check({ name: 'my-server__safe-tool' }, undefined)).toBe(
PolicyDecision.ALLOW,
);
// But specific tool exclude (priority 2.4) wins over server allow
expect(engine.check({ name: 'my-server__dangerous-tool' })).toBe(
PolicyDecision.DENY,
);
expect(
engine.check({ name: 'my-server__dangerous-tool' }, undefined),
).toBe(PolicyDecision.DENY);
});
it('should handle complex mixed configurations', async () => {
@@ -150,36 +154,44 @@ describe('Policy Engine Integration Tests', () => {
const engine = new PolicyEngine(config);
// Read-only tools should be allowed (autoAccept)
expect(engine.check({ name: 'read_file' })).toBe(PolicyDecision.ALLOW);
expect(engine.check({ name: 'list_directory' })).toBe(
expect(engine.check({ name: 'read_file' }, undefined)).toBe(
PolicyDecision.ALLOW,
);
expect(engine.check({ name: 'list_directory' }, undefined)).toBe(
PolicyDecision.ALLOW,
);
// But glob is explicitly excluded, so it should be denied
expect(engine.check({ name: 'glob' })).toBe(PolicyDecision.DENY);
expect(engine.check({ name: 'glob' }, undefined)).toBe(
PolicyDecision.DENY,
);
// Replace should ask user (normal write tool behavior)
expect(engine.check({ name: 'replace' })).toBe(PolicyDecision.ASK_USER);
expect(engine.check({ name: 'replace' }, undefined)).toBe(
PolicyDecision.ASK_USER,
);
// Explicitly allowed tools
expect(engine.check({ name: 'custom-tool' })).toBe(PolicyDecision.ALLOW);
expect(engine.check({ name: 'my-server__special-tool' })).toBe(
expect(engine.check({ name: 'custom-tool' }, undefined)).toBe(
PolicyDecision.ALLOW,
);
expect(engine.check({ name: 'my-server__special-tool' }, undefined)).toBe(
PolicyDecision.ALLOW,
);
// MCP server tools
expect(engine.check({ name: 'allowed-server__tool' })).toBe(
expect(engine.check({ name: 'allowed-server__tool' }, undefined)).toBe(
PolicyDecision.ALLOW,
);
expect(engine.check({ name: 'trusted-server__tool' })).toBe(
expect(engine.check({ name: 'trusted-server__tool' }, undefined)).toBe(
PolicyDecision.ALLOW,
);
expect(engine.check({ name: 'blocked-server__tool' })).toBe(
expect(engine.check({ name: 'blocked-server__tool' }, undefined)).toBe(
PolicyDecision.DENY,
);
// Write tools should ask by default
expect(engine.check({ name: 'write_file' })).toBe(
expect(engine.check({ name: 'write_file' }, undefined)).toBe(
PolicyDecision.ASK_USER,
);
});
@@ -198,14 +210,18 @@ describe('Policy Engine Integration Tests', () => {
const engine = new PolicyEngine(config);
// Most tools should be allowed in YOLO mode
expect(engine.check({ name: 'run_shell_command' })).toBe(
expect(engine.check({ name: 'run_shell_command' }, undefined)).toBe(
PolicyDecision.ALLOW,
);
expect(engine.check({ name: 'write_file' }, undefined)).toBe(
PolicyDecision.ALLOW,
);
expect(engine.check({ name: 'unknown_tool' }, undefined)).toBe(
PolicyDecision.ALLOW,
);
expect(engine.check({ name: 'write_file' })).toBe(PolicyDecision.ALLOW);
expect(engine.check({ name: 'unknown_tool' })).toBe(PolicyDecision.ALLOW);
// But explicitly excluded tools should still be denied
expect(engine.check({ name: 'dangerous-tool' })).toBe(
expect(engine.check({ name: 'dangerous-tool' }, undefined)).toBe(
PolicyDecision.DENY,
);
});
@@ -220,11 +236,15 @@ describe('Policy Engine Integration Tests', () => {
const engine = new PolicyEngine(config);
// Edit tools should be allowed in AUTO_EDIT mode
expect(engine.check({ name: 'replace' })).toBe(PolicyDecision.ALLOW);
expect(engine.check({ name: 'write_file' })).toBe(PolicyDecision.ALLOW);
expect(engine.check({ name: 'replace' }, undefined)).toBe(
PolicyDecision.ALLOW,
);
expect(engine.check({ name: 'write_file' }, undefined)).toBe(
PolicyDecision.ALLOW,
);
// Other tools should follow normal rules
expect(engine.check({ name: 'run_shell_command' })).toBe(
expect(engine.check({ name: 'run_shell_command' }, undefined)).toBe(
PolicyDecision.ASK_USER,
);
});
@@ -285,20 +305,24 @@ describe('Policy Engine Integration Tests', () => {
expect(readOnlyToolRule?.priority).toBeCloseTo(1.05, 5);
// Verify the engine applies these priorities correctly
expect(engine.check({ name: 'blocked-tool' })).toBe(PolicyDecision.DENY);
expect(engine.check({ name: 'blocked-server__any' })).toBe(
expect(engine.check({ name: 'blocked-tool' }, undefined)).toBe(
PolicyDecision.DENY,
);
expect(engine.check({ name: 'specific-tool' })).toBe(
expect(engine.check({ name: 'blocked-server__any' }, undefined)).toBe(
PolicyDecision.DENY,
);
expect(engine.check({ name: 'specific-tool' }, undefined)).toBe(
PolicyDecision.ALLOW,
);
expect(engine.check({ name: 'trusted-server__any' })).toBe(
expect(engine.check({ name: 'trusted-server__any' }, undefined)).toBe(
PolicyDecision.ALLOW,
);
expect(engine.check({ name: 'mcp-server__any' })).toBe(
expect(engine.check({ name: 'mcp-server__any' }, undefined)).toBe(
PolicyDecision.ALLOW,
);
expect(engine.check({ name: 'glob' }, undefined)).toBe(
PolicyDecision.ALLOW,
);
expect(engine.check({ name: 'glob' })).toBe(PolicyDecision.ALLOW);
});
it('should handle edge case: MCP server with both trust and exclusion', async () => {
@@ -322,7 +346,7 @@ describe('Policy Engine Integration Tests', () => {
const engine = new PolicyEngine(config);
// Exclusion (195) should win over trust (90)
expect(engine.check({ name: 'conflicted-server__tool' })).toBe(
expect(engine.check({ name: 'conflicted-server__tool' }, undefined)).toBe(
PolicyDecision.DENY,
);
});
@@ -345,10 +369,10 @@ describe('Policy Engine Integration Tests', () => {
// Server exclusion (195) wins over specific tool allow (100)
// This might be counterintuitive but follows the priority system
expect(engine.check({ name: 'my-server__special-tool' })).toBe(
expect(engine.check({ name: 'my-server__special-tool' }, undefined)).toBe(
PolicyDecision.DENY,
);
expect(engine.check({ name: 'my-server__other-tool' })).toBe(
expect(engine.check({ name: 'my-server__other-tool' }, undefined)).toBe(
PolicyDecision.DENY,
);
});
@@ -365,8 +389,10 @@ describe('Policy Engine Integration Tests', () => {
const engine = new PolicyEngine(engineConfig);
// ASK_USER should become DENY in non-interactive mode
expect(engine.check({ name: 'unknown_tool' })).toBe(PolicyDecision.DENY);
expect(engine.check({ name: 'run_shell_command' })).toBe(
expect(engine.check({ name: 'unknown_tool' }, undefined)).toBe(
PolicyDecision.DENY,
);
expect(engine.check({ name: 'run_shell_command' }, undefined)).toBe(
PolicyDecision.DENY,
);
});
@@ -381,13 +407,17 @@ describe('Policy Engine Integration Tests', () => {
const engine = new PolicyEngine(config);
// Should have default rules for write tools
expect(engine.check({ name: 'write_file' })).toBe(
expect(engine.check({ name: 'write_file' }, undefined)).toBe(
PolicyDecision.ASK_USER,
);
expect(engine.check({ name: 'replace' }, undefined)).toBe(
PolicyDecision.ASK_USER,
);
expect(engine.check({ name: 'replace' })).toBe(PolicyDecision.ASK_USER);
// Unknown tools should use default
expect(engine.check({ name: 'unknown' })).toBe(PolicyDecision.ASK_USER);
expect(engine.check({ name: 'unknown' }, undefined)).toBe(
PolicyDecision.ASK_USER,
);
});
it('should verify rules are created with correct priorities', async () => {