fix(security): recognize Windows package runners (#11466)

This commit is contained in:
Jasper
2026-08-24 18:52:37 +00:00
committed by GitHub
parent 11269687a8
commit 2dbec3bf64
@@ -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() {