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

@@ -20,6 +20,8 @@ use std::str;
use std::{thread, time::Duration};
use zmq::{Context, DEALER, DONTWAIT, Socket};
use bal_server::db::open_db;
use bal_server::validation::is_valid_welist_url;
use base64::{Engine as _, engine::general_purpose};
use openssl::hash::MessageDigest;
use openssl::pkey::PKey;
@@ -74,7 +76,7 @@ struct NetworkParams {
cookie_file: String,
rpc_user: String,
rpc_pass: String,
zmq_listener:String
zmq_listener: String,
}
fn get_network_params(cfg: &MyConfig, network: Network) -> &NetworkParams {
match network {
@@ -235,7 +237,13 @@ async fn main_result(cfg: &MyConfig, network_params: &NetworkParams) -> Result<(
debug!("best block hash: {}", bcinfo.best_block_hash);
let average_time = bcinfo.median_time;
let db = sqlite::open(&cfg.db_file).unwrap();
let db = match open_db(&cfg.db_file) {
Ok(c) => c,
Err(e) => {
error!("Fatal: {}", e);
std::process::exit(1);
}
};
info!("db open {}", &cfg.db_file);
let sqlquery = "SELECT * FROM tbl_tx WHERE network = :network AND status = :status AND ( locktime < :bestblock_height OR locktime > :locktime_threshold AND locktime < :bestblock_time);";
@@ -321,7 +329,11 @@ async fn main_result(cfg: &MyConfig, network_params: &NetworkParams) -> Result<(
}
async fn calculate_stats(db: &Connection, chain: String) -> Result<(), reqwest::Error> {
// Validate chain to prevent SQL injection via environment variable tampering
if !chain.chars().all(|c| c.is_alphanumeric() || c == '-' || c == '_') || chain.is_empty() {
if !chain
.chars()
.all(|c| c.is_alphanumeric() || c == '-' || c == '_')
|| chain.is_empty()
{
error!("Invalid chain name: {chain}");
return Ok(());
}
@@ -403,7 +415,13 @@ async fn send_stats_report(
debug!("sending report to welist");
let welist_url = env::var("WELIST_SERVER_URL")
.unwrap_or("https://welist.bitcoin-after.life".to_string());
if !is_valid_welist_url(&welist_url) {
warn!(
"Invalid or unsafe WELIST_SERVER_URL: {}. Skipping stats report.",
welist_url
);
return Ok(());
}
let client = rClient::new();
let url = format!("{}/ping", welist_url);
debug!("welist url: {}", url);
@@ -480,14 +498,15 @@ fn parse_env_netconfig(cfg_lock: &mut MyConfig, chain: &str) -> NetworkParams {
}
match env::var(format!("BAL_PUSHER_{}_PORT", chain.to_uppercase())) {
Ok(value) => match value.parse::<u64>() {
Ok(value) => {
match u16::try_from(value) {
Ok(port) => cfg.port = port,
Err(e) => {
error!("Port value {} exceeds u16 range for chain {}: {}", value, chain, e);
}
Ok(value) => match u16::try_from(value) {
Ok(port) => cfg.port = port,
Err(e) => {
error!(
"Port value {} exceeds u16 range for chain {}: {}",
value, chain, e
);
}
}
},
Err(_) => {}
},
Err(_) => {}
@@ -522,10 +541,13 @@ fn parse_env_netconfig(cfg_lock: &mut MyConfig, chain: &str) -> NetworkParams {
}
Err(_) => {}
}
println!("{}",format!("BAL_PUSHER_{}_ZMQ_HASHBLOCK", chain.to_uppercase()));
println!(
"{}",
format!("BAL_PUSHER_{}_ZMQ_HASHBLOCK", chain.to_uppercase())
);
match env::var(format!("BAL_PUSHER_{}_ZMQ_HASHBLOCK", chain.to_uppercase())) {
Ok(value) => {
println!("value:{}",value);
println!("value:{}", value);
cfg.zmq_listener = value;
}
Err(_) => {}
@@ -640,7 +662,7 @@ async fn main() -> std::io::Result<()> {
}
match socket.set_subscribe(b"") {
Ok(_) => {},
Ok(_) => {}
Err(e) => {
error!("ZMQ subscribe failed: {}, exiting", e);
return Ok(());