fix: Phase 3 — command injection, unwrap/expect panics, unsigned image acceptance

- VPN key gen: replaced sh -c with format string (command injection) with
  safe stdin piping to wg pubkey
- Secrets manager: replaced .unwrap() on path.parent() with proper error
- Tor proxy: replaced .expect("valid proxy") with continue on error
- Image verifier: added require_signatures flag, strict mode rejects
  unsigned images and missing cosign binary

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-18 00:45:15 +00:00
co-authored by Claude Opus 4.6
parent 430d174389
commit 7bbb9cc5cd
5 changed files with 51 additions and 28 deletions
+5 -1
View File
@@ -522,7 +522,11 @@ async fn notify_federation_peers_address_change(
});
let url = format!("http://{}/rpc/v1", &peer.onion);
let client = match reqwest::Client::builder()
.proxy(reqwest::Proxy::all(format!("socks5h://{}", proxy)).unwrap_or_else(|_| reqwest::Proxy::all("socks5h://127.0.0.1:9050").expect("valid proxy")))
.proxy(match reqwest::Proxy::all(format!("socks5h://{}", proxy))
.or_else(|_| reqwest::Proxy::all("socks5h://127.0.0.1:9050")) {
Ok(p) => p,
Err(_) => continue,
})
.timeout(std::time::Duration::from_secs(30))
.build()
{
+15 -16
View File
@@ -121,30 +121,29 @@ pub async fn generate_wireguard_keypair() -> Result<(String, String)> {
.trim()
.to_string();
let _pubkey_output = tokio::process::Command::new("wg")
let mut child = tokio::process::Command::new("wg")
.arg("pubkey")
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.spawn()
.context("Failed to spawn wg pubkey")?;
// Use echo + pipe approach instead
let pubkey_output = tokio::process::Command::new("sh")
.arg("-c")
.arg(format!("echo '{}' | wg pubkey", private_key))
.output()
.await
.context("Failed to derive public key")?;
if !pubkey_output.status.success() {
anyhow::bail!(
"wg pubkey failed: {}",
String::from_utf8_lossy(&pubkey_output.stderr)
);
if let Some(mut stdin) = child.stdin.take() {
use tokio::io::AsyncWriteExt;
stdin.write_all(private_key.as_bytes()).await
.context("Failed to write private key to wg stdin")?;
}
let public_key = String::from_utf8(pubkey_output.stdout)
.context("Invalid UTF-8 from wg pubkey")?
let output = child.wait_with_output().await
.context("wg pubkey process failed")?;
if !output.status.success() {
anyhow::bail!("wg pubkey failed: {}", String::from_utf8_lossy(&output.stderr));
}
let public_key = String::from_utf8(output.stdout)
.context("wg pubkey output is not valid UTF-8")?
.trim()
.to_string();
+23 -5
View File
@@ -6,28 +6,46 @@ use std::process::Command;
use tracing::{info, warn};
pub struct ImageVerifier {
cosign_public_key: Option<String>, // Public key for verification
cosign_public_key: Option<String>,
require_signatures: bool,
}
impl ImageVerifier {
pub fn new(cosign_public_key: Option<String>) -> Self {
Self { cosign_public_key }
Self { cosign_public_key, require_signatures: false }
}
/// Create a verifier that requires all images to be signed.
pub fn new_strict(cosign_public_key: Option<String>) -> Self {
Self { cosign_public_key, require_signatures: true }
}
/// Verify a container image signature
pub async fn verify_image(&self, image: &str, signature: Option<&str>) -> Result<bool> {
if signature.is_none() && self.cosign_public_key.is_none() {
if self.require_signatures {
return Err(anyhow::anyhow!(
"Image '{}' has no signature and no cosign key is configured. \
All container images must be signed for production use.",
image
));
}
warn!("No signature provided for image: {}", image);
return Ok(false);
}
// Check if cosign is available
let cosign_available = Command::new("cosign")
.arg("version")
.output()
.is_ok();
if !cosign_available {
if self.require_signatures {
return Err(anyhow::anyhow!(
"Cosign binary not found. Install cosign to verify container image signatures."
));
}
warn!("Cosign not available, skipping signature verification");
return Ok(false);
}
+3 -1
View File
@@ -109,7 +109,9 @@ impl SecretsManager {
.join(app_id)
.join(format!("{}.secret", secret_id));
fs::create_dir_all(secret_path.parent().unwrap()).await?;
let parent = secret_path.parent()
.ok_or_else(|| anyhow::anyhow!("Invalid secret path: no parent directory for {:?}", secret_path))?;
fs::create_dir_all(parent).await?;
let encrypted = self
.encrypt(value.as_bytes())