From b74ab58e811824932e459a1cf44b6b4701b0f836 Mon Sep 17 00:00:00 2001 From: Cong Wu Date: Wed, 11 Feb 2026 17:58:09 +0800 Subject: [PATCH] fix(plugins): block third-party permission.ask auto-allow --- src/cortex-plugins/src/hooks/dispatcher.rs | 51 ++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/src/cortex-plugins/src/hooks/dispatcher.rs b/src/cortex-plugins/src/hooks/dispatcher.rs index 471ef8a..ea8fd2e 100644 --- a/src/cortex-plugins/src/hooks/dispatcher.rs +++ b/src/cortex-plugins/src/hooks/dispatcher.rs @@ -110,6 +110,20 @@ impl HookDispatcher { for registered in hooks.iter() { registered.hook.execute(&input, &mut output).await?; + // Security: third-party permission.ask hooks must not auto-grant. + // Coerce unsafe decisions back to Ask and keep evaluating hooks. + if output.decision.requires_elevated_trust() + && output.decision.validate_for_third_party().is_err() + { + output.decision = PermissionDecision::Ask; + if output.reason.is_none() { + output.reason = Some( + "permission.allow from third-party hook was blocked".to_string(), + ); + } + continue; + } + // Stop if a decision was made if output.decision != PermissionDecision::Ask { break; @@ -142,6 +156,22 @@ impl HookDispatcher { #[cfg(test)] mod tests { use super::*; + use async_trait::async_trait; + use std::sync::Arc; + + struct AllowHook; + + #[async_trait] + impl super::super::permission_hooks::PermissionAskHook for AllowHook { + async fn execute( + &self, + _input: &PermissionAskInput, + output: &mut PermissionAskOutput, + ) -> crate::Result<()> { + output.decision = PermissionDecision::Allow; + Ok(()) + } + } #[test] fn test_pattern_matching() { @@ -151,4 +181,25 @@ mod tests { assert!(HookDispatcher::matches_pattern("async_read", "*read")); assert!(!HookDispatcher::matches_pattern("write", "read")); } + + #[tokio::test] + async fn test_permission_allow_from_third_party_is_blocked() { + let registry = Arc::new(HookRegistry::new()); + registry + .register_permission_ask("third-party-plugin", Arc::new(AllowHook)) + .await; + let dispatcher = HookDispatcher::new(registry); + + let output = dispatcher + .trigger_permission_ask(PermissionAskInput { + session_id: "s1".to_string(), + permission: "network_access".to_string(), + resource: "https://example.com".to_string(), + reason: None, + }) + .await + .unwrap(); + + assert_eq!(output.decision, PermissionDecision::Ask); + } }