Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .jules/sentinel.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,8 @@
**Vulnerability:** The `OperationQueue` worker in `mill-server` executed file operations (create, write, delete, rename) using raw paths from the operation object without validating they were within the project root.
**Learning:** Background workers that process serialized operations are a common bypass for security checks enforced at the API layer. The API layer might validate the request, but if the worker is "dumb" and blindly executes the queued operation, an internal attacker or a buggy component can exploit it.
**Prevention:** Validation must happen at the *execution point* (in the worker), not just at the ingestion point. We introduced `validate_path` in the worker loop to enforce project root containment using `canonicalize` (handling non-existent files correctly).

## 2025-06-04 - Argument Injection in Command Allowlists
**Vulnerability:** The `run_validation` command allowlist in `mill-services` verified if user-provided commands were safe by checking if the command string started with allowed prefixes using `.starts_with()`. This allowed bypassing the restriction by appending malicious strings without spaces (e.g., `cargo test-malicious`).
**Learning:** Using simple string operations like `.starts_with()` for validating commands against a prefix allowlist is insufficient and prone to argument injection bypasses. The underlying shell or command runner parses arguments based on spaces and quotes, not arbitrary character positions.
**Prevention:** Always parse the command string into its component parts (program and arguments) *before* validating against an allowlist. Ensure exact matching of the program name and each individual allowed argument.
51 changes: 36 additions & 15 deletions crates/mill-services/src/services/filesystem/file_service/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,21 +56,6 @@ impl FileService {
"make check",
];

let is_safe = safe_prefixes
.iter()
.any(|prefix| self.validation_config.command.trim().starts_with(prefix));

if !is_safe {
error!(
command = %self.validation_config.command,
"Validation command blocked by security policy. Command must start with a known safe prefix (e.g., 'cargo', 'npm', 'go', 'make')."
);
return Some(json!({
"validation_status": "error",
"validation_error": format!("Security Error: Command '{}' is not in the allowed list.", self.validation_config.command)
}));
}

// Run validation command in the project root
// SECURITY: Parse command string to avoid shell injection
let (program, args) = match parse_command_line(&self.validation_config.command) {
Expand All @@ -83,6 +68,42 @@ impl FileService {
}
};

// SECURITY: Validate the parsed command against the allowlist.
// We parse both the target command and the allowed prefixes, then ensure
// exact matching of the program name and each prefix argument.
// This prevents argument injection bypasses (e.g., "cargo test-malicious")
// that could occur if we merely checked if the command string started
// with the prefix using `.starts_with()`.
let is_safe = safe_prefixes.iter().any(|prefix| {
if let Some((safe_prog, safe_args)) = parse_command_line(prefix) {
if program != safe_prog {
return false;
}
if args.len() < safe_args.len() {
return false;
}
for (i, safe_arg) in safe_args.iter().enumerate() {
if args[i] != *safe_arg {
return false;
}
}
true

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Reject extra options after allowed validation prefixes

When the validation command starts with an allowed prefix but then supplies tool options, this still returns true, so the allowlist can still be bypassed with commands such as make test -f /tmp/evil.mk. I checked GNU make's CLI (Usage: make [options] [target] ..., and -f FILE reads that makefile), and make test -f /tmp/evil.mk executes the test target from the attacker-selected file even though only make test is allowlisted. For validation configs controlled by an untrusted or compromised source, this preserves arbitrary command execution despite the intended fix.

Useful? React with πŸ‘Β / πŸ‘Ž.

} else {
false
}
});

if !is_safe {
error!(
command = %self.validation_config.command,
"Validation command blocked by security policy. Command must start with a known safe prefix (e.g., 'cargo', 'npm', 'go', 'make')."
);
return Some(json!({
"validation_status": "error",
"validation_error": format!("Security Error: Command '{}' is not in the allowed list.", self.validation_config.command)
}));
}

let output = match Command::new(&program)
.args(&args)
.current_dir(&self.project_root)
Expand Down
Loading