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
co-authored by Claude Opus 4.6
parent 4b5eb4ed29
commit aa6c7c2e6a
9 changed files with 774 additions and 33 deletions
+32
View File
@@ -28,6 +28,38 @@ impl RpcHandler {
config.enabled = enabled;
}
if let Some(url) = params.get("url").and_then(|v| v.as_str()) {
// Validate webhook URL scheme and reject obviously dangerous targets
if !url.is_empty() {
if !url.starts_with("https://") && !url.starts_with("http://") {
anyhow::bail!("Webhook URL must use HTTP(S)");
}
if !self.config.dev_mode && !url.starts_with("https://") {
anyhow::bail!("Webhook URL must use HTTPS in production");
}
// Extract host portion and reject private/internal addresses
let host_part = url
.trim_start_matches("https://")
.trim_start_matches("http://")
.split('/')
.next()
.unwrap_or("")
.split(':')
.next()
.unwrap_or("");
let is_private = host_part == "localhost"
|| host_part == "127.0.0.1"
|| host_part == "::1"
|| host_part.starts_with("10.")
|| host_part.starts_with("172.")
|| host_part.starts_with("192.168.")
|| host_part.starts_with("169.254.");
if is_private && !self.config.dev_mode {
anyhow::bail!("Webhook URL must not point to private/local addresses");
}
if url.len() > 2048 {
anyhow::bail!("Webhook URL too long");
}
}
config.url = url.to_string();
}
if let Some(secret) = params.get("secret").and_then(|v| v.as_str()) {