fix: per-endpoint rate limits, request timeout and input hardening
bal-server: - Wire independent actix-governor buckets per endpoint class (pushtxs, searchtx, info/stats, default) so polling a cheap endpoint can no longer exhaust the push-txs budget; previously every limit was dead config - Enforce BAL_SERVER_ACTIX_TIMEOUT_SECS via a request-timeout middleware that returns 408 Request Timeout, covering slow request bodies - De-duplicate txids within a single /pushtxs request body - Replace the byte-slice xpub detection (panic on non-ASCII) with a safe prefix check bal-pusher: - Handle malformed/empty ZMQ frames without indexing panics or UTF-8 expect - Return an error from sign_message and skip the stats report instead of panicking when the private key cannot be parsed validation / db: - Reject IPv4-mapped IPv6 targets (::ffff:a.b.c.d) in is_valid_welist_url to close an SSRF bypass, with unit tests - Use Path::starts_with for the SQLite forbidden-directory check so paths like /etcfoo are not misclassified misc: - Add explicit secret patterns to .gitignore and sync the docs
This commit is contained in:
@@ -388,7 +388,13 @@ async fn send_stats_report(
|
||||
cfg.url, chain, bcinfo.blocks, bcinfo.median_time, bcinfo.best_block_hash
|
||||
);
|
||||
trace!("message to be sent: {}", message);
|
||||
let sign = sign_message(cfg.ssl_key_path.as_str(), message.as_str());
|
||||
let sign = match sign_message(cfg.ssl_key_path.as_str(), message.as_str()) {
|
||||
Ok(signature) => signature,
|
||||
Err(e) => {
|
||||
error!("Cannot sign stats report, skipping: {}", e);
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
let response = client
|
||||
.post(url)
|
||||
.header("User-Agent", format!("bal-pusher/{}", VERSION))
|
||||
@@ -414,12 +420,12 @@ async fn send_stats_report(
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
fn sign_message(private_key_path: &str, message: &str) -> String {
|
||||
let signing_key =
|
||||
SigningKey::read_pkcs8_pem_file(private_key_path).expect("failed to parse private key PEM");
|
||||
fn sign_message(private_key_path: &str, message: &str) -> Result<String, Box<dyn StdError>> {
|
||||
let signing_key = SigningKey::read_pkcs8_pem_file(private_key_path)
|
||||
.map_err(|e| format!("failed to parse private key PEM at {private_key_path}: {e}"))?;
|
||||
let signature = signing_key.sign(message.as_bytes());
|
||||
|
||||
general_purpose::STANDARD.encode(signature.to_bytes())
|
||||
Ok(general_purpose::STANDARD.encode(signature.to_bytes()))
|
||||
}
|
||||
|
||||
fn parse_env(cfg: &mut MyConfig) {
|
||||
@@ -561,12 +567,12 @@ async fn main() -> std::io::Result<()> {
|
||||
);
|
||||
}
|
||||
consecutive_timeouts = 0;
|
||||
let topic = message[0].clone();
|
||||
let body = message[1].clone();
|
||||
debug!(
|
||||
"ZMQ:GET TOPIC: {}",
|
||||
String::from_utf8(topic.clone()).expect("invalid topic")
|
||||
);
|
||||
let Some(topic) = message.first().cloned() else {
|
||||
warn!("ZMQ: received empty multipart message, skipping");
|
||||
continue;
|
||||
};
|
||||
let body = message.get(1).cloned().unwrap_or_default();
|
||||
debug!("ZMQ:GET TOPIC: {}", String::from_utf8_lossy(&topic));
|
||||
trace!("ZMQ:GET BODY: {}", hex::encode(&body));
|
||||
if topic == b"hashblock" {
|
||||
info!("NEW BLOCK: {}", hex::encode(&body));
|
||||
|
||||
Reference in New Issue
Block a user