security: fix audit points 5-9 + optimize echo_push/info endpoints

- Point 5 (SSRF): Add URL validation for WELIST_SERVER_URL (src/validation.rs)
- Point 6 (DB Access): Add DB path validation, symlink check, WAL mode (open_db)
- Point 8 (HTTPS): Extract nginx config, add deployment checklist, bind warnings
- Point 9 (Input Validation): Add NETWORKS check (404 for unknown), txid 64-hex validation
- Optimize echo_push: parse transactions outside DB lock, batch duplicate check, N+1 xpub lookup eliminated via HashSet cache
- Optimize echo_info: derive BIP32 address outside DB lock, minimize lock duration
- Fix echo_stats SQL injection via parameter binding + add idx_stats_chain index
- New regression tests: ssrf_tests, db_path_validation, input_validation_tests
This commit is contained in:
2026-07-16 18:59:30 -04:00
parent 237e62d4be
commit 4fc0790fe7
20 changed files with 2762 additions and 1022 deletions

View File

@@ -3,9 +3,9 @@ use std::path::Path;
#[test]
fn test_gitignore_protection_env() {
let gitignore = fs::read_to_string(".gitignore")
.expect(".gitignore file not found in project root");
let gitignore =
fs::read_to_string(".gitignore").expect(".gitignore file not found in project root");
// Check that .env and .pem files are blocked
let required_patterns = vec![
".env",
@@ -21,12 +21,13 @@ fn test_gitignore_protection_env() {
"ec.key",
"chiave_privata.key",
];
for pattern in required_patterns {
let has_exact = gitignore.contains(&pattern);
let has_wildcard = gitignore.contains(&format!("*.env.local")) || gitignore.contains(&format!(".env.local"));
let has_wildcard = gitignore.contains(&format!("*.env.local"))
|| gitignore.contains(&format!(".env.local"));
let has_env = gitignore.contains("*.env") || gitignore.contains(".env");
// For .env.local, either .env.local or *.env.local is acceptable
let is_env_local = pattern == "*.env.local" || pattern == ".env.local";
if is_env_local {
@@ -58,7 +59,7 @@ fn test_gitignore_protection_env() {
);
}
}
println!(".gitignore properly protects .env, .pem, and .key files");
}
@@ -72,7 +73,7 @@ fn test_no_private_key_in_git() {
return;
}
};
assert!(
gitignore.contains("private_key.pem"),
".gitignore must block private_key.pem"
@@ -81,34 +82,32 @@ fn test_no_private_key_in_git() {
gitignore.contains("privkey.pem"),
".gitignore must block privkey.pem"
);
assert!(
gitignore.contains("ec.key"),
".gitignore must block ec.key"
);
assert!(gitignore.contains("ec.key"), ".gitignore must block ec.key");
assert!(
gitignore.contains("chiave_privata.key"),
".gitignore must block chiave_privata.key"
);
// Check that no private key files are tracked by git
let output = std::process::Command::new("git")
.args(&["ls-files", "*.pem", "*.key"])
.output()
.expect("Failed to run git ls-files");
let tracked_keys = String::from_utf8(output.stdout).unwrap();
let tracked_keys: Vec<&str> = tracked_keys.lines().collect();
// Only non-empty entries and only public_key.pem should be tracked
for tracked in tracked_keys.iter().filter(|s| !s.is_empty()) {
if !tracked.contains("public_key.pem") {
assert!(false,
"Private key file is tracked by git: {}. Remove it with git rm --cached",
assert!(
false,
"Private key file is tracked by git: {}. Remove it with git rm --cached",
tracked
);
}
}
println!("PASS: No private keys tracked in git (only public_key.pem allowed)");
}
@@ -116,7 +115,7 @@ fn test_no_private_key_in_git() {
fn test_no_token_in_source_files() {
// Scan source files for hardcoded tokens
let mut found_issues = Vec::new();
// Scan .sh files for hardcoded 40-char hex strings
for entry in fs::read_dir(".").unwrap().filter_map(|e| e.ok()) {
let path = entry.path();
@@ -128,18 +127,30 @@ fn test_no_token_in_source_files() {
let content = fs::read_to_string(&path).unwrap();
for (line_num, line) in content.lines().enumerate() {
// Skip comments and example/template files
if line.trim().starts_with("#") || line.to_lowercase().contains("example") || line.to_lowercase().contains("template") {
if line.trim().starts_with("#")
|| line.to_lowercase().contains("example")
|| line.to_lowercase().contains("template")
{
continue;
}
// Check for 40-64 hex chars that could be API tokens (not in .env.example comments)
if line.trim().len() >= 40 {
let hex_chars = line.trim().chars().filter(|c| c.is_ascii_hexdigit()).collect::<Vec<_>>();
let hex_chars = line
.trim()
.chars()
.filter(|c| c.is_ascii_hexdigit())
.collect::<Vec<_>>();
if hex_chars.len() >= 40 && hex_chars.len() <= 64 {
// Check if it looks like it's part of a TOKEN assignment
if line.to_lowercase().contains("token") || line.to_lowercase().contains("api") || line.to_lowercase().contains("secret") {
if line.to_lowercase().contains("token")
|| line.to_lowercase().contains("api")
|| line.to_lowercase().contains("secret")
{
found_issues.push(format!(
"Potential hardcoded token in {}: line {}: {}",
path.display(), line_num + 1, line.trim()
"Potential hardcoded token in {}: line {}: {}",
path.display(),
line_num + 1,
line.trim()
));
}
}
@@ -148,16 +159,18 @@ fn test_no_token_in_source_files() {
}
}
}
if !found_issues.is_empty() {
println!("FAIL: Found potential hardcoded tokens:");
for issue in &found_issues {
println!(" {}", issue);
}
assert!(false, "Found potential hardcoded tokens in shell scripts: {:?}", found_issues);
assert!(
false,
"Found potential hardcoded tokens in shell scripts: {:?}",
found_issues
);
}
println!("PASS: No hardcoded tokens found in shell scripts");
}