rc2: SSRF parity, bounded DNS, strict hex parse, over-cap MKB error signaling
This commit is contained in:
+57
-5
@@ -107,10 +107,29 @@ fn resolve_and_guard(url: &str) -> Result<Vec<SocketAddr>, String> {
|
|||||||
if host.is_empty() {
|
if host.is_empty() {
|
||||||
return Err("URL has no host".into());
|
return Err("URL has no host".into());
|
||||||
}
|
}
|
||||||
let addrs: Vec<SocketAddr> = (host.as_str(), port)
|
// `to_socket_addrs` is a BLOCKING DNS lookup that can hang for the OS
|
||||||
.to_socket_addrs()
|
// resolver timeout (tens of seconds) and freeze the rip thread that called
|
||||||
.map_err(|e| format!("could not resolve host: {e}"))?
|
// query(). Run it on a spawned thread and join with a bounded deadline;
|
||||||
.collect();
|
// on timeout return Err so query() yields None and the rip proceeds
|
||||||
|
// (mirrors the bounded-resolve in autorip/libfreemkv).
|
||||||
|
let addrs: Vec<SocketAddr> = {
|
||||||
|
use std::sync::mpsc;
|
||||||
|
const DNS_TIMEOUT: Duration = Duration::from_secs(10);
|
||||||
|
let host = host.clone();
|
||||||
|
let (tx, rx) = mpsc::channel();
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
let res = (host.as_str(), port)
|
||||||
|
.to_socket_addrs()
|
||||||
|
.map(|it| it.collect::<Vec<SocketAddr>>());
|
||||||
|
// Receiver may be gone after the timeout — ignore the send error.
|
||||||
|
let _ = tx.send(res);
|
||||||
|
});
|
||||||
|
match rx.recv_timeout(DNS_TIMEOUT) {
|
||||||
|
Ok(Ok(addrs)) => addrs,
|
||||||
|
Ok(Err(e)) => return Err(format!("could not resolve host: {e}")),
|
||||||
|
Err(_) => return Err("DNS resolution timed out".into()),
|
||||||
|
}
|
||||||
|
};
|
||||||
if addrs.is_empty() {
|
if addrs.is_empty() {
|
||||||
return Err("host did not resolve to any address".into());
|
return Err("host did not resolve to any address".into());
|
||||||
}
|
}
|
||||||
@@ -163,7 +182,15 @@ impl OnlineSource {
|
|||||||
/// The single server-resolved UK for this disc, or `None`. Runs exactly the
|
/// The single server-resolved UK for this disc, or `None`. Runs exactly the
|
||||||
/// one network round-trip; `next_key` gates it to one call per session.
|
/// one network round-trip; `next_key` gates it to one call per session.
|
||||||
fn query(&mut self, inputs: &DiscInputs) -> Option<Key> {
|
fn query(&mut self, inputs: &DiscInputs) -> Option<Key> {
|
||||||
if self.base_url.is_empty() || inputs.mkb.len() > MAX_MKB_BYTES {
|
// No configured service: a clean None ("no service"), not an error.
|
||||||
|
if self.base_url.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
// An over-cap MKB is a real failure to resolve THIS disc, not "no
|
||||||
|
// service" — flag it so the caller reports it distinctly (and a later
|
||||||
|
// ask doesn't conflate it with a missing key).
|
||||||
|
if inputs.mkb.len() > MAX_MKB_BYTES {
|
||||||
|
self.errored = true;
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let b64 = base64::engine::general_purpose::STANDARD;
|
let b64 = base64::engine::general_purpose::STANDARD;
|
||||||
@@ -284,6 +311,12 @@ fn parse_uk(hex: &str) -> Option<[u8; 16]> {
|
|||||||
if hex.len() != 32 {
|
if hex.len() != 32 {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
// Reject any non-hex byte up front. `u8::from_str_radix` on a 2-char
|
||||||
|
// window otherwise accepts sign prefixes (e.g. "+5", "-A"), letting a
|
||||||
|
// signed/whitespace-tainted string slip through as a valid key.
|
||||||
|
if !hex.bytes().all(|b| b.is_ascii_hexdigit()) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
let mut out = [0u8; 16];
|
let mut out = [0u8; 16];
|
||||||
for (i, b) in out.iter_mut().enumerate() {
|
for (i, b) in out.iter_mut().enumerate() {
|
||||||
*b = u8::from_str_radix(hex.get(i * 2..i * 2 + 2)?, 16).ok()?;
|
*b = u8::from_str_radix(hex.get(i * 2..i * 2 + 2)?, 16).ok()?;
|
||||||
@@ -380,4 +413,23 @@ mod tests {
|
|||||||
assert!(!addrs.is_empty());
|
assert!(!addrs.is_empty());
|
||||||
assert_eq!(addrs[0].port(), 8080);
|
assert_eq!(addrs[0].port(), 8080);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Finding #9 regression: parse_uk must reject any non-hex byte up front so
|
||||||
|
/// sign prefixes / whitespace can't slip through the windowed 2-char parse
|
||||||
|
/// (`u8::from_str_radix` accepts "+5", "-A", etc.).
|
||||||
|
#[test]
|
||||||
|
fn parse_uk_rejects_non_hex_bytes() {
|
||||||
|
// Valid 32-char hex parses.
|
||||||
|
assert_eq!(
|
||||||
|
parse_uk("000102030405060708090a0b0c0d0e0f"),
|
||||||
|
Some([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15])
|
||||||
|
);
|
||||||
|
// Sign-prefixed window: "+5" / "-A" would parse via from_str_radix.
|
||||||
|
assert!(parse_uk("+5000102030405060708090a0b0c0d0e").is_none());
|
||||||
|
assert!(parse_uk("-A000102030405060708090a0b0c0d0e").is_none());
|
||||||
|
// Embedded whitespace.
|
||||||
|
assert!(parse_uk("00 0102030405060708090a0b0c0d0e0f").is_none());
|
||||||
|
// Wrong length is still rejected.
|
||||||
|
assert!(parse_uk("00").is_none());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user