mirror of
https://codeberg.org/likwid/likwid.git
synced 2026-03-26 19:03:08 +00:00
Harden registry installs: DNS SSRF checks, timeout, size cap
This commit is contained in:
parent
dc6647efbf
commit
0c99fa253d
1 changed files with 99 additions and 9 deletions
|
|
@ -16,6 +16,8 @@ use sqlx::PgPool;
|
||||||
use sqlx::Row;
|
use sqlx::Row;
|
||||||
use std::net::IpAddr;
|
use std::net::IpAddr;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use std::time::Duration;
|
||||||
|
use tokio::net::lookup_host;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::auth::AuthUser;
|
use crate::auth::AuthUser;
|
||||||
|
|
@ -443,7 +445,10 @@ fn verify_signature_if_required(
|
||||||
Ok(Some(sig_arr.to_vec()))
|
Ok(Some(sig_arr.to_vec()))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn enforce_registry_allowlist(url: &Url, allowlist: &[String]) -> Result<(), (StatusCode, String)> {
|
async fn enforce_registry_allowlist(
|
||||||
|
url: &Url,
|
||||||
|
allowlist: &[String],
|
||||||
|
) -> Result<(), (StatusCode, String)> {
|
||||||
let host = url.host_str().ok_or((
|
let host = url.host_str().ok_or((
|
||||||
StatusCode::BAD_REQUEST,
|
StatusCode::BAD_REQUEST,
|
||||||
"Registry URL must include host".to_string(),
|
"Registry URL must include host".to_string(),
|
||||||
|
|
@ -477,11 +482,63 @@ fn enforce_registry_allowlist(url: &Url, allowlist: &[String]) -> Result<(), (St
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
if !allowlist.is_empty() && !allowlist.iter().any(|h| h == host) {
|
if !allowlist.is_empty() {
|
||||||
return Err((
|
let allowed = allowlist.iter().any(|pattern| {
|
||||||
StatusCode::FORBIDDEN,
|
if pattern == "*" {
|
||||||
"Registry host not in allowlist".to_string(),
|
return true;
|
||||||
));
|
}
|
||||||
|
|
||||||
|
if pattern.starts_with("*.") {
|
||||||
|
let suffix = &pattern[1..];
|
||||||
|
host.ends_with(suffix) || host.eq_ignore_ascii_case(&pattern[2..])
|
||||||
|
} else {
|
||||||
|
host.eq_ignore_ascii_case(pattern)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if !allowed {
|
||||||
|
return Err((
|
||||||
|
StatusCode::FORBIDDEN,
|
||||||
|
"Registry host not in allowlist".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let port = url.port_or_known_default().unwrap_or(443);
|
||||||
|
let lookup = tokio::time::timeout(Duration::from_secs(3), lookup_host((host, port)))
|
||||||
|
.await
|
||||||
|
.map_err(|_| {
|
||||||
|
(
|
||||||
|
StatusCode::BAD_GATEWAY,
|
||||||
|
"Registry host DNS lookup timed out".to_string(),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.and_then(|res| {
|
||||||
|
res.map_err(|_| {
|
||||||
|
(
|
||||||
|
StatusCode::BAD_GATEWAY,
|
||||||
|
"Registry host DNS lookup failed".to_string(),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})?;
|
||||||
|
|
||||||
|
for addr in lookup {
|
||||||
|
let ip = addr.ip();
|
||||||
|
let is_disallowed = match ip {
|
||||||
|
IpAddr::V4(v4) => {
|
||||||
|
v4.is_loopback() || v4.is_private() || v4.is_link_local() || v4.is_unspecified()
|
||||||
|
}
|
||||||
|
IpAddr::V6(v6) => {
|
||||||
|
v6.is_loopback() || v6.is_unique_local() || v6.is_unicast_link_local() || v6.is_unspecified()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if is_disallowed {
|
||||||
|
return Err((
|
||||||
|
StatusCode::FORBIDDEN,
|
||||||
|
"Registry host is not allowed".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
@ -1049,9 +1106,18 @@ async fn install_registry_plugin_package(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enforce_registry_allowlist(&url, ®istry_allowlist)?;
|
enforce_registry_allowlist(&url, ®istry_allowlist).await?;
|
||||||
|
|
||||||
let res = reqwest::get(url.clone())
|
const MAX_REGISTRY_BUNDLE_BYTES: u64 = 2 * 1024 * 1024;
|
||||||
|
|
||||||
|
let client = reqwest::Client::builder()
|
||||||
|
.timeout(Duration::from_secs(10))
|
||||||
|
.build()
|
||||||
|
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
|
||||||
|
|
||||||
|
let mut res = client
|
||||||
|
.get(url.clone())
|
||||||
|
.send()
|
||||||
.await
|
.await
|
||||||
.map_err(|e| (StatusCode::BAD_GATEWAY, e.to_string()))?;
|
.map_err(|e| (StatusCode::BAD_GATEWAY, e.to_string()))?;
|
||||||
|
|
||||||
|
|
@ -1059,7 +1125,31 @@ async fn install_registry_plugin_package(
|
||||||
return Err((StatusCode::BAD_GATEWAY, "Registry fetch failed".to_string()));
|
return Err((StatusCode::BAD_GATEWAY, "Registry fetch failed".to_string()));
|
||||||
}
|
}
|
||||||
|
|
||||||
let bundle: UploadPluginPackageRequest = res.json().await.map_err(|_| {
|
if let Some(len) = res.content_length() {
|
||||||
|
if len > MAX_REGISTRY_BUNDLE_BYTES {
|
||||||
|
return Err((
|
||||||
|
StatusCode::BAD_GATEWAY,
|
||||||
|
"Registry response too large".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut body: Vec<u8> = Vec::new();
|
||||||
|
while let Some(chunk) = res
|
||||||
|
.chunk()
|
||||||
|
.await
|
||||||
|
.map_err(|_| (StatusCode::BAD_GATEWAY, "Registry fetch failed".to_string()))?
|
||||||
|
{
|
||||||
|
body.extend_from_slice(&chunk);
|
||||||
|
if body.len() as u64 > MAX_REGISTRY_BUNDLE_BYTES {
|
||||||
|
return Err((
|
||||||
|
StatusCode::BAD_GATEWAY,
|
||||||
|
"Registry response too large".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let bundle: UploadPluginPackageRequest = serde_json::from_slice(&body).map_err(|_| {
|
||||||
(
|
(
|
||||||
StatusCode::BAD_GATEWAY,
|
StatusCode::BAD_GATEWAY,
|
||||||
"Invalid registry response".to_string(),
|
"Invalid registry response".to_string(),
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue