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:
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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()));
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user