Merge pull request #554 from mnaza/fix/stealth-empty-ssl-cert-env-551

fix(net): treat empty SSL_CERT_FILE/SSL_CERT_DIR as unset in the stealth client
This commit is contained in:
SGavrl authored and GitHub committed 2026-08-07 16:50:04 +00:00
commit 215861b30f
2 files changed
+51 -3

No files matched your search

+47
View File
@@ -60,6 +60,23 @@ fn configured_root_certificates() -> &'static [reqwest::Certificate] {
}) })
} }
/// Whether SSL_CERT_FILE / SSL_CERT_DIR request a custom TLS trust store. A
/// variable that is set but empty (e.g. `SSL_CERT_FILE=""`, a common shell
/// accident) is treated as unset, matching the `!is_empty()` filter in
/// `configured_root_paths` above. The stealth client (wreq) relies on this:
/// supplying a store to `tls_cert_store` REPLACES the bundled webpki roots, so
/// an empty value would otherwise build a near-empty store and break all HTTPS.
///
/// The only non-test caller is the stealth (wreq) client, so a plain build
/// without the `stealth` feature sees it as unused.
#[cfg_attr(not(feature = "stealth"), allow(dead_code))]
pub(crate) fn custom_cert_store_requested(
cert_file: Option<&std::ffi::OsStr>,
cert_dir: Option<&std::ffi::OsStr>,
) -> bool {
cert_file.is_some_and(|v| !v.is_empty()) || cert_dir.is_some_and(|v| !v.is_empty())
}
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Response { pub struct Response {
pub url: Url, pub url: Url,
@@ -1014,3 +1031,33 @@ mod ssrf_tests {
assert!(client.fetch(&url).await.is_err(), "unknown CA must be rejected"); assert!(client.fetch(&url).await.is_err(), "unknown CA must be rejected");
} }
} }
#[cfg(test)]
mod cert_env_tests {
use super::custom_cert_store_requested;
use std::ffi::OsStr;
#[test]
fn empty_ssl_cert_env_is_treated_as_unset() {
// Set-but-empty must NOT request a custom store: for the stealth client
// that would replace the webpki roots with a near-empty default-paths
// store and break all HTTPS.
assert!(!custom_cert_store_requested(Some(OsStr::new("")), None));
assert!(!custom_cert_store_requested(None, Some(OsStr::new(""))));
assert!(!custom_cert_store_requested(
Some(OsStr::new("")),
Some(OsStr::new(""))
));
// Genuinely unset: no custom store.
assert!(!custom_cert_store_requested(None, None));
// Set and non-empty: build the custom store (behavior unchanged).
assert!(custom_cert_store_requested(
Some(OsStr::new("/etc/corp/ca.pem")),
None
));
assert!(custom_cert_store_requested(
None,
Some(OsStr::new("/etc/ssl/certs"))
));
}
}
+4 -3
View File
@@ -77,9 +77,10 @@ impl StealthHttpClient {
// `tls/conn/ext.rs`), it does not add to them. Applying it unconditionally would break // `tls/conn/ext.rs`), it does not add to them. Applying it unconditionally would break
// every ordinary site whenever the bundle is incomplete. With neither variable set, // every ordinary site whenever the bundle is incomplete. With neither variable set,
// behaviour is byte-for-byte what it was before. // behaviour is byte-for-byte what it was before.
if std::env::var_os("SSL_CERT_FILE").is_some() if crate::client::custom_cert_store_requested(
|| std::env::var_os("SSL_CERT_DIR").is_some() std::env::var_os("SSL_CERT_FILE").as_deref(),
{ std::env::var_os("SSL_CERT_DIR").as_deref(),
) {
match wreq::tls::trust::CertStore::builder().set_default_paths().build() { match wreq::tls::trust::CertStore::builder().set_default_paths().build() {
Ok(store) => builder = builder.tls_cert_store(store), Ok(store) => builder = builder.tls_cert_store(store),
Err(error) => tracing::warn!( Err(error) => tracing::warn!(