fix: security bugs in execution policy, approval mapping, and tool input validation#2882
fix: security bugs in execution policy, approval mapping, and tool input validation#28823 commits merged into
Conversation
1. Fix deny rule prefix matching without word boundary (execpolicy/lib.rs:351-353) - Deny rule 'rm' now blocks 'rm -rf /' but NOT 'rmdir' or 'rmview' - Previously used bare starts_with which matched any command starting with 'rm' - Add word-boundary check: command must equal rule or start with rule+space 2. Fix fallback prefix match clarity (execpolicy/bash_arity.rs:362-374) - Improve comment to clarify word-boundary matching behavior - The trailing space in starts_with already provides word boundary 3. Fix hardcoded AskForApproval::OnRequest in HTTP API (app-server/lib.rs:283) - Read approval_policy from config instead of hardcoding OnRequest - Users with 'auto'/'yolo' policy now get UnlessTrusted for API calls - Previously ignored user's configured security posture 4. Fix fuzzy indentation search destroying preceding text (tools/file.rs:714-735) - When match starts mid-line after whitespace stripping, use exact position - Previously always expanded to line start, destroying preceding content - Now only expands to line start when match is at a line boundary 5. Fix potential underflow in apply_hunk start index (tools/apply_patch.rs:1110-1115) - Use checked_add_signed to safely handle negative cumulative_offset - Prevents isize overflow on adversarial patch input - Clamp to lines.len() instead of relying on .max(0) cast
There was a problem hiding this comment.
HUQIANTAO has reached the 50-review limit for trial accounts. To continue receiving code reviews, upgrade your plan.
|
Thanks @HUQIANTAO for taking the time to contribute. This repository is currently observing a maintainer-managed contribution gate in dry-run mode, so this pull request is staying open. When enforcement is enabled, pull requests from contributors who are not listed in Please read |
There was a problem hiding this comment.
Code Review
This pull request updates the execution policy engine to resolve approval policies from configuration, implements word-boundary prefix matching for fallback and deny rules, improves patch application safety with checked offset additions, and refines fuzzy matching to prevent overwriting preceding text. The review identified a critical security vulnerability where whitespace manipulation can bypass deny rules, a logical bug mapping 'never'/'deny' policies to prompt instead of reject, and several performance optimization opportunities to avoid unnecessary string and vector allocations.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| if let Some(rule) = denied_prefixes.iter().find(|rule| { | ||
| let norm_rule = normalize_command(rule); | ||
| normalized == norm_rule || normalized.starts_with(&format!("{norm_rule} ")) | ||
| }) { |
There was a problem hiding this comment.
CRITICAL SECURITY VULNERABILITY: The deny rule matching is vulnerable to whitespace bypass. Because normalize_command only trims and lowercases the command without normalizing internal whitespace, a command like "git status" (with multiple spaces) will not match the deny rule "git status", allowing users to bypass the execution policy.
To fix this, normalize_command (at line 446) should be updated to normalize all internal whitespace (e.g., by splitting on whitespace and joining with a single space).
Additionally, we can optimize the prefix matching to avoid allocating a new String via format! on every check by checking if the character immediately following the prefix is a space.
if let Some(rule) = denied_prefixes.iter().find(|rule| {
let norm_rule = normalize_command(rule);
normalized == norm_rule
|| (normalized.starts_with(&norm_rule)
&& normalized.as_bytes().get(norm_rule.len()) == Some(&b' '))
}) {| let approval_mode = { | ||
| let cfg = state.config.read().await; | ||
| cfg.approval_policy | ||
| .as_deref() | ||
| .and_then(|p| match p.trim().to_ascii_lowercase().as_str() { | ||
| "auto" | "yolo" => Some(codewhale_execpolicy::AskForApproval::UnlessTrusted), | ||
| "never" | "deny" => Some(codewhale_execpolicy::AskForApproval::OnRequest), | ||
| _ => None, | ||
| }) | ||
| .unwrap_or(codewhale_execpolicy::AskForApproval::OnRequest) | ||
| }; |
There was a problem hiding this comment.
The mapping for "never" and "deny" config values to AskForApproval::OnRequest is a logical contradiction. OnRequest always prompts the user for approval, whereas "never"/"deny" should forbid/reject commands that require approval without prompting. They should be mapped to AskForApproval::Never instead.
Additionally, the runtime lock is acquired before reading the config. Holding the runtime lock across the .await of state.config.read() can cause unnecessary lock contention on the runtime Mutex. Although we cannot easily move the lock acquisition in this suggestion because the lock line is outside the diff, you should refactor this handler to read the config and resolve approval_mode before locking runtime.
| let approval_mode = { | |
| let cfg = state.config.read().await; | |
| cfg.approval_policy | |
| .as_deref() | |
| .and_then(|p| match p.trim().to_ascii_lowercase().as_str() { | |
| "auto" | "yolo" => Some(codewhale_execpolicy::AskForApproval::UnlessTrusted), | |
| "never" | "deny" => Some(codewhale_execpolicy::AskForApproval::OnRequest), | |
| _ => None, | |
| }) | |
| .unwrap_or(codewhale_execpolicy::AskForApproval::OnRequest) | |
| }; | |
| let approval_mode = { | |
| let cfg = state.config.read().await; | |
| cfg.approval_policy | |
| .as_deref() | |
| .and_then(|p| match p.trim().to_ascii_lowercase().as_str() { | |
| "auto" | "yolo" => Some(codewhale_execpolicy::AskForApproval::UnlessTrusted), | |
| "never" | "deny" => Some(codewhale_execpolicy::AskForApproval::Never), | |
| _ => None, | |
| }) | |
| .unwrap_or(codewhale_execpolicy::AskForApproval::OnRequest) | |
| }; |
| command_norm == pattern_norm | ||
| || command_norm.starts_with(&format!("{pattern_norm} ")) |
There was a problem hiding this comment.
We can optimize the prefix matching to avoid allocating a new String via format! on every check by checking if the character immediately following the prefix is a space.
command_norm == pattern_norm
|| (command_norm.starts_with(&pattern_norm)
&& command_norm.as_bytes().get(pattern_norm.len()) == Some(&b' '))| let start_idx = base_idx | ||
| .checked_add_signed(*cumulative_offset) | ||
| .unwrap_or(0) | ||
| .min(lines.len()); |
There was a problem hiding this comment.
While reviewing the fuzzy matching logic, we noticed that the subsequent loop (outside this diff) defines search_range by allocating a Vec via collect() on every iteration of fuzz (up to 50 times per hunk). This can be completely avoided by iterating directly over min..=max (which naturally handles fuzz == 0 as start_idx..=start_idx without any special case). Consider refactoring that loop to avoid these unnecessary allocations.
1. Fix whitespace bypass in normalize_command (execpolicy/lib.rs:446)
- Collapse internal whitespace to prevent 'git status' bypassing 'git status'
- split_whitespace().join(' ') normalizes all whitespace
2. Fix 'never'/'deny' approval mapping (app-server/lib.rs:287)
- Map to AskForApproval::Never instead of OnRequest
- 'never'/'deny' should forbid commands, not prompt for approval
3. Optimize prefix matching (execpolicy/lib.rs:355, bash_arity.rs:375)
- Avoid format! allocation on every check
- Use byte comparison for space boundary check
There was a problem hiding this comment.
HUQIANTAO has reached the 50-review limit for trial accounts. To continue receiving code reviews, upgrade your plan.
There was a problem hiding this comment.
HUQIANTAO has reached the 50-review limit for trial accounts. To continue receiving code reviews, upgrade your plan.
Summary
Fixes 5 security-related bugs in the execution policy engine, HTTP API approval mapping, and tool input validation.
Bugs Fixed
Execution Policy Bypass
Deny rule whitespace bypass (
execpolicy/lib.rs:446)normalize_commandonly trimmed and lowercased without normalizing internal whitespace. A command likegit status(double space) bypasses the deny rulegit status.normalize_commandnow collapses internal whitespace viasplit_whitespace().join(" ").Deny rule prefix matching without word boundary (
execpolicy/lib.rs:351-353)rmincorrectly blocksrmdir,rmviewdue to barestarts_with.rule + space.Fallback prefix match optimization (
execpolicy/bash_arity.rs:374)format!("{pattern_norm} ")allocates on every check.as_bytes().get(len) == Some(&b' ').Approval Policy
AskForApproval::OnRequestin HTTP API (app-server/lib.rs:283)/toolendpoint ignores user-configured approval policy, always usingOnRequest.auto/yolotoUnlessTrusted,never/denytoNever.Tool Input Validation
Fuzzy indentation search destroys preceding text (
tools/file.rs:714-735)line_start_beforealways expands to line start, destroying preceding content on the same line.apply_hunkstart index potential underflow (tools/apply_patch.rs:1110-1115)cumulative_offsetcould overflowisizeon adversarial input.checked_add_signedwith fallback to 0.