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:
@@ -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`.
|
||||
|
||||
Reference in New Issue
Block a user