Skip to content

fix: security bugs in execution policy, approval mapping, and tool input validation#2882

Merged
3 commits merged into
Hmbown:mainfrom
HUQIANTAO:fix/security-bugs
Jun 8, 2026
Merged

fix: security bugs in execution policy, approval mapping, and tool input validation#2882
3 commits merged into
Hmbown:mainfrom
HUQIANTAO:fix/security-bugs

Conversation

@HUQIANTAO

@HUQIANTAO HUQIANTAO commented Jun 7, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes 5 security-related bugs in the execution policy engine, HTTP API approval mapping, and tool input validation.

Bugs Fixed

Execution Policy Bypass

  1. Deny rule whitespace bypass (execpolicy/lib.rs:446)

    • normalize_command only trimmed and lowercased without normalizing internal whitespace. A command like git status (double space) bypasses the deny rule git status.
    • Fix: normalize_command now collapses internal whitespace via split_whitespace().join(" ").
  2. Deny rule prefix matching without word boundary (execpolicy/lib.rs:351-353)

    • Deny rule rm incorrectly blocks rmdir, rmview due to bare starts_with.
    • Fix: Add word-boundary check: command must equal rule or start with rule + space.
  3. Fallback prefix match optimization (execpolicy/bash_arity.rs:374)

    • format!("{pattern_norm} ") allocates on every check.
    • Fix: Use byte comparison as_bytes().get(len) == Some(&b' ').

Approval Policy

  1. Hardcoded AskForApproval::OnRequest in HTTP API (app-server/lib.rs:283)
    • /tool endpoint ignores user-configured approval policy, always using OnRequest.
    • Fix: Read approval policy from config. Map auto/yolo to UnlessTrusted, never/deny to Never.

Tool Input Validation

  1. Fuzzy indentation search destroys preceding text (tools/file.rs:714-735)

    • line_start_before always expands to line start, destroying preceding content on the same line.
    • Fix: Only expand to line start when match is at a line boundary; otherwise use exact mapped position.
  2. apply_hunk start index potential underflow (tools/apply_patch.rs:1110-1115)

    • Negative cumulative_offset could overflow isize on adversarial input.
    • Fix: Use checked_add_signed with fallback to 0.

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

@greptile-apps greptile-apps 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.

HUQIANTAO has reached the 50-review limit for trial accounts. To continue receiving code reviews, upgrade your plan.

@github-actions

github-actions Bot commented Jun 7, 2026

Copy link
Copy Markdown

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 .github/APPROVED_CONTRIBUTORS will be closed automatically.

Please read CONTRIBUTING.md for the expected contribution shape. A maintainer can grant PR access by commenting /lgtm on a pull request.

@gemini-code-assist gemini-code-assist 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.

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.

Comment on lines +353 to +356
if let Some(rule) = denied_prefixes.iter().find(|rule| {
let norm_rule = normalize_command(rule);
normalized == norm_rule || normalized.starts_with(&format!("{norm_rule} "))
}) {

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.

security-critical critical

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' '))
        }) {

Comment on lines +281 to +291
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)
};

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.

security-high high

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.

Suggested change
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)
};

Comment thread crates/execpolicy/src/bash_arity.rs Outdated
Comment on lines +375 to +376
command_norm == pattern_norm
|| command_norm.starts_with(&format!("{pattern_norm} "))

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.

medium

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' '))

Comment on lines +1117 to +1120
let start_idx = base_idx
.checked_add_signed(*cumulative_offset)
.unwrap_or(0)
.min(lines.len());

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.

medium

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

@greptile-apps greptile-apps 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.

HUQIANTAO has reached the 50-review limit for trial accounts. To continue receiving code reviews, upgrade your plan.

@HUQIANTAO HUQIANTAO changed the title fix: security bugs in execpolicy, app-server, and tools (5 bugs) fix: security bugs in execution policy, approval mapping, and tool input validation Jun 7, 2026

@greptile-apps greptile-apps 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.

HUQIANTAO has reached the 50-review limit for trial accounts. To continue receiving code reviews, upgrade your plan.

Hmbown added a commit that referenced this pull request Jun 7, 2026
@Hmbown Hmbown closed this pull request by merging all changes into Hmbown:main in 3d503a0 Jun 8, 2026
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