From 23790bd5ee2afdc164a47a98aefcee84b83ac299 Mon Sep 17 00:00:00 2001 From: t8y2 <1156263951@qq.com> Date: Fri, 18 Sep 2026 17:01:04 +0800 Subject: [PATCH] fix(mcp): validate sibling MongoDB commands --- crates/dbx-mcp/src/server.rs | 133 +++++++++++++++++++++++++++++++---- 1 file changed, 121 insertions(+), 12 deletions(-) diff --git a/crates/dbx-mcp/src/server.rs b/crates/dbx-mcp/src/server.rs index 637d4b8e76..5529902b88 100644 --- a/crates/dbx-mcp/src/server.rs +++ b/crates/dbx-mcp/src/server.rs @@ -2180,14 +2180,19 @@ fn validate_mongo_command_with_groups( ), ) })?; - if matches!(command, MongoCommand::RunCommand { .. }) { + let (target_database, command_to_validate) = match &command { + MongoCommand::InDatabase { database, command } => (database.as_str(), command.as_ref()), + command => (database, command), + }; + ensure_database_in_scope(database_scope, target_database)?; + if matches!(command_to_validate, MongoCommand::RunCommand { .. }) { return Err(tool_error( "SQL_BLOCKED", "MongoDB runCommand is not available through MCP; review and execute it manually in DBX.", )); } - if let MongoCommand::Aggregate { pipeline, .. } = &command { - dbx_core::mcp_policy::ensure_mongo_database_execution_scope(policy, &connection.id, database, pipeline) + if let MongoCommand::Aggregate { pipeline, .. } = command_to_validate { + dbx_core::mcp_policy::ensure_mongo_database_execution_scope(policy, &connection.id, target_database, pipeline) .map_err(|error| { let (code, message) = error .strip_prefix("QUERY_ERROR: ") @@ -2196,23 +2201,24 @@ fn validate_mongo_command_with_groups( }); tool_error(code, message) })?; - for target_database in mongo_aggregate_target_databases(pipeline, database)? { - ensure_database_in_scope(database_scope, &target_database)?; + for output_database in mongo_aggregate_target_databases(pipeline, target_database)? { + ensure_database_in_scope(database_scope, &output_database)?; } } - let effective_policy = effective_policy_for_database_with_groups(policy, group_ids, connection, database); + let effective_policy = effective_policy_for_database_with_groups(policy, group_ids, connection, target_database); let permissions = mcp_permissions(connection, &effective_policy); - // getSiblingDB() targets another database for this command only; judge that one. - let target_database = command.target_database().unwrap_or(database); - let production_database = match &command { + let production_database = match command_to_validate { MongoCommand::Aggregate { pipeline, .. } => { mongo_pipeline_targets_production_database(connection, target_database, pipeline) } _ => is_production_database(connection, target_database), }; - if let Err(error) = - mongo::validate_safety(&command, permissions.allow_writes, permissions.allow_dangerous, production_database) - { + if let Err(error) = mongo::validate_safety( + command_to_validate, + permissions.allow_writes, + permissions.allow_dangerous, + production_database, + ) { return Err(match error { MongoSafetyError::WritesDisabled => tool_error( if effective_policy.read_only { "MCP_READ_ONLY" } else { "CONNECTION_READ_ONLY" }, @@ -3340,6 +3346,109 @@ mod tests { } } + #[test] + fn mongo_sibling_database_run_command_is_never_exposed_through_mcp() { + let mongo = connection("mongo", "mongo", "mongodb", "operations"); + let policy = McpGlobalPolicy { + read_only: false, + allow_dangerous_sql: true, + allowed_connection_ids: None, + ..Default::default() + }; + + let error = validate_mongo_command( + &mongo, + &policy, + &DatabaseScope::All, + "operations", + r#"db.getSiblingDB("staging").runCommand({ping: 1})"#, + ) + .unwrap_err(); + assert!(result_text(&error).contains("SQL_BLOCKED")); + assert!(result_text(&error).contains("runCommand")); + } + + #[test] + fn mongo_sibling_database_honors_target_scope_and_read_only_policy() { + let mongo = connection("mongo", "mongo", "mongodb", "operations"); + let policy = McpGlobalPolicy { + connection_policies: vec![dbx_core::storage::McpConnectionPolicy { + connection_id: "mongo".to_string(), + read_only: false, + allow_dangerous_sql: true, + execution_mode_configured: true, + execution_mode_policy_version: Some(dbx_core::mcp_policy::MCP_EXECUTION_POLICY_VERSION), + database_scope: McpDatabaseScope::Selected, + allowed_databases: vec!["operations".to_string(), "reporting".to_string()], + database_policies: vec![ + dbx_core::storage::McpDatabasePolicy { + database_name: "operations".to_string(), + read_only: false, + allow_dangerous_sql: true, + }, + dbx_core::storage::McpDatabasePolicy { + database_name: "reporting".to_string(), + read_only: true, + allow_dangerous_sql: false, + }, + ], + }], + ..Default::default() + }; + + let read_only_error = validate_mongo_command( + &mongo, + &policy, + &DatabaseScope::Selected(vec!["operations".to_string(), "reporting".to_string()]), + "operations", + r#"db.getSiblingDB("reporting").users.deleteMany({_id: 1})"#, + ) + .unwrap_err(); + assert!(result_text(&read_only_error).contains("MCP_READ_ONLY")); + + let scope_error = validate_mongo_command( + &mongo, + &policy, + &DatabaseScope::Selected(vec!["operations".to_string()]), + "operations", + r#"db.getSiblingDB("reporting").users.find({})"#, + ) + .unwrap_err(); + assert!(result_text(&scope_error).contains("DATABASE_OUT_OF_SCOPE")); + } + + #[test] + fn mongo_sibling_database_aggregate_honors_output_scope_and_production_protection() { + let mut mongo = connection("mongo", "mongo", "mongodb", "operations"); + mongo.production_databases = vec!["production".to_string()]; + let policy = McpGlobalPolicy { + read_only: false, + allow_dangerous_sql: true, + allowed_connection_ids: None, + ..Default::default() + }; + + let production_error = validate_mongo_command( + &mongo, + &policy, + &DatabaseScope::All, + "operations", + r#"db.getSiblingDB("staging").items.aggregate([{"$out":{"db":"production","coll":"archive"}}])"#, + ) + .unwrap_err(); + assert!(result_text(&production_error).contains("PRODUCTION_WRITE_BLOCKED")); + + let scope_error = validate_mongo_command( + &mongo, + &policy, + &DatabaseScope::Selected(vec!["operations".to_string(), "staging".to_string()]), + "operations", + r#"db.getSiblingDB("staging").items.aggregate([{"$merge":{"into":{"db":"archive","coll":"items"}}}])"#, + ) + .unwrap_err(); + assert!(result_text(&scope_error).contains("DATABASE_OUT_OF_SCOPE")); + } + #[test] fn agent_results_preserve_stable_backend_policy_errors() { let result = agent_result(dbx_core::agent_events::ToolResult {