Implement audited findings
Dependency security audit / rustsec (push) Failing after 3s

This commit is contained in:
hugo committed 2026-08-27 15:34:31 +02:00
1 parent 2feecae70d
commit c2dd826920
11 files changed
+773 -220

No files matched your search

+167 -33
View File
@@ -1022,8 +1022,8 @@ impl ReqwestGitTransport {
fn response(response: reqwest::blocking::Response) -> Result<Vec<u8>, GitError> {
let status = response.status();
if status == reqwest::StatusCode::UNAUTHORIZED || status == reqwest::StatusCode::FORBIDDEN {
return Err(GitError::AuthenticationFailed);
if let Some(error) = classify_http_status(status) {
return Err(error);
}
if !status.is_success() {
return Err(GitError::InvalidRepository(format!(
@@ -1273,13 +1273,103 @@ fn map_ambiguous_push_error(error: GitError) -> GitError {
fn map_reqwest_error(error: reqwest::Error) -> GitError {
if error.is_builder() {
return invalid(error);
return generic_https_error(&error);
}
let message = error.to_string().to_ascii_lowercase();
if message.contains("certificate") || message.contains("tls") {
GitError::TlsFailed
classify_structured_https_error(&error).unwrap_or_else(|| generic_https_error(&error))
}
fn classify_http_status(status: reqwest::StatusCode) -> Option<GitError> {
matches!(
status,
reqwest::StatusCode::UNAUTHORIZED | reqwest::StatusCode::FORBIDDEN
)
.then_some(GitError::AuthenticationFailed)
}
fn classify_structured_https_error(error: &(dyn Error + 'static)) -> Option<GitError> {
let mut current = Some(error);
let mut tls = false;
let mut network = false;
while let Some(error) = current {
if let Some(error) = error.downcast_ref::<reqwest::Error>() {
if error.status().and_then(classify_http_status).is_some() {
return Some(GitError::AuthenticationFailed);
}
network |= error.is_timeout() || error.is_connect();
}
if error.is::<rustls::Error>() {
tls = true;
}
if let Some(error) = error.downcast_ref::<std::io::Error>() {
if error.kind() == std::io::ErrorKind::PermissionDenied {
return Some(GitError::AuthenticationFailed);
}
network |= matches!(
error.kind(),
std::io::ErrorKind::ConnectionRefused
| std::io::ErrorKind::ConnectionReset
| std::io::ErrorKind::ConnectionAborted
| std::io::ErrorKind::NotConnected
| std::io::ErrorKind::TimedOut
| std::io::ErrorKind::AddrNotAvailable
| std::io::ErrorKind::NetworkUnreachable
| std::io::ErrorKind::HostUnreachable
);
}
current = error.source();
}
if tls {
Some(GitError::TlsFailed)
} else if network {
Some(GitError::NetworkUnavailable)
} else {
GitError::NetworkUnavailable
None
}
}
fn generic_https_error(error: &dyn fmt::Display) -> GitError {
GitError::InvalidRepository(protocol_diagnostic(error.to_string().as_bytes()))
}
fn map_https_connect_error(error: gix::remote::connect::Error) -> GitError {
match error {
error @ gix::remote::connect::Error::Connect(_) => {
classify_structured_https_error(&error).unwrap_or_else(|| generic_https_error(&error))
}
error => generic_https_error(&error),
}
}
fn map_https_prepare_error(error: gix::remote::fetch::prepare::Error) -> GitError {
match error {
error @ gix::remote::fetch::prepare::Error::RefMap(
gix::remote::ref_map::Error::Transport(_) | gix::remote::ref_map::Error::Handshake(_),
) => classify_structured_https_error(&error).unwrap_or_else(|| generic_https_error(&error)),
error => generic_https_error(&error),
}
}
fn map_https_fetch_error(
error: gix::remote::fetch::Error,
control: &GitOperationControl,
) -> GitError {
if control.is_cancelled() {
return GitError::Cancelled;
}
match error {
gix::remote::fetch::Error::Fetch(gix::protocol::fetch::Error::ConsumePack(_)) => {
GitError::MalformedGitPack
}
gix::remote::fetch::Error::Fetch(
gix::protocol::fetch::Error::FetchResponse(_)
| gix::protocol::fetch::Error::Negotiate(_)
| gix::protocol::fetch::Error::MissingServerFeature { .. },
) => GitError::GitProtocolFailed,
error @ (gix::remote::fetch::Error::Fetch(gix::protocol::fetch::Error::Client(_))
| gix::remote::fetch::Error::Client(_)) => {
classify_structured_https_error(&error).unwrap_or_else(|| generic_https_error(&error))
}
error => generic_https_error(&error),
}
}
@@ -1905,7 +1995,7 @@ impl GitRepository {
.map_err(invalid)?;
let mut connection = remote
.connect(gix::remote::Direction::Fetch)
.map_err(invalid)?;
.map_err(map_https_connect_error)?;
connection.set_credentials(move |action| match action {
gix::credentials::helper::Action::Get(context) => {
let same_origin = context
@@ -1931,32 +2021,10 @@ impl GitRepository {
});
let prepared = connection
.prepare_fetch(gix::progress::Discard, Default::default())
.map_err(invalid)?;
.map_err(map_https_prepare_error)?;
let outcome = prepared
.receive(gix::progress::Discard, control.cancelled.as_ref())
.map_err(|error| {
if control.is_cancelled() {
return GitError::Cancelled;
}
let text = error.to_string();
let lower = text.to_ascii_lowercase();
if text.contains("401")
|| text.contains("403")
|| lower.contains("authentication")
|| (lower.contains("credential") && lower.contains("not accepted"))
{
GitError::AuthenticationFailed
} else if lower.contains("certificate") || lower.contains("tls") {
GitError::TlsFailed
} else if lower.contains("network")
|| lower.contains("connect")
|| lower.contains("dns")
{
GitError::NetworkUnavailable
} else {
invalid(error)
}
})?;
.map_err(|error| map_https_fetch_error(error, control))?;
Ok(matches!(
outcome.status,
gix::remote::fetch::Status::Change { .. }
@@ -4331,8 +4399,12 @@ mod ssh_push_tests;
#[cfg(test)]
mod tests {
use std::{error::Error, fmt, io};
use super::{
GitError, GitIdentity, GitRemoteCredentialOverride, GitRepository, SshPassphraseProvider,
GitError, GitIdentity, GitOperationControl, GitRemoteCredentialOverride, GitRepository,
SshPassphraseProvider, classify_http_status, classify_structured_https_error,
generic_https_error, map_https_fetch_error,
};
use crate::{
config::SshFingerprint,
@@ -4341,6 +4413,17 @@ mod tests {
struct Passphrases;
#[derive(Debug)]
struct Diagnostic(String);
impl fmt::Display for Diagnostic {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(&self.0)
}
}
impl Error for Diagnostic {}
impl SshPassphraseProvider for Passphrases {
fn ssh_key_passphrase(
&self,
@@ -4362,6 +4445,57 @@ mod tests {
assert!(repository.repository.committer().is_some());
}
#[test]
fn https_failures_use_only_structured_status_and_sources() {
assert_eq!(
classify_http_status(reqwest::StatusCode::UNAUTHORIZED),
Some(GitError::AuthenticationFailed)
);
assert_eq!(
classify_http_status(reqwest::StatusCode::FORBIDDEN),
Some(GitError::AuthenticationFailed)
);
assert_eq!(
classify_structured_https_error(&io::Error::from(io::ErrorKind::ConnectionRefused)),
Some(GitError::NetworkUnavailable)
);
assert_eq!(
classify_structured_https_error(&io::Error::from(io::ErrorKind::TimedOut)),
Some(GitError::NetworkUnavailable)
);
assert_eq!(
classify_structured_https_error(&rustls::Error::General("opaque".to_owned())),
Some(GitError::TlsFailed)
);
let rendered_only = Diagnostic(
"401 403 authentication credential certificate tls network connect dns".to_owned(),
);
assert_eq!(classify_structured_https_error(&rendered_only), None);
assert!(matches!(
generic_https_error(&rendered_only),
GitError::InvalidRepository(_)
));
let local_permission = gix::remote::fetch::Error::RemovePackKeepFile {
path: "pack.keep".into(),
source: io::Error::from(io::ErrorKind::PermissionDenied),
};
assert!(matches!(
map_https_fetch_error(local_permission, &GitOperationControl::default()),
GitError::InvalidRepository(_)
));
}
#[test]
fn generic_https_diagnostic_is_bounded() {
let error = generic_https_error(&Diagnostic("x".repeat(32 * 1024)));
let GitError::InvalidRepository(diagnostic) = error else {
panic!("unstructured HTTPS error must stay generic");
};
assert!(diagnostic.len() <= 8 * 1024);
}
#[test]
fn one_ssh_passphrase_override_is_bound_to_its_fingerprint() {
let selected = SshFingerprint::parse("SHA256:47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU")