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
@@ -1,4 +1,5 @@
|
||||
use hmac::{Hmac, Mac};
|
||||
use rand::RngCore;
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::collections::HashMap;
|
||||
use std::net::IpAddr;
|
||||
@@ -234,16 +235,31 @@ impl SessionStore {
|
||||
None
|
||||
}
|
||||
|
||||
/// Upgrade a pending session to a full session.
|
||||
pub async fn upgrade_to_full(&self, token: &str) {
|
||||
let hash = hash_token(token);
|
||||
/// Upgrade a pending session to a full session with token rotation.
|
||||
/// Deletes the old pending session and creates a new full session with a fresh token.
|
||||
/// Returns the new plaintext token so the caller can set it as the new cookie.
|
||||
pub async fn upgrade_to_full(&self, token: &str) -> Option<String> {
|
||||
let old_hash = hash_token(token);
|
||||
let mut sessions = self.sessions.write().await;
|
||||
if let Some(session) = sessions.get_mut(&hash) {
|
||||
session.session_type = SessionType::Full;
|
||||
// Only upgrade if the old session exists and is pending
|
||||
if sessions.remove(&old_hash).is_some() {
|
||||
let new_token_bytes: [u8; 32] = rand::random();
|
||||
let new_token = hex::encode(new_token_bytes);
|
||||
let new_hash = hash_token(&new_token);
|
||||
let now = SystemTime::now();
|
||||
session.created_at = now;
|
||||
session.last_activity = now;
|
||||
self.evict_if_over_limit(&mut sessions);
|
||||
sessions.insert(
|
||||
new_hash,
|
||||
Session {
|
||||
created_at: now,
|
||||
last_activity: now,
|
||||
session_type: SessionType::Full,
|
||||
},
|
||||
);
|
||||
Self::save_to_disk_sync(&sessions, &self.persist_path);
|
||||
Some(new_token)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
@@ -393,25 +409,21 @@ impl SessionStore {
|
||||
}
|
||||
|
||||
pub fn load_or_create_remember_secret() -> Vec<u8> {
|
||||
// Try existing secret file first (backwards compatibility)
|
||||
// Try existing secret file first
|
||||
if let Ok(secret) = std::fs::read(REMEMBER_SECRET_FILE) {
|
||||
if secret.len() == 32 {
|
||||
return secret;
|
||||
}
|
||||
}
|
||||
// Derive a deterministic secret from machine-id so it survives restarts
|
||||
// without storing plaintext key material
|
||||
let machine_id = std::fs::read_to_string("/etc/machine-id")
|
||||
.unwrap_or_else(|_| uuid::Uuid::new_v4().to_string());
|
||||
let salt = b"archipelago-remember-me-v1";
|
||||
let mut hasher = sha2::Sha256::new();
|
||||
use sha2::Digest;
|
||||
hasher.update(machine_id.trim().as_bytes());
|
||||
hasher.update(salt);
|
||||
let secret = hasher.finalize();
|
||||
let secret_vec = secret.to_vec();
|
||||
let _ = std::fs::write(REMEMBER_SECRET_FILE, &secret_vec);
|
||||
secret_vec
|
||||
// Generate a cryptographically random 32-byte secret on first boot
|
||||
let mut secret = [0u8; 32];
|
||||
rand::rngs::OsRng.fill_bytes(&mut secret);
|
||||
// Ensure parent directory exists
|
||||
if let Some(parent) = std::path::Path::new(REMEMBER_SECRET_FILE).parent() {
|
||||
let _ = std::fs::create_dir_all(parent);
|
||||
}
|
||||
let _ = std::fs::write(REMEMBER_SECRET_FILE, &secret);
|
||||
secret.to_vec()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -605,9 +617,15 @@ mod tests {
|
||||
let got = store.get_pending_secret(&token).await;
|
||||
assert_eq!(got, Some(secret));
|
||||
|
||||
// Upgrade to full
|
||||
store.upgrade_to_full(&token).await;
|
||||
assert!(store.validate(&token).await);
|
||||
// Upgrade to full — returns a new rotated token
|
||||
let new_token = store.upgrade_to_full(&token).await;
|
||||
assert!(new_token.is_some());
|
||||
let new_token = new_token.unwrap();
|
||||
|
||||
// Old token should be invalid (rotated)
|
||||
assert!(!store.validate(&token).await);
|
||||
// New token should be valid
|
||||
assert!(store.validate(&new_token).await);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
Reference in New Issue
Block a user