fix(security): stop classifying read-only pacman -S queries as Install (#2667)

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
sanil-23
2026-05-26 15:19:12 +05:30
committed by GitHub
co-authored by Claude Opus 4.7
parent 4e7c6e1f53
commit ef7a94dc6f
2 changed files with 37 additions and 1 deletions
+18 -1
View File
@@ -814,6 +814,23 @@ const NODE_PKG_READ_VERBS: &[&str] = &[
/// run build scripts, so they are fail-closed to `Write`.
const CARGO_READ_VERBS: &[&str] = &["tree", "metadata", "search", "info", "version", "help"];
/// Detect a pacman *install/upgrade* from its bundled operation flag.
///
/// pacman packs its operation and modifiers into a single flag (`-Syu`, `-Ss`),
/// and `args` reach us already lowercased — so the `-S` (sync) operation is
/// indistinguishable from a literal `-s` by case alone. We therefore key off
/// the *modifier* letters instead of a blanket `starts_with("-s")`, which would
/// over-match every read-only `-S` query: a `-S`-family flag mutates the host
/// only when it carries none of pacman's read-only query modifiers — search
/// (`s`), info (`i`), list (`l`), groups (`g`) or print (`p`). So `-S pkg`,
/// `-Sy`, `-Syu` are installs while `-Ss`/`-Si`/`-Sl`/`-Sg`/`-Sp` are reads.
fn is_pacman_install(args: &[String]) -> bool {
args.iter().any(|a| {
a.strip_prefix("-s")
.is_some_and(|modifiers| !modifiers.contains(['s', 'i', 'l', 'g', 'p']))
})
}
/// Detect a package-manager *install* invocation. These mutate the host /
/// global environment, so they are the always-ask `Install` bucket (even in
/// Full) — the same gate the dedicated `install_tool` enforces, applied to the
@@ -826,7 +843,7 @@ fn is_install_command(base: &str, args: &[String]) -> bool {
match base {
// System package managers.
"apt" | "apt-get" | "dnf" | "yum" | "zypper" => has("install"),
"pacman" => args.iter().any(|a| a.starts_with("-s")), // -S / -Sy / -Syu (lowercased)
"pacman" => is_pacman_install(args),
"apk" => has("add"),
"brew" | "snap" | "flatpak" | "winget" | "choco" | "scoop" => has("install"),
// Language package managers — host/global-modifying installs only.
+19
View File
@@ -427,6 +427,8 @@ fn classify_installs_are_install_bucket() {
"apt-get install -y curl",
"brew install ripgrep",
"pacman -S vim",
"pacman -Sy",
"pacman -Syu",
"apk add bash",
"dnf install nginx",
"pip install requests",
@@ -456,6 +458,23 @@ fn classify_local_installs_are_write_not_install() {
assert_eq!(p.classify_command("cargo add serde"), CommandClass::Write);
}
#[test]
fn classify_pacman_readonly_queries_are_not_install() {
let p = default_policy();
// pacman's `-S` family includes read-only queries (search/info/list/groups/
// print). A blanket `starts_with("-s")` mis-bucketed these as always-ask
// Install; they must fall through to the fail-closed Write default instead.
for c in [
"pacman -Ss firefox", // search
"pacman -Si vim", // info
"pacman -Sl core", // list a repo
"pacman -Sg", // list groups
"pacman -Sp vim", // print download URLs
] {
assert_eq!(p.classify_command(c), CommandClass::Write, "{c}");
}
}
#[test]
fn gate_decision_install_always_asks_even_in_full() {
assert_eq!(