Skip to content

fix(core): prevent indirect prompt injection via build file modifications and untrusted flags - #29250

Open
villahernandez-coder wants to merge 3 commits into
google-gemini:mainfrom
villahernandez-coder:FixBug-cla-445881265
Open

fix(core): prevent indirect prompt injection via build file modifications and untrusted flags#29250
villahernandez-coder wants to merge 3 commits into
google-gemini:mainfrom
villahernandez-coder:FixBug-cla-445881265

Conversation

@villahernandez-coder

@villahernandez-coder villahernandez-coder commented Sep 8, 2026

Copy link
Copy Markdown

Summary

This PR implements robust mechanisms to improve workspace boundary validation (specifically focusing on build configuration files and external command parameters) under restricted workspace mode. It refactors built-in execution paths (including shell, edit, and write_file) to check for command flags or arguments guided by external context tags (such as Google Docs, Buganizer, web fetch, or MCP server responses) and require explicit user confirmation.

Details

  1. External Context Processor: Refactored the tracking utilities to scan conversation history, extract tokens from <untrusted_context> blocks, and verify whether a command contains parameters guided by external input.
  2. Build Configuration Tracking: Implemented tracking of build configuration files (e.g., package.json, Makefile, pyproject.toml, BUILD.bazel) when edited or created. If build configuration changes are detected within the current session, any subsequent build or test command execution (such as npm run, make, cargo, blaze) is surfaced for explicit user confirmation.
  3. Refined User Confirmation UI: Updated ToolConfirmationMessage to display detailed context to the user, highlighting specific parameters or recent build modifications, and explaining the execution options.

Related Issues

Related to FixBug-cla-445881265

How to Validate

  1. Run unit tests in CLI to verify warning UI:
    npm test -w @google/gemini-cli

Pre-Merge Checklist

  • [ x] Updated relevant documentation and README (if needed)
  • [ x] Added/updated tests (if needed)
  • Noted breaking changes (if any)
  • [ x] Validated on required platforms/methods:
    • MacOS
      • npm run
      • npx
      • Docker
      • Podman
      • Seatbelt
    • Windows
      • npm run
      • npx
      • Docker
    • [ x] Linux
      • [ x] npm run
      • npx
      • Docker

@villahernandez-coder
villahernandez-coder requested review from a team as code owners September 8, 2026 18:45
@google-cla

google-cla Bot commented Sep 8, 2026

Copy link
Copy Markdown

Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

View this failed invocation of the CLA check for more information.

For the most up to date status, view the checks section at the bottom of the pull request.

@github-actions github-actions Bot added the size/xl An extra large PR label Sep 8, 2026
@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown

📊 PR Size: size/XL

  • Lines changed: 1344
  • Additions: +1291
  • Deletions: -53
  • Files changed: 18

@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown

🛑 Action Required: Evaluation Approval

Steering changes have been detected in this PR. To prevent regressions, a maintainer must approve the evaluation run before this PR can be merged.

Maintainers:

  1. Go to the Workflow Run Summary.
  2. Click the yellow 'Review deployments' button.
  3. Select the 'eval-gate' environment and click 'Approve'.

Once approved, the evaluation results will be posted here automatically.

@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces a robust security layer to mitigate indirect prompt injection attacks by tracking untrusted context and enforcing strict confirmation requirements for build-related operations. It adds mechanisms to detect untrusted shell flags and mandates user approval when modifying or executing build configuration files, significantly hardening the workspace against unauthorized modifications and command execution.

Highlights

  • Untrusted Context Tracker: Introduced a utility to monitor conversation history for external, unauthenticated inputs wrapped in <untrusted_context> tags and flag potentially malicious shell command parameters.
  • Build File Protection: Implemented mandatory user confirmation for editing or writing to critical build configuration files such as package.json, Makefile, and BUILD.bazel.
  • Build Execution Security: Added tracking for build file modifications within a session, requiring explicit user confirmation for subsequent build or test commands.
  • UI/UX Enhancements: Updated ToolConfirmationMessage to display critical security warnings and suppress persistent approvals for sensitive operations involving untrusted flags or build modifications.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize the Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counterproductive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@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 introduces build file protection and untrusted context tracking to prevent unauthorized build modifications and command execution with untrusted flags. The reviewer provided critical feedback highlighting several security and architectural vulnerabilities: naive whitespace splitting and command parsing in findUntrustedFlags and isBuildOrTestCommand can be bypassed with quotes or prefixed environment variables; the policy engine's build file protection can be bypassed via MCP filesystem tools; and tracking modified build files using a module-level global Set violates repository rules against global state, risking race conditions in concurrent environments. The reviewer recommended using shell-quote and getCommandRoots for robust command parsing, expanding tool name matching to cover MCP tools, adopting a session-scoped WeakMap for tracking modified files, and suppressing persistent approval options in the UI when modified build files are detected.

Note: Security Review did not run due to the size of the PR.

const detected = new Set<string>();

// Tokenize the command arguments
const rawTokens = command.trim().split(/\s+/);

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

The current implementation of findUntrustedFlags tokenizes the command using a naive whitespace split (command.trim().split(/\s+/)). This fails to correctly parse arguments containing spaces (e.g., inside quotes) and leaves quotes attached to the tokens (e.g., '"//foo/bar:target"' or '--flag="value"'), which prevents exact matching against untrustedTokens (where quotes are stripped). This allows attackers to easily bypass the prompt injection protection by simply quoting their arguments or flags. Use the existing shell-quote library to parse the command safely and accurately.

  // Parse the command safely using shell-quote to handle quotes and escapes correctly
  let parsed: ReturnType<typeof shellParse>;
  try {
    parsed = shellParse(command);
  } catch {
    // Fallback to whitespace split if parsing fails
    parsed = command.trim().split(/\s+/);
  }

  const rawTokens = parsed
    .map((x) => {
      if (typeof x === 'string') return x;
      if (x && typeof x === 'object' && 'pattern' in x) return x.pattern;
      return '';
    })
    .filter(Boolean);

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Done
@gemini-code-assist check my changes and let me know if you have additional comments in this thread?

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.

The implementation of findUntrustedFlags now correctly uses shell-quote to parse the command, which properly handles quoted arguments and escapes. This effectively mitigates the identified prompt injection bypass risk. The logic for extracting tokens from the parsed output is also correct. The changes look good.

Comment on lines +240 to +253
export function isBuildOrTestCommand(command: string): boolean {
if (!command) {
return false;
}

const trimmed = command.trim();
const root = trimmed.split(/\s+/)[0];
if (!root) {
return false;
}

const normalized = root.toLowerCase();
return BUILD_TEST_COMMAND_ROOTS.has(normalized);
}

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

The current implementation of isBuildOrTestCommand only checks the very first token of the command. This can be easily bypassed by prefixing the command with environment variables (e.g., VAR=1 npm test) or common shell builtins/chains (e.g., cd . && npm test). Use the existing getCommandRoots utility from shell-utils.js to extract all command roots and check if any of them match the build/test command roots.

Suggested change
export function isBuildOrTestCommand(command: string): boolean {
if (!command) {
return false;
}
const trimmed = command.trim();
const root = trimmed.split(/\s+/)[0];
if (!root) {
return false;
}
const normalized = root.toLowerCase();
return BUILD_TEST_COMMAND_ROOTS.has(normalized);
}
import { getCommandRoots } from './shell-utils.js';
export function isBuildOrTestCommand(command: string): boolean {
if (!command) {
return false;
}
try {
const roots = getCommandRoots(command);
return roots.some((root) => BUILD_TEST_COMMAND_ROOTS.has(root.toLowerCase()));
} catch {
// Fallback if parsing fails
const trimmed = command.trim();
const root = trimmed.split(/\s+/)[0];
return root ? BUILD_TEST_COMMAND_ROOTS.has(root.toLowerCase()) : false;
}
}

Comment on lines +773 to +778
const isFileEditTool = toolNamesToTry.some(
(name) =>
EDIT_TOOL_NAMES.has(name) ||
name === 'replace' ||
name === 'write_file',
);

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

The "Build File Protection" check only triggers if the tool name is in a hardcoded list of core edit tools (EDIT_TOOL_NAMES, replace, write_file). This allows an agent to completely bypass the protection by using MCP filesystem tools (such as the official @modelcontextprotocol/server-filesystem tool write_file or edit_file) to modify build files. Update the check to also match any tool names containing write/edit keywords when they target a build file.

      const isFileEditTool = toolNamesToTry.some(
        (name) =>
          EDIT_TOOL_NAMES.has(name) ||
          name === 'replace' ||
          name === 'write_file' ||
          /write|edit|replace|patch|update|create|append|save/i.test(name),
      );

* SPDX-License-Identifier: Apache-2.0
*/

import type { Content } from '@google/genai';

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.

high

Import parse from shell-quote to enable safe and robust shell command parsing in findUntrustedFlags.

Suggested change
import type { Content } from '@google/genai';
import type { Content } from '@google/genai';
import { parse as shellParse } from 'shell-quote';

Comment on lines +255 to +276
const sessionModifiedBuildFiles = new Set<string>();

/**
* Records that a build configuration file was modified in this session.
*/
export function recordModifiedBuildFile(filePath: string): void {
sessionModifiedBuildFiles.add(filePath);
}

/**
* Returns all build configuration files that were modified in this session.
*/
export function getModifiedBuildFiles(): string[] {
return Array.from(sessionModifiedBuildFiles);
}

/**
* Resets the tracked modified build files (primarily for testing or session reset).
*/
export function resetModifiedBuildFiles(): void {
sessionModifiedBuildFiles.clear();
}

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.

high

Using a module-level global Set (sessionModifiedBuildFiles) to track modified build files violates the repository's general rules against global state. In concurrent environments (such as multiple agent sessions running in the same Node.js process), this state will be shared across all sessions, leading to race conditions and cross-session information leaks. Use a WeakMap keyed by the session's Config or context instance to ensure session-scoped isolation.

const globalSessionKey = {};
const sessionModifiedBuildFiles = new WeakMap<object, Set<string>>();

/**
 * Records that a build configuration file was modified in this session.
 */
export function recordModifiedBuildFile(filePath: string, sessionKey: object = globalSessionKey): void {
  let files = sessionModifiedBuildFiles.get(sessionKey);
  if (!files) {
    files = new Set<string>();
    sessionModifiedBuildFiles.set(sessionKey, files);
  }
  files.add(filePath);
}

/**
 * Returns all build configuration files that were modified in this session.
 */
export function getModifiedBuildFiles(sessionKey: object = globalSessionKey): string[] {
  const files = sessionModifiedBuildFiles.get(sessionKey);
  return files ? Array.from(files) : [];
}

/**
 * Resets the tracked modified build files (primarily for testing or session reset).
 */
export function resetModifiedBuildFiles(sessionKey: object = globalSessionKey): void {
  sessionModifiedBuildFiles.delete(sessionKey);
}
References
  1. Avoid module-level global variables for state to prevent race conditions in concurrent environments. Instead, use instance-scoped properties, such as within a session class, for state that is primarily used within that scope.

Comment on lines +355 to +358
const hasUntrustedFlags =
confirmationDetails.untrustedFlags &&
confirmationDetails.untrustedFlags.length > 0;
if (isTrustedFolder && !hasUntrustedFlags) {

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.

high

When modifiedBuildFiles is present, the shell tool's shouldConfirmExecute will always force confirmation on subsequent build commands, regardless of any session-scoped or permanent allow rules. Offering the user "Allow for this session" or "Allow this command for all future sessions" in this state is confusing because those options will be silently ignored on the next execution. Suppress these persistent approval options when modifiedBuildFiles is detected, just like we do for untrustedFlags.

      const hasUntrustedFlags = 
        confirmationDetails.untrustedFlags &&
        confirmationDetails.untrustedFlags.length > 0;
      const hasModifiedBuildFiles = 
        confirmationDetails.modifiedBuildFiles &&
        confirmationDetails.modifiedBuildFiles.length > 0;
      if (isTrustedFolder && !hasUntrustedFlags && !hasModifiedBuildFiles) {

Comment on lines +962 to +964
if (isBuildFile(this.resolvedPath)) {
recordModifiedBuildFile(this.resolvedPath);
}

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.

high

Pass this.config as the session key to recordModifiedBuildFile to ensure session-scoped tracking of modified build files.

Suggested change
if (isBuildFile(this.resolvedPath)) {
recordModifiedBuildFile(this.resolvedPath);
}
if (isBuildFile(this.resolvedPath)) {
recordModifiedBuildFile(this.resolvedPath, this.config);
}

Comment on lines +442 to +444
if (isBuildFile(this.resolvedPath)) {
recordModifiedBuildFile(this.resolvedPath);
}

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.

high

Pass this.config as the session key to recordModifiedBuildFile to ensure session-scoped tracking of modified build files.

Suggested change
if (isBuildFile(this.resolvedPath)) {
recordModifiedBuildFile(this.resolvedPath);
}
if (isBuildFile(this.resolvedPath)) {
recordModifiedBuildFile(this.resolvedPath, this.config);
}

Comment on lines +305 to +310
const command = stripShellWrapper(this.params.command);
const history = this.getHistory();
const untrustedContext = extractUntrustedContext(history);
const untrustedFlags = findUntrustedFlags(command, untrustedContext);
const modifiedBuildFiles = getModifiedBuildFiles();
const isBuildCmd = isBuildOrTestCommand(command);

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.

high

Pass this.context.config to getModifiedBuildFiles to retrieve only the build files modified within the current session.

Suggested change
const command = stripShellWrapper(this.params.command);
const history = this.getHistory();
const untrustedContext = extractUntrustedContext(history);
const untrustedFlags = findUntrustedFlags(command, untrustedContext);
const modifiedBuildFiles = getModifiedBuildFiles();
const isBuildCmd = isBuildOrTestCommand(command);
const command = stripShellWrapper(this.params.command);
const history = this.getHistory();
const untrustedContext = extractUntrustedContext(history);
const untrustedFlags = findUntrustedFlags(command, untrustedContext);
const modifiedBuildFiles = getModifiedBuildFiles(this.context.config);
const isBuildCmd = isBuildOrTestCommand(command);

Comment on lines +488 to +492
const history = this.getHistory();
const untrustedContext = extractUntrustedContext(history);
const untrustedFlags = findUntrustedFlags(command, untrustedContext);
const modifiedBuildFiles = getModifiedBuildFiles();
const isBuildCmd = isBuildOrTestCommand(command);

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.

high

Pass this.context.config to getModifiedBuildFiles to retrieve only the build files modified within the current session.

Suggested change
const history = this.getHistory();
const untrustedContext = extractUntrustedContext(history);
const untrustedFlags = findUntrustedFlags(command, untrustedContext);
const modifiedBuildFiles = getModifiedBuildFiles();
const isBuildCmd = isBuildOrTestCommand(command);
const history = this.getHistory();
const untrustedContext = extractUntrustedContext(history);
const untrustedFlags = findUntrustedFlags(command, untrustedContext);
const modifiedBuildFiles = getModifiedBuildFiles(this.context.config);
const isBuildCmd = isBuildOrTestCommand(command);

@gemini-cli gemini-cli Bot added the status/need-issue Pull requests that need to have an associated issue. label Sep 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/xl An extra large PR status/need-issue Pull requests that need to have an associated issue.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant