security+feat: v1.3.0 — pentest remediation, container reliability, UI overhaul
Security (33 pentest findings addressed): - CRITICAL: backend binds 127.0.0.1, path traversal in tor.rs/dwn fixed - HIGH: federation requires signatures, XSS login redirect, RBAC viewer restricted - HIGH: tar slip prevention, S3 SSRF validation, backup ID validation - MEDIUM: remember-me random secret, TOTP session rotation, password re-auth - LOW: CSP unsafe-inline removed, CORS dev-only, onion/webhook validation Container reliability: - Memory limits on all 37 containers (OOM prevention) - Exited vs stopped state distinction with health-aware status badges - Crash recovery coordination (no more restart cascade) - User-stopped tracking survives reboots - Tiered boot recovery (databases → core → services → apps) UI: - Wallet TransactionsModal, health-aware app status badges - Restart button on containers, exited/crashed red state - Mesh view overhaul, glass button updates, BaseModal/ToggleSwitch - Apps sticky header removed, dev faucet, mutable mock wallet Infrastructure: - LND REST port 8080 exposed over Tor (LND Connect fix) - Nginx cookie_session fix, deploy script Tor config updated - Dev environment: podman auto-start, boot mode simulation Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
28763c4f09
commit
84a56c80de
@@ -121,8 +121,24 @@ impl DwnStore {
|
||||
Ok(message)
|
||||
}
|
||||
|
||||
/// Validate a record ID to prevent path traversal.
|
||||
fn validate_record_id(record_id: &str) -> Result<()> {
|
||||
if record_id.is_empty()
|
||||
|| record_id.len() > 128
|
||||
|| !record_id
|
||||
.chars()
|
||||
.all(|c| c.is_alphanumeric() || c == '-' || c == '_')
|
||||
{
|
||||
return Err(anyhow::anyhow!(
|
||||
"Invalid record ID (alphanumeric, hyphens, underscores only)"
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Read a message by record ID.
|
||||
pub async fn read_message(&self, record_id: &str) -> Result<Option<DwnMessage>> {
|
||||
Self::validate_record_id(record_id)?;
|
||||
let path = self.messages_dir.join(format!("{}.json", record_id));
|
||||
if !path.exists() {
|
||||
return Ok(None);
|
||||
@@ -137,6 +153,7 @@ impl DwnStore {
|
||||
|
||||
/// Delete a message by record ID.
|
||||
pub async fn delete_message(&self, record_id: &str) -> Result<bool> {
|
||||
Self::validate_record_id(record_id)?;
|
||||
let path = self.messages_dir.join(format!("{}.json", record_id));
|
||||
if !path.exists() {
|
||||
return Ok(false);
|
||||
|
||||
@@ -322,11 +322,37 @@ pub async fn save_router_config(data_dir: &Path, config: &RouterConfig) -> Resul
|
||||
fs::write(&path, data).await.context("Writing router config")
|
||||
}
|
||||
|
||||
/// Validate that an IP string is a private/LAN address (not public, not localhost).
|
||||
fn is_valid_private_ip(ip_str: &str) -> bool {
|
||||
let ip: std::net::IpAddr = match ip_str.parse() {
|
||||
Ok(ip) => ip,
|
||||
Err(_) => return false, // Reject hostnames
|
||||
};
|
||||
match ip {
|
||||
std::net::IpAddr::V4(v4) => {
|
||||
// Allow only RFC1918 private ranges, reject localhost and public
|
||||
let octets = v4.octets();
|
||||
let is_10 = octets[0] == 10;
|
||||
let is_172_private = octets[0] == 172 && (16..=31).contains(&octets[1]);
|
||||
let is_192_168 = octets[0] == 192 && octets[1] == 168;
|
||||
is_10 || is_172_private || is_192_168
|
||||
}
|
||||
std::net::IpAddr::V6(_) => false, // Reject IPv6 for gateway detection
|
||||
}
|
||||
}
|
||||
|
||||
/// Detect router type by probing common endpoints on the gateway.
|
||||
pub async fn detect_router_type(gateway_ip: &str) -> RouterType {
|
||||
// Validate that gateway is a private IP — prevent SSRF to arbitrary hosts
|
||||
if !is_valid_private_ip(gateway_ip) {
|
||||
tracing::warn!(gateway = gateway_ip, "Rejected non-private gateway IP");
|
||||
return RouterType::Unknown;
|
||||
}
|
||||
|
||||
let client = reqwest::Client::builder()
|
||||
.timeout(std::time::Duration::from_secs(5))
|
||||
.danger_accept_invalid_certs(true)
|
||||
.redirect(reqwest::redirect::Policy::none())
|
||||
.build()
|
||||
.unwrap_or_default();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user