From b92d3569fd2eb1ffd598fb0e59d8c7b009edce26 Mon Sep 17 00:00:00 2001 From: Hunter Bown Date: Sat, 9 May 2026 22:30:16 -0500 Subject: [PATCH] =?UTF-8?q?chore(release):=20prepare=20v0.8.26=20=E2=80=94?= =?UTF-8?q?=20security=20hotfix?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two responsibly-disclosed security fixes: - GHSA-88gh-2526-gfrr (@JafarAkhondali) - GHSA-72w5-pf8h-xfp4 (@47Cid) Plus version bump, CHANGELOG, regression tests for both. Co-Authored-By: Claude Opus 4.7 (1M context) --- CHANGELOG.md | 15 +++++++++ Cargo.lock | 28 ++++++++-------- Cargo.toml | 2 +- crates/agent/Cargo.toml | 2 +- crates/app-server/Cargo.toml | 18 +++++----- crates/cli/Cargo.toml | 14 ++++---- crates/config/Cargo.toml | 2 +- crates/core/Cargo.toml | 16 ++++----- crates/execpolicy/Cargo.toml | 2 +- crates/hooks/Cargo.toml | 2 +- crates/tools/Cargo.toml | 2 +- crates/tui/Cargo.toml | 4 +-- crates/tui/src/config.rs | 16 +++++++-- crates/tui/src/task_manager.rs | 39 +++++++++++++++++++++- crates/tui/src/tools/fetch_url.rs | 55 ++++++++++++++++++++++++++++++- npm/deepseek-tui/package.json | 4 +-- 16 files changed, 169 insertions(+), 52 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bf2f0d95..0049abdb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,21 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.8.26] - 2026-05-09 + +A security release. Two responsibly-disclosed issues were patched. Big +thanks to **@JafarAkhondali** and **@47Cid** for the disclosures. + +### Security + +- Hardened the `fetch_url` tool's network-target validation + (GHSA-88gh-2526-gfrr). Thanks to **@JafarAkhondali**. +- Tightened the default privileges of sub-agents created through + `task_create` (GHSA-72w5-pf8h-xfp4). Thanks to **@47Cid**. + +Both items will have full advisory text once the GHSA entries are +published. + ## [0.8.25] - 2026-05-09 A stabilization + drift-fixes release. Headline work hardens the diff --git a/Cargo.lock b/Cargo.lock index 1677aea4..2b68a81c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1151,7 +1151,7 @@ dependencies = [ [[package]] name = "deepseek-agent" -version = "0.8.25" +version = "0.8.26" dependencies = [ "deepseek-config", "serde", @@ -1159,7 +1159,7 @@ dependencies = [ [[package]] name = "deepseek-app-server" -version = "0.8.25" +version = "0.8.26" dependencies = [ "anyhow", "axum", @@ -1181,7 +1181,7 @@ dependencies = [ [[package]] name = "deepseek-config" -version = "0.8.25" +version = "0.8.26" dependencies = [ "anyhow", "deepseek-secrets", @@ -1193,7 +1193,7 @@ dependencies = [ [[package]] name = "deepseek-core" -version = "0.8.25" +version = "0.8.26" dependencies = [ "anyhow", "chrono", @@ -1211,7 +1211,7 @@ dependencies = [ [[package]] name = "deepseek-execpolicy" -version = "0.8.25" +version = "0.8.26" dependencies = [ "anyhow", "deepseek-protocol", @@ -1220,7 +1220,7 @@ dependencies = [ [[package]] name = "deepseek-hooks" -version = "0.8.25" +version = "0.8.26" dependencies = [ "anyhow", "async-trait", @@ -1234,7 +1234,7 @@ dependencies = [ [[package]] name = "deepseek-mcp" -version = "0.8.25" +version = "0.8.26" dependencies = [ "anyhow", "serde", @@ -1243,7 +1243,7 @@ dependencies = [ [[package]] name = "deepseek-protocol" -version = "0.8.25" +version = "0.8.26" dependencies = [ "serde", "serde_json", @@ -1251,7 +1251,7 @@ dependencies = [ [[package]] name = "deepseek-secrets" -version = "0.8.25" +version = "0.8.26" dependencies = [ "dirs", "keyring", @@ -1264,7 +1264,7 @@ dependencies = [ [[package]] name = "deepseek-state" -version = "0.8.25" +version = "0.8.26" dependencies = [ "anyhow", "chrono", @@ -1276,7 +1276,7 @@ dependencies = [ [[package]] name = "deepseek-tools" -version = "0.8.25" +version = "0.8.26" dependencies = [ "anyhow", "async-trait", @@ -1289,7 +1289,7 @@ dependencies = [ [[package]] name = "deepseek-tui" -version = "0.8.25" +version = "0.8.26" dependencies = [ "anyhow", "arboard", @@ -1350,7 +1350,7 @@ dependencies = [ [[package]] name = "deepseek-tui-cli" -version = "0.8.25" +version = "0.8.26" dependencies = [ "anyhow", "chrono", @@ -1375,7 +1375,7 @@ dependencies = [ [[package]] name = "deepseek-tui-core" -version = "0.8.25" +version = "0.8.26" [[package]] name = "deltae" diff --git a/Cargo.toml b/Cargo.toml index 9aa88fa0..a993404c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,7 +19,7 @@ default-members = ["crates/cli", "crates/app-server", "crates/tui"] resolver = "2" [workspace.package] -version = "0.8.25" +version = "0.8.26" edition = "2024" # Rust 1.88 stabilized `let_chains` in `if`/`while` conditions, which the # codebase relies on extensively. Cargo enforces this so users on older diff --git a/crates/agent/Cargo.toml b/crates/agent/Cargo.toml index 42425e37..4475f1d5 100644 --- a/crates/agent/Cargo.toml +++ b/crates/agent/Cargo.toml @@ -7,5 +7,5 @@ repository.workspace = true description = "Model/provider registry and fallback strategy for DeepSeek workspace architecture" [dependencies] -deepseek-config = { path = "../config", version = "0.8.25" } +deepseek-config = { path = "../config", version = "0.8.26" } serde.workspace = true diff --git a/crates/app-server/Cargo.toml b/crates/app-server/Cargo.toml index 223ebbec..1e342820 100644 --- a/crates/app-server/Cargo.toml +++ b/crates/app-server/Cargo.toml @@ -10,15 +10,15 @@ description = "Codex-style app-server transport for DeepSeek workspace architect anyhow.workspace = true axum.workspace = true clap.workspace = true -deepseek-agent = { path = "../agent", version = "0.8.25" } -deepseek-config = { path = "../config", version = "0.8.25" } -deepseek-core = { path = "../core", version = "0.8.25" } -deepseek-execpolicy = { path = "../execpolicy", version = "0.8.25" } -deepseek-hooks = { path = "../hooks", version = "0.8.25" } -deepseek-mcp = { path = "../mcp", version = "0.8.25" } -deepseek-protocol = { path = "../protocol", version = "0.8.25" } -deepseek-state = { path = "../state", version = "0.8.25" } -deepseek-tools = { path = "../tools", version = "0.8.25" } +deepseek-agent = { path = "../agent", version = "0.8.26" } +deepseek-config = { path = "../config", version = "0.8.26" } +deepseek-core = { path = "../core", version = "0.8.26" } +deepseek-execpolicy = { path = "../execpolicy", version = "0.8.26" } +deepseek-hooks = { path = "../hooks", version = "0.8.26" } +deepseek-mcp = { path = "../mcp", version = "0.8.26" } +deepseek-protocol = { path = "../protocol", version = "0.8.26" } +deepseek-state = { path = "../state", version = "0.8.26" } +deepseek-tools = { path = "../tools", version = "0.8.26" } serde.workspace = true serde_json.workspace = true tokio.workspace = true diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index d96ca61e..7d66b6d2 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -14,13 +14,13 @@ path = "src/main.rs" anyhow.workspace = true clap.workspace = true clap_complete.workspace = true -deepseek-agent = { path = "../agent", version = "0.8.25" } -deepseek-app-server = { path = "../app-server", version = "0.8.25" } -deepseek-config = { path = "../config", version = "0.8.25" } -deepseek-execpolicy = { path = "../execpolicy", version = "0.8.25" } -deepseek-mcp = { path = "../mcp", version = "0.8.25" } -deepseek-secrets = { path = "../secrets", version = "0.8.25" } -deepseek-state = { path = "../state", version = "0.8.25" } +deepseek-agent = { path = "../agent", version = "0.8.26" } +deepseek-app-server = { path = "../app-server", version = "0.8.26" } +deepseek-config = { path = "../config", version = "0.8.26" } +deepseek-execpolicy = { path = "../execpolicy", version = "0.8.26" } +deepseek-mcp = { path = "../mcp", version = "0.8.26" } +deepseek-secrets = { path = "../secrets", version = "0.8.26" } +deepseek-state = { path = "../state", version = "0.8.26" } chrono.workspace = true dirs.workspace = true serde.workspace = true diff --git a/crates/config/Cargo.toml b/crates/config/Cargo.toml index 5b3a9aa8..c61e5139 100644 --- a/crates/config/Cargo.toml +++ b/crates/config/Cargo.toml @@ -8,7 +8,7 @@ description = "Config schema and precedence model for DeepSeek workspace archite [dependencies] anyhow.workspace = true -deepseek-secrets = { path = "../secrets", version = "0.8.25" } +deepseek-secrets = { path = "../secrets", version = "0.8.26" } dirs.workspace = true serde.workspace = true toml.workspace = true diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index 61aec00d..5c1ac9bf 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -9,13 +9,13 @@ description = "Core runtime boundaries for DeepSeek workspace architecture" [dependencies] anyhow.workspace = true chrono.workspace = true -deepseek-agent = { path = "../agent", version = "0.8.25" } -deepseek-config = { path = "../config", version = "0.8.25" } -deepseek-execpolicy = { path = "../execpolicy", version = "0.8.25" } -deepseek-hooks = { path = "../hooks", version = "0.8.25" } -deepseek-mcp = { path = "../mcp", version = "0.8.25" } -deepseek-protocol = { path = "../protocol", version = "0.8.25" } -deepseek-state = { path = "../state", version = "0.8.25" } -deepseek-tools = { path = "../tools", version = "0.8.25" } +deepseek-agent = { path = "../agent", version = "0.8.26" } +deepseek-config = { path = "../config", version = "0.8.26" } +deepseek-execpolicy = { path = "../execpolicy", version = "0.8.26" } +deepseek-hooks = { path = "../hooks", version = "0.8.26" } +deepseek-mcp = { path = "../mcp", version = "0.8.26" } +deepseek-protocol = { path = "../protocol", version = "0.8.26" } +deepseek-state = { path = "../state", version = "0.8.26" } +deepseek-tools = { path = "../tools", version = "0.8.26" } serde_json.workspace = true uuid.workspace = true diff --git a/crates/execpolicy/Cargo.toml b/crates/execpolicy/Cargo.toml index b5b962a3..58aea3dd 100644 --- a/crates/execpolicy/Cargo.toml +++ b/crates/execpolicy/Cargo.toml @@ -8,5 +8,5 @@ description = "Execution policy and approval model parity for DeepSeek workspace [dependencies] anyhow.workspace = true -deepseek-protocol = { path = "../protocol", version = "0.8.25" } +deepseek-protocol = { path = "../protocol", version = "0.8.26" } serde.workspace = true diff --git a/crates/hooks/Cargo.toml b/crates/hooks/Cargo.toml index 20a1a106..b84f4d17 100644 --- a/crates/hooks/Cargo.toml +++ b/crates/hooks/Cargo.toml @@ -10,7 +10,7 @@ description = "Hook dispatch and notifications parity for DeepSeek workspace arc anyhow.workspace = true async-trait.workspace = true chrono.workspace = true -deepseek-protocol = { path = "../protocol", version = "0.8.25" } +deepseek-protocol = { path = "../protocol", version = "0.8.26" } reqwest.workspace = true serde.workspace = true serde_json.workspace = true diff --git a/crates/tools/Cargo.toml b/crates/tools/Cargo.toml index 9766d5b2..3de98881 100644 --- a/crates/tools/Cargo.toml +++ b/crates/tools/Cargo.toml @@ -9,7 +9,7 @@ description = "Tool invocation lifecycle, schema validation, and scheduler paral [dependencies] anyhow.workspace = true async-trait.workspace = true -deepseek-protocol = { path = "../protocol", version = "0.8.25" } +deepseek-protocol = { path = "../protocol", version = "0.8.26" } serde.workspace = true serde_json.workspace = true tokio.workspace = true diff --git a/crates/tui/Cargo.toml b/crates/tui/Cargo.toml index c9c8540d..0e9beb30 100644 --- a/crates/tui/Cargo.toml +++ b/crates/tui/Cargo.toml @@ -21,8 +21,8 @@ path = "src/main.rs" [dependencies] anyhow = "1.0.100" arboard = "3.4" -deepseek-secrets = { path = "../secrets", version = "0.8.25" } -deepseek-tools = { path = "../tools", version = "0.8.25" } +deepseek-secrets = { path = "../secrets", version = "0.8.26" } +deepseek-tools = { path = "../tools", version = "0.8.26" } schemaui = { version = "0.12.0", default-features = false, optional = true } async-stream = "0.3.6" async-trait = "0.1" diff --git a/crates/tui/src/config.rs b/crates/tui/src/config.rs index 7fefea79..851d0047 100644 --- a/crates/tui/src/config.rs +++ b/crates/tui/src/config.rs @@ -1492,10 +1492,11 @@ impl Config { self.context.project_pack.unwrap_or(true) } - /// Return whether shell execution is allowed. + /// Return whether shell execution is allowed. Defaults to `false`: shell + /// access must be opted into explicitly (GHSA-72w5-pf8h-xfp4). #[must_use] pub fn allow_shell(&self) -> bool { - self.allow_shell.unwrap_or(true) + self.allow_shell.unwrap_or(false) } /// Return the maximum number of concurrent sub-agents. @@ -3120,6 +3121,17 @@ mod tests { use std::os::unix::fs::PermissionsExt; use std::time::{SystemTime, UNIX_EPOCH}; + // GHSA-72w5-pf8h-xfp4 — regression: `allow_shell` must be opt-in. + #[test] + fn allow_shell_defaults_to_false_when_unset() { + let config = Config::default(); + assert_eq!(config.allow_shell, None, "default Config has no opt-in set"); + assert!( + !config.allow_shell(), + "Config::allow_shell() must default to false when no opt-in is recorded" + ); + } + #[test] fn network_policy_toml_maps_proxy_hosts_to_runtime_policy() { let policy: NetworkPolicyToml = toml::from_str( diff --git a/crates/tui/src/task_manager.rs b/crates/tui/src/task_manager.rs index e19d146e..f78179de 100644 --- a/crates/tui/src/task_manager.rs +++ b/crates/tui/src/task_manager.rs @@ -828,7 +828,9 @@ impl TaskManager { mode: req.mode.unwrap_or_else(|| self.cfg.default_mode.clone()), allow_shell: req.allow_shell.unwrap_or(self.cfg.allow_shell), trust_mode: req.trust_mode.unwrap_or(self.cfg.trust_mode), - auto_approve: req.auto_approve.unwrap_or(true), + // Auto-approval must be opted into explicitly + // (GHSA-72w5-pf8h-xfp4). + auto_approve: req.auto_approve.unwrap_or(false), status: TaskStatus::Queued, created_at: Utc::now(), started_at: None, @@ -1818,6 +1820,41 @@ mod tests { Ok(()) } + // GHSA-72w5-pf8h-xfp4 — regression: omitted optional fields must not + // silently elevate the spawned task's privileges. + #[tokio::test] + async fn add_task_without_optional_fields_does_not_grant_shell_or_auto_approve() -> Result<()> { + let root = std::env::temp_dir().join(format!("deepseek-task-test-{}", Uuid::new_v4())); + let manager = + TaskManager::start_with_executor(test_config(root.clone()), Arc::new(MockExecutor)) + .await?; + + let req = NewTaskRequest { + prompt: "fix TODOs and write a README".to_string(), + model: None, + workspace: None, + mode: None, + allow_shell: None, + trust_mode: None, + auto_approve: None, + }; + let task = manager.add_task(req).await?; + + assert!( + !task.allow_shell, + "model-omitted allow_shell must default to false (no silent shell grant)" + ); + assert!( + !task.auto_approve, + "model-omitted auto_approve must default to false (no silent auto-approval)" + ); + assert!( + !task.trust_mode, + "model-omitted trust_mode must default to false" + ); + Ok(()) + } + #[tokio::test] async fn rejects_newer_task_schema_on_recovery() -> Result<()> { let root = std::env::temp_dir().join(format!("deepseek-task-test-{}", Uuid::new_v4())); diff --git a/crates/tui/src/tools/fetch_url.rs b/crates/tui/src/tools/fetch_url.rs index 188b05b7..4e62b079 100644 --- a/crates/tui/src/tools/fetch_url.rs +++ b/crates/tui/src/tools/fetch_url.rs @@ -318,7 +318,14 @@ async fn validate_fetch_target( "requests to localhost are not allowed", )); } - if let Ok(ip) = host.parse::() { + // Normalize bracketed IPv6 literals before the literal-IP check so they + // route through the same restricted-IP policy as unbracketed forms + // (GHSA-88gh-2526-gfrr). + let ip_candidate = host + .strip_prefix('[') + .and_then(|s| s.strip_suffix(']')) + .unwrap_or(host.as_str()); + if let Ok(ip) = ip_candidate.parse::() { if is_restricted_ip(&ip) { return Err(ToolError::permission_denied(format!( "IP {ip} is a restricted address (private/loopback/link-local)" @@ -569,6 +576,52 @@ mod tests { assert!(format!("{err}").contains("restricted address")); } + // GHSA-88gh-2526-gfrr — regression coverage for bracketed IPv6 literals. + #[tokio::test] + async fn rejects_ipv6_literal_loopback() { + let url = reqwest::Url::parse("http://[::1]/").unwrap(); + let err = validate_fetch_target(&url, &ctx()) + .await + .expect_err("[::1] must be rejected as restricted"); + assert!(format!("{err}").contains("restricted")); + } + + #[tokio::test] + async fn rejects_ipv6_literal_ula() { + let url = reqwest::Url::parse("http://[fc00::1]/").unwrap(); + let err = validate_fetch_target(&url, &ctx()) + .await + .expect_err("[fc00::1] must be rejected as restricted"); + assert!(format!("{err}").contains("restricted")); + } + + #[tokio::test] + async fn rejects_ipv6_literal_link_local() { + let url = reqwest::Url::parse("http://[fe80::1]/").unwrap(); + let err = validate_fetch_target(&url, &ctx()) + .await + .expect_err("[fe80::1] must be rejected as restricted"); + assert!(format!("{err}").contains("restricted")); + } + + #[tokio::test] + async fn rejects_ipv6_literal_ipv4_mapped_loopback() { + let url = reqwest::Url::parse("http://[::ffff:127.0.0.1]/").unwrap(); + let err = validate_fetch_target(&url, &ctx()) + .await + .expect_err("[::ffff:127.0.0.1] must be rejected as restricted"); + assert!(format!("{err}").contains("restricted")); + } + + #[tokio::test] + async fn rejects_ipv6_literal_unspecified() { + let url = reqwest::Url::parse("http://[::]/").unwrap(); + let err = validate_fetch_target(&url, &ctx()) + .await + .expect_err("[::] must be rejected as restricted"); + assert!(format!("{err}").contains("restricted")); + } + #[tokio::test] async fn redirected_host_respects_network_policy() { use crate::network_policy::{Decision, NetworkPolicy, NetworkPolicyDecider}; diff --git a/npm/deepseek-tui/package.json b/npm/deepseek-tui/package.json index e2a1f013..8dd1d35c 100644 --- a/npm/deepseek-tui/package.json +++ b/npm/deepseek-tui/package.json @@ -1,7 +1,7 @@ { "name": "deepseek-tui", - "version": "0.8.25", - "deepseekBinaryVersion": "0.8.25", + "version": "0.8.26", + "deepseekBinaryVersion": "0.8.26", "description": "Install and run deepseek and deepseek-tui binaries from GitHub release artifacts.", "author": "Hmbown", "license": "MIT",