Skip to content

fix(gerrit): prevent path traversal in publish_code_suggestions#2314

Open
gvago wants to merge 3 commits intomainfrom
fix/gerrit-path-traversal
Open

fix(gerrit): prevent path traversal in publish_code_suggestions#2314
gvago wants to merge 3 commits intomainfrom
fix/gerrit-path-traversal

Conversation

@gvago
Copy link
Copy Markdown

@gvago gvago commented Apr 14, 2026

Summary

  • Sanitize relevant_file path in publish_code_suggestions() to prevent directory traversal
  • Resolve the target path and verify it stays within repo_path before writing
  • Skip suggestions with suspicious paths and log a warning

Problem

suggestion['relevant_file'] comes from AI output and is used directly in Path(repo_path) / relevant_file without validation. A path like ../../etc/passwd could write outside the repo directory.

Test plan

  • Verify normal file paths still work
  • Verify ../ paths are rejected with a warning log

🤖 Generated with Claude Code

Sanitize the `relevant_file` path before writing to prevent directory
traversal attacks. The path is resolved and verified to stay within
`repo_path` before any file write occurs. Suggestions with suspicious
paths are skipped with a warning log.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@qodo-free-for-open-source-projects
Copy link
Copy Markdown
Contributor

Review Summary by Qodo

Prevent path traversal in Gerrit code suggestions publishing

🐞 Bug fix

Grey Divider

Walkthroughs

Description
• Sanitize file paths in publish_code_suggestions() to prevent directory traversal attacks
• Resolve target path and verify it stays within repository bounds
• Skip suggestions with suspicious paths and log warnings
Diagram
flowchart LR
  A["suggestion['relevant_file']"] --> B["Resolve to absolute path"]
  B --> C{"Path within repo_root?"}
  C -->|No| D["Log warning and skip"]
  C -->|Yes| E["Write suggestion to file"]
Loading

Grey Divider

File Changes

1. pr_agent/git_providers/gerrit_provider.py 🐞 Bug fix +7/-1

Add path traversal protection to code suggestions

• Added path resolution using pathlib.Path.resolve() to normalize file paths
• Implemented validation check to ensure target path stays within repo_root boundary
• Added warning log and continue statement to skip suggestions with path traversal attempts
• Updated add_suggestion() call to use sanitized target_path instead of unsanitized
 concatenation

pr_agent/git_providers/gerrit_provider.py


Grey Divider

Qodo Logo

@qodo-free-for-open-source-projects
Copy link
Copy Markdown
Contributor

qodo-free-for-open-source-projects bot commented Apr 14, 2026

Code Review by Qodo

🐞 Bugs (0)   📘 Rule violations (2)   📎 Requirement gaps (0)
📘\ ☼ Reliability (1) ⚙ Maintainability (1) ⭐ New (1)

Grey Divider


Action required

1. Useless f-string in warning 📘
Description
get_logger().warning(f"...") is an f-string with no interpolation, which will trigger Ruff’s
F541 and can fail CI linting. This introduces a repository lint/format violation in newly added
code.
Code

pr_agent/git_providers/gerrit_provider.py[349]

+                get_logger().warning(f"Skipping malformed suggestion: missing or invalid 'relevant_file'")
Evidence
Compliance requires new/modified code to remain compliant with repository linting tools (Ruff). The
added line uses an f-string without any {...} placeholders, which Ruff flags as F541 (f-string
missing placeholders).

pr_agent/git_providers/gerrit_provider.py[349-349]
Best Practice: Learned patterns

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A new `warning()` call uses an f-string without any interpolation (`f"..."`), which Ruff flags (F541) and may fail CI.
## Issue Context
This line logs a static message and does not need f-string formatting.
## Fix Focus Areas
- pr_agent/git_providers/gerrit_provider.py[349-349]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Unvalidated suggestion["relevant_file"] access📘
Description
The new code indexes suggestion["relevant_file"] without checking that suggestion is a dict and
the key exists/is a string, so malformed AI output can raise TypeError/KeyError and break
publishing. This violates the requirement to defensively validate external inputs and handle edge
cases gracefully.
Code

pr_agent/git_providers/gerrit_provider.py[R345-351]

+        repo_root = pathlib.Path(self.repo_path).resolve()
    for suggestion in code_suggestions:
+            # Sanitize file path to prevent directory traversal
+            target_path = (repo_root / suggestion["relevant_file"]).resolve()
+            if not str(target_path).startswith(str(repo_root)):
+                get_logger().warning(f"Skipping suggestion with path traversal: {suggestion['relevant_file']}")
+                continue
Evidence
The checklist requires null/type checks before indexing/calling methods on external inputs; the
added line directly indexes suggestion["relevant_file"] and then operates on it without any
guards, which can raise runtime exceptions and halt the loop.

Rule 3: Robust Error Handling
pr_agent/git_providers/gerrit_provider.py[345-351]
Best Practice: Learned patterns

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`publish_code_suggestions()` assumes every `suggestion` has a string `relevant_file` and indexes it directly. If AI output is missing keys or has wrong types, this can raise `KeyError`/`TypeError` and stop publishing.
## Issue Context
This function processes external/AI-provided structures (`code_suggestions`). Defensive validation is required before indexing and before passing values into `pathlib` operations.
## Fix Focus Areas
- pr_agent/git_providers/gerrit_provider.py[345-351]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Unsafe startswith path containment📘
Description
The repo containment check uses str(target_path).startswith(str(repo_root)), which can be bypassed
by absolute paths sharing the same string prefix (e.g., repo_root=/tmp/repo and
target_path=/tmp/repo2/...). This undermines the intended path traversal protection.
Code

pr_agent/git_providers/gerrit_provider.py[R348-350]

+            target_path = (repo_root / suggestion["relevant_file"]).resolve()
+            if not str(target_path).startswith(str(repo_root)):
+                get_logger().warning(f"Skipping suggestion with path traversal: {suggestion['relevant_file']}")
Evidence
The checklist requires robust handling of edge cases; the added containment logic relies on string
prefix matching rather than proper path relationship checks, which is a known incorrect approach for
filesystem boundary enforcement.

Rule 3: Robust Error Handling
pr_agent/git_providers/gerrit_provider.py[348-350]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The current containment check uses `startswith` on stringified paths, which is not a correct security boundary check and can allow false positives (prefix matches).
## Issue Context
Python 3.12 supports `Path.is_relative_to()`, which should be used for reliable containment checks (or `target_path.relative_to(repo_root)` inside a targeted `try/except ValueError`).
## Fix Focus Areas
- pr_agent/git_providers/gerrit_provider.py[348-350]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

4. Unhandled exceptions from Path.resolve() 📘
Description
target_path = (repo_root / suggestion["relevant_file"]).resolve() can raise non-ValueError
exceptions (e.g., filesystem/OS errors or symlink loop RuntimeError) that are not caught, which
can crash publish_code_suggestions() instead of skipping the bad suggestion. This violates the
requirement to handle edge cases and errors gracefully for external/untrusted inputs.
Code

pr_agent/git_providers/gerrit_provider.py[R352-357]

+            try:
+                target_path = (repo_root / suggestion["relevant_file"]).resolve()
+                target_path.relative_to(repo_root)
+            except ValueError:
+                get_logger().warning(f"Skipping suggestion with path traversal: {suggestion['relevant_file']}")
+                continue
Evidence
PR Compliance ID 3 requires robust handling of potential errors; the new code only catches
ValueError while calling Path.resolve(), leaving other likely exceptions unhandled and able to
abort processing.

Rule 3: Robust Error Handling
pr_agent/git_providers/gerrit_provider.py[352-357]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`Path.resolve()` in `publish_code_suggestions()` is only guarded by `except ValueError`, but `resolve()` may raise other exceptions (e.g., `OSError`, `RuntimeError`), which can crash suggestion publishing.

## Issue Context
`relevant_file` is externally generated (AI output) and should not be able to abort the whole loop via unexpected filesystem/path-resolution errors.

## Fix Focus Areas
- pr_agent/git_providers/gerrit_provider.py[352-357]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. Partial suggestion validation🐞
Description
publish_code_suggestions() validates only suggestion['relevant_file'] but still directly indexes
body and line-range keys; a suggestion that passes the new check but is missing those keys will
raise KeyError and abort the whole publishing loop. This can break Gerrit suggestion publishing for
all suggestions in the batch, not just the malformed one.
Code

pr_agent/git_providers/gerrit_provider.py[R347-363]

+            # Validate suggestion structure before accessing keys
+            if not isinstance(suggestion, dict) or not isinstance(suggestion.get("relevant_file"), str):
+                get_logger().warning(f"Skipping malformed suggestion: missing or invalid 'relevant_file'")
+                continue
+            # Sanitize file path to prevent directory traversal
+            try:
+                target_path = (repo_root / suggestion["relevant_file"]).resolve()
+                target_path.relative_to(repo_root)
+            except ValueError:
+                get_logger().warning(f"Skipping suggestion with path traversal: {suggestion['relevant_file']}")
+                continue
          description, code = self.split_suggestion(suggestion['body'])
          add_suggestion(
-                pathlib.Path(self.repo_path) / suggestion["relevant_file"],
+                target_path,
              code,
              suggestion["relevant_lines_start"],
              suggestion["relevant_lines_end"],
Evidence
The new guard checks only that suggestion is a dict and that relevant_file is a string, but the
code immediately accesses suggestion['body'], suggestion['relevant_lines_start'], and
suggestion['relevant_lines_end'] without validation, which will raise KeyError if any are missing.
The caller publishes the entire list in one call without wrapping it in a try/except, so an
exception here can interrupt the overall code-suggestion publishing flow.

pr_agent/git_providers/gerrit_provider.py[343-364]
pr_agent/tools/pr_code_suggestions.py[539-579]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`GerritProvider.publish_code_suggestions()` only validates `relevant_file`, but still blindly indexes other required fields (`body`, `relevant_lines_start`, `relevant_lines_end`). If any of these are missing or invalid, the method can raise and stop publishing the rest of the suggestions.
## Issue Context
This method is called with a batch of suggestions; it should skip malformed items rather than aborting the entire batch.
## Fix Focus Areas
- pr_agent/git_providers/gerrit_provider.py[343-374]
- pr_agent/tools/pr_code_suggestions.py[539-579]
## Suggested change
- Extend the validation to require `body` (str) and `relevant_lines_start`/`relevant_lines_end` (ints or int-coercible) before use.
- Optionally wrap the per-suggestion processing block in a `try/except Exception` that logs and `continue`s, so one bad suggestion doesn’t prevent publishing others.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

ⓘ The new review experience is currently in Beta. Learn more

Grey Divider

Grey Divider

Previous review results

Review updated until commit 1dcc22e

Results up to commit N/A


🐞 Bugs (0)  
📘 Rule violations (1)  
📎 Requirement gaps (0)

Grey Divider
Action required
1. Useless f-string in warning 📘
Description
get_logger().warning(f"...") is an f-string with no interpolation, which will trigger Ruff’s
F541 and can fail CI linting. This introduces a repository lint/format violation in newly added
code.
Code

pr_agent/git_providers/gerrit_provider.py[349]

+                get_logger().warning(f"Skipping malformed suggestion: missing or invalid 'relevant_file'")
Evidence
Compliance requires new/modified code to remain compliant with repository linting tools (Ruff). The
added line uses an f-string without any {...} placeholders, which Ruff flags as F541 (f-string
missing placeholders).

pr_agent/git_providers/gerrit_provider.py[349-349]
Best Practice: Learned patterns

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A new `warning()` call uses an f-string without any interpolation (`f"..."`), which Ruff flags (F541) and may fail CI.
## Issue Context
This line logs a static message and does not need f-string formatting.
## Fix Focus Areas
- pr_agent/git_providers/gerrit_provider.py[349-349]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Unvalidated suggestion["relevant_file"] access📘
Description
The new code indexes suggestion["relevant_file"] without checking that suggestion is a dict and
the key exists/is a string, so malformed AI output can raise TypeError/KeyError and break
publishing. This violates the requirement to defensively validate external inputs and handle edge
cases gracefully.
Code

pr_agent/git_providers/gerrit_provider.py[R345-351]

+        repo_root = pathlib.Path(self.repo_path).resolve()
     for suggestion in code_suggestions:
+            # Sanitize file path to prevent directory traversal
+            target_path = (repo_root / suggestion["relevant_file"]).resolve()
+            if not str(target_path).startswith(str(repo_root)):
+                get_logger().warning(f"Skipping suggestion with path traversal: {suggestion['relevant_file']}")
+                continue
Evidence
The checklist requires null/type checks before indexing/calling methods on external inputs; the
added line directly indexes suggestion["relevant_file"] and then operates on it without any
guards, which can raise runtime exceptions and halt the loop.

Rule 3: Robust Error Handling
pr_agent/git_providers/gerrit_provider.py[345-351]
Best Practice: Learned patterns

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`publish_code_suggestions()` assumes every `suggestion` has a string `relevant_file` and indexes it directly. If AI output is missing keys or has wrong types, this can raise `KeyError`/`TypeError` and stop publishing.
## Issue Context
This function processes external/AI-provided structures (`code_suggestions`). Defensive validation is required before indexing and before passing values into `pathlib` operations.
## Fix Focus Areas
- pr_agent/git_providers/gerrit_provider.py[345-351]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Unsafe startswith path containment📘
Description
The repo containment check uses str(target_path).startswith(str(repo_root)), which can be bypassed
by absolute paths sharing the same string prefix (e.g., repo_root=/tmp/repo and
target_path=/tmp/repo2/...). This undermines the intended path traversal protection.
Code

pr_agent/git_providers/gerrit_provider.py[R348-350]

+            target_path = (repo_root / suggestion["relevant_file"]).resolve()
+            if not str(target_path).startswith(str(repo_root)):
+                get_logger().warning(f"Skipping suggestion with path traversal: {suggestion['relevant_file']}")
Evidence
The checklist requires robust handling of edge cases; the added containment logic relies on string
prefix matching rather than proper path relationship checks, which is a known incorrect approach for
filesystem boundary enforcement.

Rule 3: Robust Error Handling
pr_agent/git_providers/gerrit_provider.py[348-350]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The current containment check uses `startswith` on stringified paths, which is not a correct security boundary check and can allow false positives (prefix matches).
## Issue Context
Python 3.12 supports `Path.is_relative_to()`, which should be used for reliable containment checks (or `target_path.relative_to(repo_root)` inside a targeted `try/except ValueError`).
## Fix Focus Areas
- pr_agent/git_providers/gerrit_provider.py[348-350]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended
4. Partial suggestion validation🐞
Description
publish_code_suggestions() validates only suggestion['relevant_file'] but still directly indexes
body and line-range keys; a suggestion that passes the new check but is missing those keys will
raise KeyError and abort the whole publishing loop. This can break Gerrit suggestion publishing for
all suggestions in the batch, not just the malformed one.
Code

pr_agent/git_providers/gerrit_provider.py[R347-363]

+            # Validate suggestion structure before accessing keys
+            if not isinstance(suggestion, dict) or not isinstance(suggestion.get("relevant_file"), str):
+                get_logger().warning(f"Skipping malformed suggestion: missing or invalid 'relevant_file'")
+                continue
+            # Sanitize file path to prevent directory traversal
+            try:
+                target_path = (repo_root / suggestion["relevant_file"]).resolve()
+                target_path.relative_to(repo_root)
+            except ValueError:
+                get_logger().warning(f"Skipping suggestion with path traversal: {suggestion['relevant_file']}")
+                continue
           description, code = self.split_suggestion(suggestion['body'])
           add_suggestion(
-                pathlib.Path(self.repo_path) / suggestion["relevant_file"],
+                target_path,
               code,
               suggestion["relevant_lines_start"],
               suggestion["relevant_lines_end"],
Evidence
The new guard checks only that suggestion is a dict and that relevant_file is a string, but the
code immediately accesses suggestion['body'], suggestion['relevant_lines_start'], and
suggestion['relevant_lines_end'] without validation, which will raise KeyError if any are missing.
The caller publishes the entire list in one call without wrapping it in a try/except, so an
exception here can interrupt the overall code-suggestion publishing flow.

pr_agent/git_providers/gerrit_provider.py[343-364]
pr_agent/tools/pr_code_suggestions.py[539-579]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`GerritProvider.publish_code_suggestions()` only validates `relevant_file`, but still blindly indexes other required fields (`body`, `relevant_lines_start`, `relevant_lines_end`). If any of these are missing or invalid, the method can raise and stop publishing the rest of the suggestions.
## Issue Context
This method is called with a batch of suggestions; it should skip malformed items rather than aborting the entire batch.
## Fix Focus Areas
- pr_agent/git_providers/gerrit_provider.py[343-374]
- pr_agent/tools/pr_code_suggestions.py[539-579]
## Suggested change
- Extend the validation to require `body` (str) and `relevant_lines_start`/`relevant_lines_end` (ints or int-coercible) before use.
- Optionally wrap the per-suggestion processing block in a `try/except Exception` that logs and `continue`s, so one bad suggestion doesn’t prevent publishing others.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider Grey Divider

Qodo Logo

@gvago gvago self-assigned this Apr 14, 2026
@gvago gvago added the bug 🐛 Something is working label Apr 14, 2026
Comment thread pr_agent/git_providers/gerrit_provider.py
Comment thread pr_agent/git_providers/gerrit_provider.py Outdated
@qodo-free-for-open-source-projects
Copy link
Copy Markdown
Contributor

qodo-free-for-open-source-projects bot commented Apr 14, 2026

Persistent review updated to latest commit a9332a2

for suggestion in code_suggestions:
# Validate suggestion structure before accessing keys
if not isinstance(suggestion, dict) or not isinstance(suggestion.get("relevant_file"), str):
get_logger().warning(f"Skipping malformed suggestion: missing or invalid 'relevant_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.

Action required

1. Useless f-string in warning 📘 Rule violation ⚙ Maintainability

get_logger().warning(f"...") is an f-string with no interpolation, which will trigger Ruff’s
F541 and can fail CI linting. This introduces a repository lint/format violation in newly added
code.
Agent Prompt
## Issue description
A new `warning()` call uses an f-string without any interpolation (`f"..."`), which Ruff flags (F541) and may fail CI.

## Issue Context
This line logs a static message and does not need f-string formatting.

## Fix Focus Areas
- pr_agent/git_providers/gerrit_provider.py[349-349]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@qodo-free-for-open-source-projects
Copy link
Copy Markdown
Contributor

qodo-free-for-open-source-projects bot commented Apr 15, 2026

Persistent review updated to latest commit 1dcc22e

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

Labels

bug 🐛 Something is working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant