fix: harden input validation across all RPC endpoints (PENTEST-02)

Manual security audit of 130+ RPC endpoints. Critical fixes:
- LND: validate pubkey (66-char hex), Bitcoin addresses, channel points,
  amount bounds, payment request format, memo length, peer address
- Package: validate_app_id on start/stop/restart/bundled-app handlers,
  validate volume host paths (must be under /var/lib/archipelago/),
  validate Docker image in bundled-app-start
- Container: validate_app_id on all 6 handlers, canonicalize manifest paths
- Network: path traversal prevention in connection request deletion
- Backup: backup ID validation in delete handler
- Webhooks: URL scheme validation, SSRF prevention for private IPs
- Security: validate app_id in secret rotation
- Interfaces: WiFi password length/null validation, strict IP/gateway/DNS
  parsing for static ethernet config

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-11 14:32:49 +00:00
parent daa33d098b
commit 89acc3ed5c
9 changed files with 774 additions and 33 deletions

View File

@@ -1,4 +1,5 @@
use super::RpcHandler;
use crate::network::dns;
use anyhow::{Context, Result};
use tracing::debug;
@@ -36,6 +37,10 @@ impl RpcHandler {
if ssid.len() > 64 || ssid.contains('\0') {
anyhow::bail!("Invalid SSID");
}
// Validate WiFi password
if password.len() > 63 || password.contains('\0') {
anyhow::bail!("Invalid WiFi password (max 63 chars, no null bytes)");
}
tracing::info!("Connecting to WiFi network: {}", ssid);
connect_wifi(ssid, password).await?;
@@ -85,11 +90,22 @@ impl RpcHandler {
.and_then(|v| v.as_str())
.unwrap_or("1.1.1.1");
// Basic IP format validation
if ip.parse::<std::net::IpAddr>().is_err() && !ip.contains('/') {
// Validate IP: must parse as IP or CIDR
let ip_part = ip.split('/').next().unwrap_or("");
if ip_part.parse::<std::net::IpAddr>().is_err() {
anyhow::bail!("Invalid IP address format");
}
// Validate gateway if provided
if !gateway.is_empty() && gateway.parse::<std::net::IpAddr>().is_err() {
anyhow::bail!("Invalid gateway IP address");
}
// Validate DNS server IP
if dns.parse::<std::net::IpAddr>().is_err() {
anyhow::bail!("Invalid DNS server IP address");
}
tracing::info!("Setting {} to static IP {}", interface, ip);
configure_ethernet_static(interface, ip, gateway, dns).await?;
}
@@ -98,6 +114,71 @@ impl RpcHandler {
Ok(serde_json::json!({ "ok": true, "interface": interface, "mode": mode }))
}
/// network.dns-status — get current DNS configuration and status.
pub(super) async fn handle_network_dns_status(&self) -> Result<serde_json::Value> {
debug!("Getting DNS status");
let status = dns::get_status(&self.config.data_dir).await?;
Ok(serde_json::to_value(status)?)
}
/// network.configure-dns — configure DNS servers and provider.
pub(super) async fn handle_network_configure_dns(
&self,
params: Option<serde_json::Value>,
) -> Result<serde_json::Value> {
let params = params.ok_or_else(|| anyhow::anyhow!("Missing params"))?;
let provider_str = params
.get("provider")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("Missing required parameter: provider"))?;
let provider = match provider_str {
"system" => dns::DnsProvider::System,
"cloudflare" => dns::DnsProvider::Cloudflare,
"google" => dns::DnsProvider::Google,
"quad9" => dns::DnsProvider::Quad9,
"mullvad" => dns::DnsProvider::Mullvad,
"custom" => dns::DnsProvider::Custom,
other => anyhow::bail!("Unknown DNS provider: {}. Use: system, cloudflare, google, quad9, mullvad, custom", other),
};
let custom_servers: Vec<String> = if provider == dns::DnsProvider::Custom {
params
.get("servers")
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str().map(String::from))
.collect()
})
.unwrap_or_default()
} else {
Vec::new()
};
if provider == dns::DnsProvider::Custom && custom_servers.is_empty() {
anyhow::bail!("Custom provider requires at least one DNS server in 'servers' array");
}
// Validate custom server IPs
for s in &custom_servers {
if s.parse::<std::net::IpAddr>().is_err() {
anyhow::bail!("Invalid DNS server IP: {}", s);
}
}
tracing::info!(provider = provider_str, "Configuring DNS");
let config = dns::configure(&self.config.data_dir, provider, custom_servers).await?;
Ok(serde_json::json!({
"ok": true,
"provider": config.provider.to_string(),
"servers": config.servers,
"doh_enabled": config.doh_enabled,
"doh_url": config.doh_url,
}))
}
}
/// List network interfaces using `ip -j addr show`.