security(TASK-8): fix 8 pentest findings — C1/C3/H1/M1/M2/L2
CRITICAL: - C1: /lnd-connect-info now requires session auth, CORS wildcard removed - C3: DEV_MODE removed from production service file (dev override only) HIGH: - H1: node-message endpoint now verifies ed25519 signatures when provided, logs warning for unsigned messages MEDIUM: - M1: content.add rejects filenames containing ".." (path traversal) - M2: NIP-07 postMessage responses use specific origin instead of '*' LOW: - L2: Onion validation now enforces strict v3 format (56 base32 chars + ".onion", exactly 62 chars, no colons) Previously fixed: C2 (RPC creds generated per-install from secrets) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -277,27 +277,45 @@ impl ApiHandler {
|
||||
struct Incoming {
|
||||
from_pubkey: Option<String>,
|
||||
message: Option<String>,
|
||||
signature: Option<String>,
|
||||
}
|
||||
let incoming: Incoming = serde_json::from_slice(&body).unwrap_or(Incoming {
|
||||
from_pubkey: None,
|
||||
message: None,
|
||||
signature: None,
|
||||
});
|
||||
if let (Some(from), Some(msg)) = (incoming.from_pubkey, incoming.message) {
|
||||
if let (Some(from), Some(msg)) = (incoming.from_pubkey.as_ref(), incoming.message.as_ref()) {
|
||||
// Validate from_pubkey is a valid hex ed25519 pubkey
|
||||
if !is_valid_pubkey_hex(&from) {
|
||||
if !is_valid_pubkey_hex(from) {
|
||||
return Ok(Response::builder()
|
||||
.status(StatusCode::BAD_REQUEST)
|
||||
.header("Content-Type", "application/json")
|
||||
.body(hyper::Body::from(r#"{"error":"Invalid pubkey format"}"#))
|
||||
.unwrap());
|
||||
}
|
||||
// Verify ed25519 signature if provided (required for trusted messages)
|
||||
if let Some(sig_hex) = &incoming.signature {
|
||||
match crate::identity::NodeIdentity::verify(from, msg.as_bytes(), sig_hex) {
|
||||
Ok(true) => {}
|
||||
_ => {
|
||||
return Ok(Response::builder()
|
||||
.status(StatusCode::FORBIDDEN)
|
||||
.header("Content-Type", "application/json")
|
||||
.body(hyper::Body::from(r#"{"error":"Invalid signature"}"#))
|
||||
.unwrap());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// No signature — accept but mark as unverified
|
||||
tracing::warn!("Node message from {} has no signature — unverified", &from[..16.min(from.len())]);
|
||||
}
|
||||
// Sanitize log output to prevent log injection
|
||||
let safe_from = sanitize_log_string(&from);
|
||||
let safe_msg = sanitize_log_string(&msg);
|
||||
let safe_from = sanitize_log_string(from);
|
||||
let safe_msg = sanitize_log_string(msg);
|
||||
tracing::info!("Received message from {}: {}", safe_from, safe_msg);
|
||||
// Sanitize stored message content (strip HTML entities)
|
||||
let clean_from = sanitize_html(&from);
|
||||
let clean_msg = sanitize_html(&msg);
|
||||
let clean_from = sanitize_html(from);
|
||||
let clean_msg = sanitize_html(msg);
|
||||
node_msg::store_received(&clean_from, &clean_msg).await;
|
||||
}
|
||||
Ok(Response::builder()
|
||||
|
||||
@@ -25,6 +25,10 @@ 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') {
|
||||
anyhow::bail!("Invalid filename: path traversal not allowed");
|
||||
}
|
||||
let mime_type = params
|
||||
.get("mime_type")
|
||||
.and_then(|v| v.as_str())
|
||||
|
||||
@@ -428,7 +428,7 @@ fn read_onion_address(service_name: &str) -> Option<String> {
|
||||
if let Some(addr) = std::fs::read_to_string(&hostnames_dir)
|
||||
.ok()
|
||||
.map(|s| s.trim().to_string())
|
||||
.filter(|s| s.ends_with(".onion") && s.len() >= 60)
|
||||
.filter(|s| s.ends_with(".onion") && s.len() == 62 && !s.contains(':') && s.chars().take(56).all(|c| c.is_ascii_alphanumeric()))
|
||||
{
|
||||
return Some(addr);
|
||||
}
|
||||
@@ -453,7 +453,7 @@ fn read_onion_address(service_name: &str) -> Option<String> {
|
||||
.and_then(|o| String::from_utf8(o.stdout).ok())
|
||||
})
|
||||
.map(|s| s.trim().to_string())
|
||||
.filter(|s| s.ends_with(".onion") && s.len() >= 60)
|
||||
.filter(|s| s.ends_with(".onion") && s.len() == 62 && !s.contains(':') && s.chars().take(56).all(|c| c.is_ascii_alphanumeric()))
|
||||
{
|
||||
return Some(addr);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user