google-gemini/gemini-cli

hasPromptCommandTransform compares node.type to '@' instead of node.text, failing to detect bash @P transform

Summary

  • Context: The hasPromptCommandTransform function in packages/core/src/utils/shell-utils.ts is part of the shell command security validation system that detects dangerous bash parameter expansion patterns before executing shell commands.

  • Bug: The function checks operatorNode?.type === '@' instead of operatorNode?.text === '@', causing it to never detect the bash @P prompt transformation operator.

  • Actual vs. expected: The function currently compares the tree-sitter node’s grammar rule type (e.g., “expansion”, “operator”) against the literal string '@', when it should compare the node’s text content to detect the actual @character in the source code.

  • Impact: This bug allows dangerous bash prompt transformation commands like echo ${var@P} to bypass security checks, potentially enabling command injection attacks through carefully crafted prompt expansion exploits.

Code with bug

function hasPromptCommandTransform(root: Node): boolean {
  const stack: Node[] = [root];

  while (stack.length > 0) {
    const current = stack.pop();
    if (!current) {
      continue;
    }

    if (current.type === 'expansion') {
      for (let i = 0; i < current.childCount - 1; i += 1) {
        const operatorNode = current.child(i);
        const transformNode = current.child(i + 1);

        if (
          operatorNode?.type === '@' &&  // <-- BUG 🔴 Should be operatorNode?.text === '@'
          transformNode?.text?.toLowerCase() === 'p'
        ) {
          return true;
        }
      }
    }

    for (let i = current.namedChildCount - 1; i >= 0; i -= 1) {
      const child = current.namedChild(i);
      if (child) {
        stack.push(child);
      }
    }
  }

  return false;
}

Logical proof

Tree-sitter nodes expose two relevant properties:

/** Get this node's type as a string. */
get type(): string;
/** Get the string content of this node. */
get text(): string;
  • node.type is the grammar rule name (e.g., “operator”, “expansion”).

  • node.text is the literal source text (e.g., “@”, “P”).

For a parameter expansion like ${foo@P}, the node representing @ has type = "operator" (or similar) and text = "@". Comparing operatorNode.type === '@' will always be false, so hasPromptCommandTransform never flags @P. Switching to operatorNode.text === '@' correctly detects the operator and allows the function to block such commands as intended.

Recommended fix

Replace the comparison of the operator node from using type to using text:

if (
  operatorNode?.type === '@' &&
  transformNode?.text?.toLowerCase() === 'p'
) {

with:

if (
  operatorNode?.text === '@' &&  // <-- FIX 🟢
  transformNode?.text?.toLowerCase() === 'p'
) {