chore(release): prepare v0.8.26 — security hotfix

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) <noreply@anthropic.com>
This commit is contained in:
Hunter Bown
2026-05-09 22:30:16 -05:00
parent 506343f44e
commit b92d3569fd
16 changed files with 169 additions and 52 deletions
+15
View File
@@ -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
Generated
+14 -14
View File
@@ -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"
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+9 -9
View File
@@ -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
+7 -7
View File
@@ -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
+1 -1
View File
@@ -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
+8 -8
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+2 -2
View File
@@ -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"
+14 -2
View File
@@ -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(
+38 -1
View File
@@ -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()));
+54 -1
View File
@@ -318,7 +318,14 @@ async fn validate_fetch_target(
"requests to localhost are not allowed",
));
}
if let Ok(ip) = host.parse::<std::net::IpAddr>() {
// 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::<std::net::IpAddr>() {
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};
+2 -2
View File
@@ -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",