Skip to content

fix(core): contain legacy checkpoint paths to the checkpoint directory - #29521

Open
ManoharPaturi wants to merge 3 commits into
google-gemini:mainfrom
ManoharPaturi:fix/checkpoint-legacy-path-traversal
Open

ManoharPaturi wants to merge 3 commits into
google-gemini:mainfrom
ManoharPaturi:fix/checkpoint-legacy-path-traversal

Conversation

@ManoharPaturi

@ManoharPaturi ManoharPaturi commented Sep 27, 2026 •

Copy link
Copy Markdown

Summary

_getCheckpointPath and deleteCheckpoint build the legacy fallback path with path.join(geminiDir, +"checkpoint-${tag}.json"+) using the raw tag, and path.join normalizes the .. segments away. A tag such as x/../../secret therefore resolves to a file outside the checkpoint directory: /chat delete <tag> unlinks that file and checkpoint load returns its contents. The new encoded path (_checkpointPath) is safe — only the legacy raw fallback is exposed.

Details

  • Adds a _isInsideCheckpointDir helper: the legacy raw path is only considered when it resolves inside the checkpoint directory, so a traversal tag simply misses with normal ENOENT semantics. Applied at both legacy uses: the load/exists fallback in _getCheckpointPath and the backward-compat unlink in deleteCheckpoint.
  • Containment uses the repo's isSubpath (path.relative-based, case-insensitive on win32/darwin) against both the raw and the realpath'd directory, so setups where a directory segment is a symlink (/var → /private/var on macOS) don't drop legitimate checkpoints. The candidate itself stays lexical — resolveToRealPath decodes %XX sequences and the raw tag is user input, so it must be checked exactly as the fs calls will open it.
  • Flat legacy tags (the only shape historic checkpoints were written with) behave exactly as before.
  • Same bug class as fix(a2a-server): enforce authentication and stop checkpoint path traversal #28699 fixed for the a2a-server restore path; this closes the equivalent gap in the core logger paths raised in Path traversal in checkpoint delete/load: a ../ tag deletes files outside the checkpoints directory (legacy raw path) #29191.
  • Commits 2–3 address the gemini-code-assist review (case-insensitive comparison, then the symlinked-directory mismatch it caught).

Related Issues

Fixes #29191

How to Validate

cd packages/core
npx vitest run src/core/logger.test.ts -t "traversal"
  • On main, the three traversal regression tests fail: deleteCheckpoint('x/../../outside-target') deletes a file next to the checkpoint directory and returns true, and loadCheckpoint / checkpointExists reach the outside file.
  • With this fix, all three return their miss paths (false / empty history) and the outside file is untouched.
  • The flat-legacy test confirms a genuine legacy raw-path checkpoint still loads and deletes normally.

Pre-Merge Checklist

  • Added/updated tests (if needed)
  • Validated on required platforms/methods:
    • MacOS
      • npx vitest run src/core/logger.test.ts — 43/43 passing
      • npx eslint + tsc --noEmit clean on changed files

The legacy fallback path in _getCheckpointPath and deleteCheckpoint is
built with path.join(geminiDir, `checkpoint-${tag}.json`) using the raw
tag, and path.join normalizes the '..' segments away, so a tag such as
'x/../../secret' resolves outside the checkpoint directory: /chat delete
unlinks the file and checkpoint load reads it back.

Gate both legacy uses behind a resolve-based containment check so a
traversal tag simply misses (ENOENT semantics) while flat legacy tags
keep working. Regression tests cover delete/load/exists traversal plus
the flat legacy path.
@ManoharPaturi
ManoharPaturi requested a review from a team as a code owner September 27, 2026 05:26
@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 addresses a security vulnerability where legacy checkpoint paths could be manipulated via directory traversal to access or delete files outside the intended checkpoint directory. By introducing a validation helper, the system now ensures that any legacy path resolution is constrained to the safe directory, effectively neutralizing traversal attempts while preserving existing functionality for legitimate legacy checkpoints.

Highlights

  • Security Fix: Implemented a path validation check to prevent directory traversal attacks via legacy checkpoint tags.
  • New Helper Method: Added _isInsideCheckpointDir to ensure that legacy raw paths resolve strictly within the designated checkpoint directory.
  • Backward Compatibility: Maintained support for standard flat legacy checkpoint tags while securing the system against malicious path inputs.
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. ↩

@github-actions github-actions Bot added the size/m A medium sized PR label Sep 27, 2026
@github-actions

github-actions Bot commented Sep 27, 2026 •

Copy link
Copy Markdown

📊 PR Size: size/M

  • Lines changed: 95
  • Additions: +94
  • Deletions: -1
  • Files changed: 2

@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 enhances security by preventing path traversal vulnerabilities when loading or deleting legacy raw-path checkpoints. It introduces a helper method _isInsideCheckpointDir to verify that resolved paths remain within the designated checkpoint directory, accompanied by comprehensive unit tests. The feedback suggests improving this validation by using a robust resolveToRealPath function and handling case-insensitive filesystems (like Windows and macOS) to avoid potential mismatches with legitimate checkpoints.

Comment on lines +304 to +307
private _isInsideCheckpointDir(candidatePath: string): boolean {
const checkpointDir = path.resolve(this.geminiDir!);
return path.resolve(candidatePath).startsWith(checkpointDir + path.sep);
}

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

On case-insensitive filesystems (such as Windows and macOS), path casing can vary. Using a simple case-sensitive startsWith check can cause legitimate legacy checkpoints to fail to load or delete due to casing mismatches. Additionally, ensure consistent path resolution and prevent path traversal by using resolveToRealPath instead of standard path.resolve for all related path validations.

  private _isInsideCheckpointDir(candidatePath: string): boolean {
    const checkpointDir = resolveToRealPath(this.geminiDir!);
    const resolvedCandidate = resolveToRealPath(candidatePath);
    const isCaseInsensitive = process.platform === 'win32' || process.platform === 'darwin';
    const prefix = checkpointDir + path.sep;
    return isCaseInsensitive
      ? resolvedCandidate.toLowerCase().startsWith(prefix.toLowerCase())
      : resolvedCandidate.startsWith(prefix);
  }
References
  1. Ensure consistent path resolution by using a single, robust function (e.g., resolveToRealPath) for all related path validations, including internal validations in components like WorkspaceContext.
  2. Sanitize user-provided file paths used in file system operations to prevent path traversal vulnerabilities.

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.

Applied it with one change: I kept resolveToRealPath on the directory only and left the candidate path lexical. resolveToRealPath decodes %XX sequences, and the tag here is raw user input, so the guard has to compare the exact string the fs calls will open. Case-insensitive prefix check is in for win32/darwin now.

@gemini-cli gemini-cli Bot added priority/p1 Important and should be addressed in the near term. area/security Issues related to security labels Sep 27, 2026
Realpath the checkpoint directory so symlinked segments can't split the
prefix, and compare case-insensitively on win32/darwin. The candidate
path stays lexical on purpose: resolveToRealPath decodes %XX sequences,
and the raw tag is user input that must be checked exactly as the fs
calls will open it.
@ManoharPaturi

Copy link
Copy Markdown
Author

Small update in the 2nd commit, based on the code assist review:

  • checkpoint dir now goes through resolveToRealPath, so symlinked segments can't break the prefix check
  • prefix comparison is case-insensitive on win32/darwin (both have case-insensitive filesystems)
  • the candidate path is still compared lexically on purpose. resolveToRealPath decodes %XX sequences and the tag here is raw user input, so the guard needs to check the exact string the fs calls will open

logger tests 43/43, eslint + tsc clean on the changed file.

resolveToRealPath on the directory while resolving the candidate
lexically caused a prefix mismatch wherever a segment of the
checkpoint directory is itself a symlink (e.g. /var ->
/private/var on macOS), dropping legitimate legacy checkpoints.

Use the repo's isSubpath (path.relative-based, case-handling for
win32/darwin included) against BOTH the raw and the realpath'd
directory. The candidate itself stays lexical on purpose:
resolveToRealPath decodes %XX sequences and the raw tag is user
input, so it must be checked as the fs calls will open it.
@gemini-cli

gemini-cli Bot commented Oct 5, 2026

Copy link
Copy Markdown
Contributor

Hi there! Thank you for your interest in contributing to Gemini CLI.

To ensure we maintain high code quality and focus on our prioritized roadmap, we only guarantee review and consideration of pull requests for issues that are explicitly labeled as 'help wanted'.

This PR will be closed in 7 days if it remains without that designation. We encourage you to find and contribute to existing 'help wanted' issues in our backlog! Thank you for your understanding.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/security Issues related to security priority/p1 Important and should be addressed in the near term. size/m A medium sized PR status/pr-nudge-sent

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Path traversal in checkpoint delete/load: a ../ tag deletes files outside the checkpoints directory (legacy raw path)

1 participant