fix(security): recognize Windows package runners (#11466)
This commit is contained in:
@@ -41,16 +41,8 @@ impl OsvChecker {
|
||||
}
|
||||
}
|
||||
|
||||
/// Convenience: infer ecosystem from command token + parse first package arg.
|
||||
/// - ends_with("npx") → npm
|
||||
/// - ends_with("uvx") → PyPI
|
||||
/// unknown commands → skip (fail open)
|
||||
pub async fn deny_if_malicious_cmd_args(cmd: &str, args: &[String]) -> Result<(), ExtensionError> {
|
||||
let ecosystem = if cmd.ends_with("uvx") {
|
||||
"PyPI"
|
||||
} else if cmd.ends_with("npx") {
|
||||
"npm"
|
||||
} else {
|
||||
let Some(ecosystem) = command_ecosystem(cmd) else {
|
||||
debug!(%cmd, ?args, "Unknown ecosystem for command; skipping OSV check (fail open).");
|
||||
return Ok(());
|
||||
};
|
||||
@@ -67,6 +59,19 @@ pub async fn deny_if_malicious_cmd_args(cmd: &str, args: &[String]) -> Result<()
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn command_ecosystem(cmd: &str) -> Option<&'static str> {
|
||||
let executable = cmd
|
||||
.rsplit(['/', '\\'])
|
||||
.next()
|
||||
.unwrap_or_default()
|
||||
.to_ascii_lowercase();
|
||||
match executable.as_str() {
|
||||
"npx" | "npx.cmd" => Some("npm"),
|
||||
"uvx" | "uvx.exe" => Some("PyPI"),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Direct call without command inference.
|
||||
pub async fn deny_if_malicious(
|
||||
name: &str,
|
||||
@@ -708,6 +713,94 @@ mod tests {
|
||||
assert!(msg.contains("MAL-9999"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial_test::serial]
|
||||
async fn cmd_args_windows_shims_block_malicious_packages() {
|
||||
let server = MockServer::start().await;
|
||||
Mock::given(method("POST"))
|
||||
.and(path("/v1/query"))
|
||||
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
|
||||
"vulns": [ { "id": "MAL-462", "summary": "Malicious package" } ],
|
||||
"next_page_token": null
|
||||
})))
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
let _env = TempEnvVar::set("OSV_ENDPOINT", &format!("{}/v1/query", server.uri()));
|
||||
for (command, package) in [
|
||||
(r"C:\Program Files\nodejs\NPX.CMD", "bad-npm-package"),
|
||||
("C:/Program Files/nodejs/nPx.CmD", "bad-npm-package"),
|
||||
(r"C:\Users\user\.local\bin\UVX.EXE", "bad_pypi_package"),
|
||||
("C:/Users/user/.local/bin/uVx.ExE", "bad_pypi_package"),
|
||||
] {
|
||||
let result = deny_if_malicious_cmd_args(command, &[package.to_string()]).await;
|
||||
|
||||
assert!(result.is_err(), "expected {command} to query OSV");
|
||||
assert!(format!("{:?}", result.unwrap_err()).contains("MAL-462"));
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial_test::serial]
|
||||
async fn cmd_args_path_qualified_commands_allow_clean_packages() {
|
||||
let server = MockServer::start().await;
|
||||
Mock::given(method("POST"))
|
||||
.and(path("/v1/query"))
|
||||
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
|
||||
"vulns": [],
|
||||
"next_page_token": null
|
||||
})))
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
let _env = TempEnvVar::set("OSV_ENDPOINT", &format!("{}/v1/query", server.uri()));
|
||||
for (command, package) in [
|
||||
("/usr/local/bin/NPX", "clean-npm-package"),
|
||||
("/opt/venv/bin/UvX", "clean_pypi_package"),
|
||||
] {
|
||||
let result = deny_if_malicious_cmd_args(command, &[package.to_string()]).await;
|
||||
|
||||
assert!(
|
||||
result.is_ok(),
|
||||
"expected {command} to allow a clean package"
|
||||
);
|
||||
}
|
||||
|
||||
assert_eq!(server.received_requests().await.unwrap().len(), 2);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial_test::serial]
|
||||
async fn cmd_args_lookalike_commands_are_skipped() {
|
||||
let server = MockServer::start().await;
|
||||
Mock::given(method("POST"))
|
||||
.and(path("/v1/query"))
|
||||
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
|
||||
"vulns": [ { "id": "MAL-462", "summary": "Malicious package" } ],
|
||||
"next_page_token": null
|
||||
})))
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
let _env = TempEnvVar::set("OSV_ENDPOINT", &format!("{}/v1/query", server.uri()));
|
||||
for command in [
|
||||
"my-npx",
|
||||
"npx.exe",
|
||||
"npx.cmd.bak",
|
||||
"/usr/bin/notnpx",
|
||||
"uvx.cmd",
|
||||
"uvx.exe.bak",
|
||||
r"C:\Tools\my-uvx",
|
||||
] {
|
||||
let result =
|
||||
deny_if_malicious_cmd_args(command, &["malicious-package".to_string()]).await;
|
||||
|
||||
assert!(result.is_ok(), "expected {command} to remain fail-open");
|
||||
}
|
||||
|
||||
assert!(server.received_requests().await.unwrap().is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial_test::serial]
|
||||
async fn cmd_args_skip_flags_then_parse() {
|
||||
|
||||
Reference in New Issue
Block a user