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:
Dorian
2026-03-19 12:44:31 +00:00
co-authored by Claude Opus 4.6
parent d1b48388fb
commit 1a74a930f7
77 changed files with 2485 additions and 966 deletions
+24 -7
View File
@@ -4,6 +4,16 @@ use crate::network::dwn_store::DwnStore;
use anyhow::{Context, Result};
use tracing::debug;
/// Validate a v3 Tor onion address.
/// Must be exactly 62 chars: 56 base32 characters (a-z, 2-7) followed by ".onion".
fn is_valid_v3_onion(addr: &str) -> bool {
if addr.len() != 62 || !addr.ends_with(".onion") {
return false;
}
let prefix = &addr[..56];
prefix.chars().all(|c| c.is_ascii_lowercase() || ('2'..='7').contains(&c))
}
const FILE_CATALOG_PROTOCOL: &str = "https://archipelago.dev/protocols/file-catalog/v1";
impl RpcHandler {
@@ -25,10 +35,16 @@ impl RpcHandler {
.get("filename")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("Missing filename"))?;
// Prevent path traversal
if filename.contains("..") || filename.contains('\0') {
// Validate filename: prevent path traversal, hidden files, and excessive length
if filename.contains("..") || filename.contains('\0') || filename.contains('/') || filename.contains('\\') {
anyhow::bail!("Invalid filename: path traversal not allowed");
}
if filename.starts_with('.') {
anyhow::bail!("Invalid filename: hidden files not allowed");
}
if filename.is_empty() || filename.len() > 255 {
anyhow::bail!("Invalid filename: must be 1-255 characters");
}
let mime_type = params
.get("mime_type")
.and_then(|v| v.as_str())
@@ -191,8 +207,9 @@ impl RpcHandler {
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("Missing content_id"))?;
if !onion.ends_with(".onion") || onion.len() < 10 {
return Err(anyhow::anyhow!("Invalid onion address"));
// Validate v3 onion address: 56 base32 chars + ".onion" = 62 chars total
if !is_valid_v3_onion(onion) {
return Err(anyhow::anyhow!("Invalid v3 onion address"));
}
let socks_proxy = reqwest::Proxy::all("socks5h://127.0.0.1:9050")
@@ -252,9 +269,9 @@ impl RpcHandler {
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("Missing onion address"))?;
// Validate onion address format
if !onion.ends_with(".onion") || onion.len() < 10 {
return Err(anyhow::anyhow!("Invalid onion address"));
// Validate v3 onion address: 56 base32 chars + ".onion" = 62 chars total
if !is_valid_v3_onion(onion) {
return Err(anyhow::anyhow!("Invalid v3 onion address"));
}
// Connect via Tor SOCKS proxy to the peer's content catalog endpoint