Skip to content

Harden allowlist-only process execution - #621

Closed
logbie wants to merge 3 commits into
mainfrom
agent/harden-allowlist-shell-boundary
Closed

Harden allowlist-only process execution#621
logbie wants to merge 3 commits into
mainfrom
agent/harden-allowlist-shell-boundary

Conversation

@logbie

@logbie logbie commented Jul 16, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • make allowlist_only authorize direct executable launches only
  • reject shell chaining, pipes, redirects, expansion, and other shell-backed forms before execution
  • stop name-only entries such as echo from authorizing ./echo, /tmp/echo, or another explicit path with the same basename
  • require path-bearing allowlist entries to canonicalize to the executable being invoked
  • keep explicit argument-list execution available for allowlisted programs
  • replace the Windows test fixture that allowlisted cmd.exe with a non-shell executable
  • document the direct-exec, path-binding, and interpreter allowlisting boundaries

Security impact

The previous policy checked only the first program token and then permitted the entire command through a shell. An allowlisted prefix could therefore launch additional, unlisted commands. It also reduced explicit executable paths to their basename, so an attacker-controlled binary at another path inherited the authority of a trusted name. The policy now rejects shell parsing in allowlist_only, distinguishes name-only execution from explicit paths, and binds path entries to the same canonical executable.

Validation

  • git diff --check
  • changed Rust source passes a tree-sitter syntax scan
  • unit regressions cover chaining, pipes, command substitution, redirects, path substitution, and the public allowlist helper
  • end-to-end subprocess regression covers the policy boundary on Unix and Windows
  • uploaded Git blobs/tree match the inspected local commit
  • native Cargo/rustfmt are unavailable locally; repository CI is the source-of-truth validation and passed on the final head
  • all GitHub CI, config lint, and review checks passed on the final head

Production readiness

  • area: Security / Reliability / Documentation / Testing
  • advances the subprocess security gate tracked in WFL Production Readiness: 8/10 by January 1, 2027 #610
  • compatibility impact: configurations using shell syntax with allowlist_only must move to explicit arguments or deliberately opt into sanitized / unrestricted; explicit executable paths must be allowlisted explicitly

@coderabbitai

coderabbitai Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@logbie, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 59 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 386045e2-99c1-4dd0-bfea-ef1f38b83190

📥 Commits

Reviewing files that changed from the base of the PR and between 0f52b3a and cac1318.

📒 Files selected for processing (4)
  • Docs/04-advanced-features/subprocess-execution.md
  • Docs/reference/configuration-reference.md
  • src/interpreter/command_sanitizer.rs
  • tests/subprocess_security_test.rs
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch agent/harden-allowlist-shell-boundary

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@logbie
logbie marked this pull request as ready for review July 16, 2026 18:13
Copilot AI review requested due to automatic review settings July 16, 2026 18:13

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: cac1318451

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +202 to +204
if needs_shell {
return Ok(ValidationResult::Blocked {
reason: "Shell features are not permitted in allowlist_only mode; use the direct-exec form with an explicit argument list"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve raw Windows path allowlist execution

When allowlist_only is used on Windows, a direct raw command such as execute command "C:\\Tools\\tool.exe" is classified as needs_shell before this call because contains_shell_metacharacters treats backslashes as shell metacharacters; this new unconditional block then rejects it before the canonical path allowlist check can run, even if allowed_shell_commands contains that same executable path. That makes the documented explicit-path allowlist unusable for the common Windows path form unless callers add a dummy with arguments list or rewrite paths with /, so this should distinguish path separators from actual shell syntax before blocking.

Useful? React with 👍 / 👎.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR hardens WFL’s allowlist_only subprocess policy by ensuring allowlists authorize only direct executable launches (not entire shell command lines), tightening name-vs-path matching semantics, and updating tests/docs to reflect the stronger boundary.

Changes:

  • Block shell-backed syntax (chaining/pipes/redirects/substitution/etc.) in allowlist_only, requiring direct exec with an explicit argument list.
  • Distinguish name-only allowlist entries from explicit paths; for path entries, require canonical path equality to the invoked executable.
  • Update Windows allowlist-only tests to avoid allowlisting a shell (cmd.exe) and document the new boundaries.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.

File Description
src/interpreter/command_sanitizer.rs Enforces “direct-exec only” in allowlist_only, tightens allowlist matching rules, and adds regression tests.
tests/subprocess_security_test.rs Updates Windows fixture to use a non-shell allowlisted program and adds an injection/chaining regression.
Docs/reference/configuration-reference.md Documents name-vs-path allowlist semantics and the “no shell features in allowlist_only” rule.
Docs/04-advanced-features/subprocess-execution.md Adds user-facing guidance about allowlist-only boundaries and interpreter allowlisting risks.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 259 to +262
pub fn is_allowlisted(&self, command: &str) -> bool {
let base_command = Self::program_basename(&self.get_command_base(command));
self.is_program_allowlisted(&base_command)
if Self::contains_shell_metacharacters(command) {
return false;
}
Comment on lines +269 to +293
fn is_program_allowlisted(&self, program: &str) -> bool {
let program_has_path = Self::has_path_syntax(program);
self.config.allowed_shell_commands.iter().any(|allowed| {
let allowed_has_path = Self::has_path_syntax(allowed);

// A name-only entry delegates resolution to the trusted process
// environment's PATH. It must not also authorize a caller-selected
// executable at `./name`, `/tmp/name`, or another explicit path.
if program_has_path != allowed_has_path {
return false;
}
if program_has_path {
let Ok(program_path) = std::fs::canonicalize(program) else {
return false;
};
let Ok(allowed_path) = std::fs::canonicalize(allowed) else {
return false;
};

#[cfg(windows)]
{
return program_path
.to_string_lossy()
.eq_ignore_ascii_case(&allowed_path.to_string_lossy());
}

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Devin Review found 2 potential issues.

Open in Devin Review

Comment on lines +202 to +207
if needs_shell {
return Ok(ValidationResult::Blocked {
reason: "Shell features are not permitted in allowlist_only mode; use the direct-exec form with an explicit argument list"
.to_string(),
});
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔍 Windows explicit-path commands in the string form are unreachable in allowlist_only

The docs now advertise that explicit executable paths can be allowlisted in allowlist_only mode (e.g. allowed_shell_commands = C:\tools\where.exe). However, needs_shell is derived from CommandSanitizer::contains_shell_metacharacters(command) (see src/interpreter/mod.rs:1683-1684), and backslash \ is in the metacharacter set (src/interpreter/command_sanitizer.rs:147-150). Consequently, a Windows string-form invocation such as execute command "C:\tools\prog.exe" (empty argument list) always sets needs_shell = true and is rejected by the new AllowlistOnly shell-feature block (command_sanitizer.rs:202-207) before the path allowlist check runs. The path-allowlist feature therefore only works via the explicit with arguments [...] (argv) form on Windows, since that form sets args non-empty and keeps needs_shell = false. This is a platform-specific usability gap, not a correctness/security bug (argv form works, and the code in mod.rs is outside this PR's diff), but reviewers may want to confirm the docs make the argv-only requirement explicit for Windows explicit paths.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines 193 to 218
ShellExecutionMode::AllowlistOnly => {
if self.is_program_allowlisted(&program_base) {
if needs_shell {
Ok(ValidationResult::RequiresShell {
reason: "Command is allowlisted".to_string(),
warnings: vec!["Using shell execution (allowlisted)".to_string()],
})
} else {
Ok(ValidationResult::Safe)
}
// An allowlist can authorize one executable, but it cannot
// safely authorize an entire shell command line. If shell
// parsing is allowed here, a command such as
// `echo safe; unlisted-program` passes the `echo` check and the
// shell executes both commands. Require the argv/direct-exec
// form in this mode; callers that intentionally need pipes,
// redirects, expansion, or chaining must opt into `sanitized`
// or `unrestricted` explicitly.
if needs_shell {
return Ok(ValidationResult::Blocked {
reason: "Shell features are not permitted in allowlist_only mode; use the direct-exec form with an explicit argument list"
.to_string(),
});
}

if self.is_program_allowlisted(program) {
Ok(ValidationResult::Safe)
} else {
Ok(ValidationResult::Blocked {
reason: format!(
"Program '{}' is not in the allowlist (allowed_shell_commands)",
program_base
program
),
})
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔍 Non-trivial security behavior change without a Dev Diary entry

This PR is a non-trivial security/behavior change (allowlist_only now rejects all shell-backed forms and binds path entries to a canonical executable). CLAUDE.md/AGENTS.md state that non-trivial behavior changes should ship a Dev diary/ entry in the same change. The diff does not add a new Dev Diary entry, though related prior entries exist (e.g. Dev diary/2026-07-11-subprocess-policy-enforcement.md, 2026-07-13-issue-610-phase-1-*). Reviewers should confirm whether an existing entry sufficiently covers this hardening step or whether a new one is expected per the documentation policy.

(Refers to lines 193-219)

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

logbie commented Jul 17, 2026

Copy link
Copy Markdown
Collaborator Author

Superseded by #632, which preserves this security fix in the consolidated Rust-source hardening PR. The combined head is mergeable and all required CI checks are green.

@logbie logbie closed this Jul 17, 2026
@logbie
logbie deleted the agent/harden-allowlist-shell-boundary branch August 14, 2026 04:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants