← Blog

April 09, 2026

Gemini CLI

Google open-sourced their AI coding agent, Gemini CLI. So we audited it, and found four security vulnerabilities — one medium and three low severity. All were verified against source code and reported to Google via their Bug Hunters program.

GitHub Token Leaked to Redirect Targets

Severity: Low | Confirmed: PoC demonstrates credential leakage

The downloadFile function in github.ts attaches the user’s GITHUB_TOKEN as an Authorization header on every HTTP request, including recursive calls that follow 301/302 redirects:

// packages/cli/src/config/extensions/github.ts:528-531
const token = await getGitHubToken();
if (token) {
  headers['Authorization'] = `token ${token}`;
}

When GitHub redirects a download to a CDN (which it does for tarballs and zipballs), the token is forwarded unconditionally. There is no comparison of the original host vs. the redirect target. A man-in-the-middle or a malicious redirect target receives the user’s GitHub personal access token, potentially granting read/write access to private repositories.

Proof of concept

// "GitHub" server redirects to a "CDN"
const github = http.createServer((_req, res) => {
  res.writeHead(302, { Location: "http://localhost:9998/release.tar.gz" });
  res.end();
});

// "CDN" server logs received headers
const cdn = http.createServer((req, res) => {
  console.log(`Authorization: ${req.headers["authorization"]}`);
  res.writeHead(200);
  res.end();
});
$ node poc-token-leak.js

GET http://localhost:9997/repo/tarball
  Authorization: token ghp_SECRET_TOKEN_12345
  -> Redirect to http://localhost:9998/release.tar.gz
GET http://localhost:9998/release.tar.gz
  Authorization: token ghp_SECRET_TOKEN_12345

--- CDN received request ---
Authorization header: token ghp_SECRET_TOKEN_12345

BUG CONFIRMED: GitHub token leaked to third-party CDN.

The fix is to strip the header on cross-origin redirects:

const redirectUrl = new URL(res.headers.location);
const originalUrl = new URL(url);
if (redirectUrl.hostname !== originalUrl.hostname) {
  delete headers['Authorization'];
}

The redirect handling in github.ts and github_fetch.ts has a second, independent bug:

Infinite Redirect Loop (Post-Increment Bug)

Severity: Low | Confirmed: PoC demonstrates infinite recursion

The fetchJson function in github_fetch.ts has a one-character bug that disables its redirect limit entirely:

// Line 34 — the bug
fetchJson<T>(res.headers.location, redirectCount++);

// Line 28 — the guard that never triggers
if (redirectCount >= 10) {
  throw new Error('Too many redirects');
}

The post-increment operator (redirectCount++) returns the value before incrementing. Every recursive call receives 0, so the >= 10 guard is unreachable. The correct pattern (redirectCount + 1) is already used elsewhere in the same codebase.

Proof of concept

// Tiny server that always redirects
const server = http.createServer((_req, res) => {
  res.writeHead(302, { Location: "http://localhost:9999/" });
  res.end();
});

// The vulnerable pattern (mirrors github_fetch.ts line 34)
function fetchJson(url, redirectCount = 0) {
  if (redirectCount >= 10) throw new Error("Too many redirects");
  return http.get(url, (res) => {
    if (res.statusCode === 302) {
      fetchJson(res.headers.location, redirectCount++); // BUG: always passes 0
    }
  });
}
$ node poc-redirect-loop.js

call #1  redirectCount = 0
call #2  redirectCount = 0
call #3  redirectCount = 0
...
call #847  redirectCount = 0

Stopped after 847 calls — redirectCount never exceeded 0.

Cross-Task Workspace Corruption via process.chdir()

Severity: Medium | Confirmed: PoC demonstrates cross-task data leakage

Gemini CLI’s a2a server is its agent-to-agent protocol server, designed for multi-agent orchestration — it handles concurrent task requests from multiple IDE windows. When a task arrives, setTargetDir() in config.ts calls process.chdir() to set the workspace directory:

// packages/a2a-server/src/config/config.ts:216
process.chdir(resolvedPath);

The problem: process.chdir() is a process-global mutation. It changes the working directory for the entire Node.js process — every request, every task, every concurrent operation. The server then reads it back asynchronously via process.cwd() in loadConfig(), creating a classic TOCTOU race:

  1. Task Alice calls setTargetDir("/alice-project") — CWD is now /alice-project
  2. Task Bob calls setTargetDir("/bob-project") — CWD is now /bob-project
  3. Task Alice resumes and reads process.cwd() — gets /bob-project

Alice’s agent now operates on Bob’s workspace. It loads Bob’s GEMINI.md instructions, indexes Bob’s files, and writes outputs to Bob’s directory.

Proof of concept

// Simulate the vulnerable code path from config.ts + executor.ts
function setTargetDir(workspacePath) {
  process.chdir(path.resolve(workspacePath)); // global mutation
}

async function loadConfig(taskId) {
  await new Promise(r => setTimeout(r, 1)); // async work yields event loop
  return { taskId, workspaceDir: process.cwd() }; // reads corrupted CWD
}

async function getConfig(workspacePath, taskId) {
  setTargetDir({ workspacePath });
  return await loadConfig(taskId); // race window between chdir and cwd read
}

// Two concurrent requests — Alice gets Bob's workspace
const [alice, bob] = await Promise.all([
  getConfig("/alice-project", "task-alice"),
  getConfig("/bob-project", "task-bob"),
]);

// alice.workspaceDir === "/bob-project" — WRONG
$ node poc-chdir-race.js

Task Alice requested: /tmp/workspace-a
Task Alice received:  /tmp/workspace-b (WRONG!)
Task Bob   requested: /tmp/workspace-b
Task Bob   received:  /tmp/workspace-b (correct)

BUG CONFIRMED: A task received the wrong workspace directory.

Process-Global Environment Variable Race Condition

Severity: Low

The a2a server’s _handleToolConfirmationPart method temporarily deletes GOOGLE_CLOUD_PROJECT and GOOGLE_APPLICATION_CREDENTIALS from process.env to prevent credential leakage into tool calls, then restores them in a finally block:

// packages/a2a-server/src/agent/task.ts:941-942
delete process.env.GOOGLE_CLOUD_PROJECT;
delete process.env.GOOGLE_APPLICATION_CREDENTIALS;

// ... async tool execution ...

// finally block at lines 979-984 restores them

Since process.env is shared across the entire Node.js process, concurrent requests observe missing credentials during the async window between deletion and restoration. This is the same class of process-global mutation as the process.chdir() race above — legitimate GCP operations in other requests fail, and the credential isolation mechanism itself is unreliable under concurrency.

Proof of concept

async function handleToolConfirmation() {
  const saved = process.env.GOOGLE_CLOUD_PROJECT;
  delete process.env.GOOGLE_CLOUD_PROJECT; // visible to all concurrent requests
  try {
    await new Promise(r => setTimeout(r, 50)); // async tool execution
  } finally {
    if (saved) process.env.GOOGLE_CLOUD_PROJECT = saved;
  }
}

async function legitimateGcpOperation() {
  await new Promise(r => setTimeout(r, 10)); // hits the race window
  return process.env.GOOGLE_CLOUD_PROJECT; // undefined!
}

await Promise.all([handleToolConfirmation(), legitimateGcpOperation()]);
$ node poc-env-race.js

Initial: GOOGLE_CLOUD_PROJECT = my-production-project

Results:
  Task GCP saw GOOGLE_CLOUD_PROJECT = (undefined)
  Task GCP saw GOOGLE_APPLICATION_CREDENTIALS = (undefined)

BUG CONFIRMED: Credentials missing during concurrent operation.

PoCs

Try it yourself:

git clone https://github.com/zack-eth/gemini-cli-audited
node gemini-cli-audited/poc-token-leak.js
node gemini-cli-audited/poc-redirect-loop.js
node gemini-cli-audited/poc-chdir-race.js
node gemini-cli-audited/poc-env-race.js

Source: github.com/zack-eth/gemini-cli-audited

Disclosure

  • April 9, 2026 — Report submitted to Google via Bug Hunters (g.co/vulnz)

This is the third AI coding agent we’ve audited. Claude Code had one finding, Codex had one finding, and Gemini CLI had four — the most of the three. Two of Gemini CLI’s findings stem from process-global state mutation (process.chdir() and process.env), a pattern that’s particularly dangerous in concurrent Node.js servers where multiple tasks share a single process.

The full audit report is available at audited.xyz/report/gemini-cli.