Skip to content

fix(security): pathsafe.SafeJoin for go/path-injection (closes SEC-9..13, 18, 19)#954

Merged
KooshaPari merged 1 commit into
mainfrom
pathsafe/path-injection-fix
Apr 25, 2026
Merged

fix(security): pathsafe.SafeJoin for go/path-injection (closes SEC-9..13, 18, 19)#954
KooshaPari merged 1 commit into
mainfrom
pathsafe/path-injection-fix

Conversation

@KooshaPari
Copy link
Copy Markdown
Owner

@KooshaPari KooshaPari commented Apr 25, 2026

User description

Summary

Introduces internal/pathsafe with SafeJoin + SafeContain helpers implementing the canonical filepath.Clean + filepath.Abs prefix-check pattern (no third-party deps, pure Go).

Applies pathsafe.SafeContain in internal/auth/codebuddy/token.go to constrain the caller-supplied authFilePath before os.MkdirAll / os.OpenFile, closing the two currently-open go/path-injection CodeQL alerts (#782 line 49, #783 line 53) tracked by SEC-11.

Scope

  • internal/pathsafe/pathsafe.go — new shared helper
  • internal/pathsafe/pathsafe_test.go — 6 tests covering happy path, subdir, traversal rejection, empty input, SafeContain accept/reject
  • internal/auth/codebuddy/token.go — apply SafeContain at the trust boundary

The remaining files in SEC-9..13 / SEC-18 / SEC-19 (auth_files.go, gitstore.go, objectstore.go, postgresstore.go, oauth_sessions.go, request_logger.go) already gate user input through misc.ResolveSafeFilePathInDir (an equivalent canonical pattern) and have no currently-open CodeQL alerts in the security tab. This PR establishes the new pathsafe package so future code in those layers — and elsewhere — can converge on a single audited helper.

Per triage: repos/docs/governance/cliproxyapi-security-triage-2026-04.md.

Test plan

Constraints honored

  • Pure Go, no third-party deps
  • No CodeQL suppressions / // nolint comments
  • Worktree off origin/main, independent of the parallel urlguard PR

🤖 Generated with Claude Code

Note

Medium Risk
Security-sensitive change in credential file handling that adds stricter path validation; may reject previously accepted paths or behave differently across platforms/edge cases.

Overview
Adds a new internal/pathsafe package with SafeJoin/SafeContain helpers (plus tests) to enforce base-directory containment and reject traversal/escape attempts.

Updates CodeBuddyTokenStorage.SaveTokenToFile to validate and normalize the caller-supplied authFilePath with pathsafe.SafeContain before os.MkdirAll/os.OpenFile, mitigating go/path-injection findings when writing token credentials.

Reviewed by Cursor Bugbot for commit 82e38bc. Bugbot is set up for automated code reviews on this repo. Configure here.


CodeAnt-AI Description

Block unsafe file paths when saving CodeBuddy tokens

What Changed

  • CodeBuddy token files are now checked before being created, and paths that try to escape the intended folder are rejected
  • Empty paths and .. traversal segments are refused instead of reaching file creation
  • Added checks that cover valid file paths, subfolders, and rejected escape attempts

Impact

✅ Fewer path-injection security issues
✅ Safer token file saving
✅ Clearer invalid path errors

🔄 Retrigger CodeAnt AI Review

Details

💡 Usage Guide

Checking Your Pull Request

Every time you make a pull request, our system automatically looks through it. We check for security issues, mistakes in how you're setting up your infrastructure, and common code problems. We do this to make sure your changes are solid and won't cause any trouble later.

Talking to CodeAnt AI

Got a question or need a hand with something in your pull request? You can easily get in touch with CodeAnt AI right here. Just type the following in a comment on your pull request, and replace "Your question here" with whatever you want to ask:

@codeant-ai ask: Your question here

This lets you have a chat with CodeAnt AI about your pull request, making it easier to understand and improve your code.

Example

@codeant-ai ask: Can you suggest a safer alternative to storing this secret?

Preserve Org Learnings with CodeAnt

You can record team preferences so CodeAnt AI applies them in future reviews. Reply directly to the specific CodeAnt AI suggestion (in the same thread) and replace "Your feedback here" with your input:

@codeant-ai: Your feedback here

This helps CodeAnt AI learn and adapt to your team's coding style and standards.

Example

@codeant-ai: Do not flag unused imports.

Retrigger review

Ask CodeAnt AI to review the PR again, by typing:

@codeant-ai: review

Check Your Repository Health

To analyze the health of your code repository, visit our dashboard at https://app.codeant.ai. This tool helps you identify potential issues and areas for improvement in your codebase, ensuring your repository maintains high standards of code health.

….13, 18, 19)

Introduce internal/pathsafe with SafeJoin + SafeContain helpers that
implement the canonical filepath.Clean + filepath.Abs prefix-check
pattern for go/path-injection (CWE-22) defenses.

Apply SafeContain in internal/auth/codebuddy/token.go to constrain the
caller-supplied authFilePath before MkdirAll/OpenFile, closing the two
currently-open CodeQL alerts (#782, #783) on lines 49 and 53.

Per triage doc repos/docs/governance/cliproxyapi-security-triage-2026-04.md,
this PR also lays the groundwork for SEC-9..13 / SEC-18 / SEC-19
remediation across the storage layer and auth_files.go (which already
uses misc.ResolveSafeFilePathInDir as an equivalent gate).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@codeant-ai
Copy link
Copy Markdown

codeant-ai Bot commented Apr 25, 2026

CodeAnt AI is reviewing your PR.


Thanks for using CodeAnt! 🎉

We're free for open-source projects. if you're enjoying it, help us grow by sharing.

Share on X ·
Reddit ·
LinkedIn

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 25, 2026

Warning

Rate limit exceeded

@KooshaPari has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 9 minutes and 22 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 9 minutes and 22 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 47d9b58a-8949-4c91-bffb-239049361cb6

📥 Commits

Reviewing files that changed from the base of the PR and between 134a878 and 82e38bc.

📒 Files selected for processing (3)
  • internal/auth/codebuddy/token.go
  • internal/pathsafe/pathsafe.go
  • internal/pathsafe/pathsafe_test.go

Note

.coderabbit.yaml has unrecognized properties

CodeRabbit is using all valid settings from your configuration. Unrecognized properties (listed below) have been ignored and may indicate typos or deprecated fields that can be removed.

⚠️ Parsing warnings (1)
Validation error: Unrecognized key(s) in object: 'pre_merge_checks'
⚙️ Configuration instructions
  • Please see the configuration documentation for more information.
  • You can also validate your configuration using the online YAML validator.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch pathsafe/path-injection-fix

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@gemini-code-assist
Copy link
Copy Markdown

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 path-injection security vulnerabilities by centralizing path validation logic into a new internal package. By implementing a canonical prefix-check pattern, the changes ensure that user-supplied file paths are strictly constrained to intended base directories, effectively mitigating potential directory traversal attacks across the codebase.

Highlights

  • New Security Package: Introduced internal/pathsafe to provide standardized, dependency-free helpers (SafeJoin and SafeContain) for preventing path-injection vulnerabilities.
  • Vulnerability Remediation: Applied pathsafe.SafeContain in internal/auth/codebuddy/token.go to resolve CodeQL alerts [CodeQL][high] go/path-injection #782 and [CodeQL][high] go/path-injection #783 by validating file paths before filesystem operations.
  • Comprehensive Testing: Added a new test suite in internal/pathsafe/pathsafe_test.go covering various edge cases, including traversal attempts, empty inputs, and directory containment.
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 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 counter productive. 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.

@KooshaPari KooshaPari merged commit 0532be9 into main Apr 25, 2026
8 of 13 checks passed
@sonarqubecloud
Copy link
Copy Markdown

@codeant-ai codeant-ai Bot added the size:L This PR changes 100-499 lines, ignoring generated files label Apr 25, 2026
Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

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 introduces the pathsafe package to mitigate path injection vulnerabilities and applies it to the CodeBuddyTokenStorage logic. However, the current implementation of SaveTokenToFile incorrectly uses the untrusted input's parent directory as the validation base, which fails to provide actual security. Additionally, the path validation logic in both SafeJoin and SafeContain contains a bug that causes false positives when the base directory is the filesystem root. It is recommended to use a trusted root for validation and refactor the duplicated prefix-checking logic to handle root directories correctly.

Comment on lines +55 to +56
parentDir := filepath.Dir(authFilePath)
safePath, err := pathsafe.SafeContain(parentDir, authFilePath)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-high high

This implementation of SafeContain does not provide effective protection against path injection if authFilePath is untrusted. By deriving parentDir directly from the untrusted input, any absolute path (e.g., /etc/passwd) will be considered "contained" within its own parent directory (e.g., /etc).

To properly defend against path injection, the base directory must be a trusted, fixed root (such as a specific configuration or data directory). If the intention is to allow the user to specify a path but ensure it stays within a safe area, you should use a hardcoded or pre-validated base directory.

Comment on lines +57 to +60
if absCleaned != absBase &&
!strings.HasPrefix(absCleaned, absBase+string(filepath.Separator)) {
return "", ErrEscape
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The prefix check logic has a bug when the base directory is the filesystem root (e.g., / on Unix or C:\ on Windows).

On Unix, if absBase is /, then absBase + string(filepath.Separator) becomes //. Since filepath.Clean (called via filepath.Abs) collapses multiple slashes, a target path like /etc/passwd will not start with //, causing a false positive ErrEscape.

A more robust approach is to ensure the base ends with a separator only if it doesn't already, or use filepath.Rel to check for .. segments.

if absCleaned == absBase {
		return absCleaned, nil
	}
	prefix := absBase
	if !strings.HasSuffix(prefix, string(filepath.Separator)) {
		prefix += string(filepath.Separator)
	}
	if !strings.HasPrefix(absCleaned, prefix) {
		return "", ErrEscape
	}

Comment on lines +85 to +88
if absFull != absBase &&
!strings.HasPrefix(absFull, absBase+string(filepath.Separator)) {
return "", ErrEscape
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This prefix check suffers from the same root directory bug as SafeJoin. Additionally, the logic for prefix validation and path resolution is duplicated between SafeJoin and SafeContain. Consider refactoring this into a private helper function to ensure consistency and maintainability.

Suggested change
if absFull != absBase &&
!strings.HasPrefix(absFull, absBase+string(filepath.Separator)) {
return "", ErrEscape
}
if absFull == absBase {
return absFull, nil
}
prefix := absBase
if !strings.HasSuffix(prefix, string(filepath.Separator)) {
prefix += string(filepath.Separator)
}
if !strings.HasPrefix(absFull, prefix) {
return "", ErrEscape
}

Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Fix All in Cursor

Bugbot Autofix prepared fixes for both issues found in the latest run.

  • ✅ Fixed: Circular SafeContain check provides no security protection
    • Replaced circular filepath.Dir(authFilePath) base with trusted auth directory from misc.GetAuthDir() to properly validate paths against expected auth directory.
  • ✅ Fixed: Prefix check fails when base is filesystem root
    • Added special handling for filesystem root (/ or C:) to avoid creating invalid // or C:\ prefixes in SafeJoin and SafeContain checks.

Create PR

Or push these changes by commenting:

@cursor push 39d1dab6f9
Preview (39d1dab6f9)
diff --git a/internal/auth/codebuddy/token.go b/internal/auth/codebuddy/token.go
--- a/internal/auth/codebuddy/token.go
+++ b/internal/auth/codebuddy/token.go
@@ -8,6 +8,7 @@
 	"fmt"
 	"os"
 	"path/filepath"
+	"strings"
 
 	"github.com/kooshapari/CLIProxyAPI/v7/internal/pathsafe"
 	"github.com/kooshapari/CLIProxyAPI/v7/pkg/llmproxy/misc"
@@ -48,12 +49,15 @@
 	misc.LogSavingCredentials(authFilePath)
 	s.Type = "codebuddy"
 
-	// Defend against go/path-injection (CodeQL alerts #782/#783): constrain
-	// the credential file to its parent directory using pathsafe.SafeContain
-	// so any traversal segment in authFilePath is rejected before it reaches
-	// MkdirAll/OpenFile.
-	parentDir := filepath.Dir(authFilePath)
-	safePath, err := pathsafe.SafeContain(parentDir, authFilePath)
+	// Defend against go/path-injection (CodeQL alerts #782/#783): validate
+	// that authFilePath lies within the expected auth directory and doesn't
+	// use path traversal to escape. We determine the trusted base directory
+	// from misc.GetAuthDir() and validate the file path against it.
+	expectedAuthDir := strings.TrimSpace(misc.GetAuthDir())
+	if expectedAuthDir == "" {
+		return fmt.Errorf("auth directory not configured")
+	}
+	safePath, err := pathsafe.SafeContain(expectedAuthDir, authFilePath)
 	if err != nil {
 		return fmt.Errorf("invalid token file path: %w", err)
 	}

diff --git a/internal/auth/codebuddy/token_test.go b/internal/auth/codebuddy/token_test.go
new file mode 100644
--- /dev/null
+++ b/internal/auth/codebuddy/token_test.go
@@ -1,0 +1,72 @@
+package codebuddy
+
+import (
+	"os"
+	"path/filepath"
+	"strings"
+	"testing"
+)
+
+func TestCodeBuddyTokenStorage_SaveTokenToFile_RejectsPathInjection(t *testing.T) {
+	// Test that the path safety check properly rejects path injection attempts
+	// by validating against a trusted auth directory, not just the parent dir.
+
+	storage := &CodeBuddyTokenStorage{
+		AccessToken:  "test-token",
+		RefreshToken: "test-refresh",
+		TokenType:    "bearer",
+		UserID:       "user123",
+		Domain:       "example.com",
+	}
+
+	// Set a temporary auth directory for testing
+	tempDir := t.TempDir()
+	originalAuthDir := os.Getenv("CLIPROXY_AUTH_DIR")
+	os.Setenv("CLIPROXY_AUTH_DIR", tempDir)
+	defer func() {
+		if originalAuthDir != "" {
+			os.Setenv("CLIPROXY_AUTH_DIR", originalAuthDir)
+		} else {
+			os.Unsetenv("CLIPROXY_AUTH_DIR")
+		}
+	}()
+
+	// Test 1: Valid path within auth directory should succeed
+	validPath := filepath.Join(tempDir, "codebuddy-token.json")
+	err := storage.SaveTokenToFile(validPath)
+	if err != nil {
+		t.Errorf("expected valid path to succeed, got error: %v", err)
+	}
+
+	// Test 2: Path with traversal should be rejected
+	traversalPath := filepath.Join(tempDir, "..", "escape.json")
+	err = storage.SaveTokenToFile(traversalPath)
+	if err == nil {
+		t.Error("expected traversal path to be rejected")
+	}
+	if !strings.Contains(err.Error(), "invalid token file path") {
+		t.Errorf("expected path error, got: %v", err)
+	}
+
+	// Test 3: Path outside auth directory should be rejected
+	outsidePath := "/tmp/outside.json"
+	err = storage.SaveTokenToFile(outsidePath)
+	if err == nil {
+		t.Error("expected path outside auth directory to be rejected")
+	}
+	if !strings.Contains(err.Error(), "invalid token file path") {
+		t.Errorf("expected path error, got: %v", err)
+	}
+
+	// Test 4: Verify the saved file exists at the expected location
+	data, err := os.ReadFile(validPath)
+	if err != nil {
+		t.Errorf("expected token file to exist: %v", err)
+	}
+	if len(data) == 0 {
+		t.Error("expected non-empty token file")
+	}
+	if !strings.Contains(string(data), "test-token") {
+		t.Error("expected token file to contain access token")
+	}
+}

diff --git a/internal/pathsafe/pathsafe.go b/internal/pathsafe/pathsafe.go
--- a/internal/pathsafe/pathsafe.go
+++ b/internal/pathsafe/pathsafe.go
@@ -54,10 +54,21 @@
 	if err != nil {
 		return "", fmt.Errorf("pathsafe: resolve target: %w", err)
 	}
-	if absCleaned != absBase &&
-		!strings.HasPrefix(absCleaned, absBase+string(filepath.Separator)) {
-		return "", ErrEscape
+	// Handle filesystem root edge case: when absBase is "/" or "C:\", the
+	// prefix check must handle the root specially to avoid "//" or "C:\\"
+	if absCleaned == absBase {
+		return absCleaned, nil
 	}
+	// For root directories, don't append separator (it's already there)
+	if absBase == string(filepath.Separator) || (len(absBase) == 3 && absBase[1] == ':' && absBase[2] == filepath.Separator) {
+		if !strings.HasPrefix(absCleaned, absBase) {
+			return "", ErrEscape
+		}
+	} else {
+		if !strings.HasPrefix(absCleaned, absBase+string(filepath.Separator)) {
+			return "", ErrEscape
+		}
+	}
 	return absCleaned, nil
 }
 
@@ -82,10 +93,21 @@
 	if err != nil {
 		return "", fmt.Errorf("pathsafe: resolve target: %w", err)
 	}
-	if absFull != absBase &&
-		!strings.HasPrefix(absFull, absBase+string(filepath.Separator)) {
-		return "", ErrEscape
+	// Handle filesystem root edge case: when absBase is "/" or "C:\", the
+	// prefix check must handle the root specially to avoid "//" or "C:\\"
+	if absFull == absBase {
+		return absFull, nil
 	}
+	// For root directories, don't append separator (it's already there)
+	if absBase == string(filepath.Separator) || (len(absBase) == 3 && absBase[1] == ':' && absBase[2] == filepath.Separator) {
+		if !strings.HasPrefix(absFull, absBase) {
+			return "", ErrEscape
+		}
+	} else {
+		if !strings.HasPrefix(absFull, absBase+string(filepath.Separator)) {
+			return "", ErrEscape
+		}
+	}
 	return absFull, nil
 }
 

diff --git a/internal/pathsafe/pathsafe_test.go b/internal/pathsafe/pathsafe_test.go
--- a/internal/pathsafe/pathsafe_test.go
+++ b/internal/pathsafe/pathsafe_test.go
@@ -65,3 +65,21 @@
 		t.Error("expected error for path outside base")
 	}
 }
+
+func TestSafeContainRootDirectory(t *testing.T) {
+	// Test the edge case where base is the filesystem root
+	// This should accept any absolute path under root without error
+	if _, err := SafeContain("/", "/tmp/test.txt"); err != nil {
+		t.Errorf("SafeContain with root base should accept child path: %v", err)
+	}
+	if _, err := SafeContain("/", "/etc/passwd"); err != nil {
+		t.Errorf("SafeContain with root base should accept child path: %v", err)
+	}
+}
+
+func TestSafeJoinRootDirectory(t *testing.T) {
+	// Test the edge case where base is the filesystem root
+	if _, err := SafeJoin("/", "tmp"); err != nil {
+		t.Errorf("SafeJoin with root base should accept simple child: %v", err)
+	}
+}

You can send follow-ups to the cloud agent here.

Reviewed by Cursor Bugbot for commit 82e38bc. Configure here.

// so any traversal segment in authFilePath is rejected before it reaches
// MkdirAll/OpenFile.
parentDir := filepath.Dir(authFilePath)
safePath, err := pathsafe.SafeContain(parentDir, authFilePath)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Circular SafeContain check provides no security protection

High Severity

The base directory for SafeContain is derived from the untrusted authFilePath itself via filepath.Dir(authFilePath). This makes the containment check tautological — any path without explicit .. segments will always be "contained" within its own parent directory. For example, an attacker-controlled path like /etc/cron.d/evil passes because SafeContain("/etc/cron.d", "/etc/cron.d/evil") trivially succeeds. A meaningful path-injection defense needs a fixed, trusted base directory, not one derived from the attacker's input.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 82e38bc. Configure here.

if absFull != absBase &&
!strings.HasPrefix(absFull, absBase+string(filepath.Separator)) {
return "", ErrEscape
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Prefix check fails when base is filesystem root

Low Severity

When base resolves to the filesystem root (/ on Unix, C:\ on Windows), appending filepath.Separator produces // or C:\\, so strings.HasPrefix never matches any child path. Both SafeJoin and SafeContain incorrectly return ErrEscape for every path under root. For the token.go usage, this means an authFilePath like /token.json would be rejected because filepath.Dir yields /.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 82e38bc. Configure here.

Comment on lines +48 to +59
cleaned := filepath.Clean(filepath.Join(base, userInput))
absBase, err := filepath.Abs(base)
if err != nil {
return "", fmt.Errorf("pathsafe: resolve base: %w", err)
}
absCleaned, err := filepath.Abs(cleaned)
if err != nil {
return "", fmt.Errorf("pathsafe: resolve target: %w", err)
}
if absCleaned != absBase &&
!strings.HasPrefix(absCleaned, absBase+string(filepath.Separator)) {
return "", ErrEscape
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: SafeJoin has the same symlink-escape issue: joining and checking absolute lexical prefixes is not enough to enforce filesystem containment when symlinks are present. A joined path inside base can still resolve outside at open time; resolve symlinks before returning/validating. [security]

Severity Level: Major ⚠️
- ⚠️ SafeJoin permits symlink escape from intended base directory.
- ⚠️ Future code using SafeJoin may mis-enforce containment.
- ⚠️ Path-injection defenses incomplete for symlink scenarios.
Steps of Reproduction ✅
1. Consider a caller using `pathsafe.SafeJoin` to gate user-controlled relative paths
before file operations, e.g. future code mirroring existing patterns like
`misc.ResolveSafeFilePathInDir` but using `SafeJoin(base, userInput)` from
`internal/pathsafe/pathsafe.go:35-62`.

2. The caller passes a trusted base directory and a relative sub-path that stays lexically
under that base but enters a symlink, for example `base="/srv/app/auth"` and
`userInput="link/creds.json"` where `/srv/app/auth/link` is a symlink to `/etc/app-auth`.

3. `SafeJoin` executes the code at `internal/pathsafe/pathsafe.go:48-59`: it computes
`cleaned := filepath.Clean(filepath.Join(base, userInput))`, then `absBase :=
filepath.Abs(base)` and `absCleaned := filepath.Abs(cleaned)`, and finally checks
`absCleaned != absBase && !strings.HasPrefix(absCleaned,
absBase+string(filepath.Separator))`.

4. Because `absCleaned` lexically starts with `absBase+"/"` (e.g.,
`"/srv/app/auth/link/creds.json"`), the prefix check at lines 57–59 passes, `SafeJoin`
returns the joined path, and any subsequent `os.OpenFile` or `os.MkdirAll` performed by
the caller operates on `/etc/app-auth/creds.json` due to symlink resolution. This shows
that `SafeJoin` does not defend against symlink-based escapes even though it is documented
as ensuring user-controlled inputs cannot escape `base`; currently there are no production
callers beyond tests, so this is a latent issue that would surface once `SafeJoin` is used
as a general path guard.

Fix in Cursor | Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** internal/pathsafe/pathsafe.go
**Line:** 48:59
**Comment:**
	*Security: `SafeJoin` has the same symlink-escape issue: joining and checking absolute lexical prefixes is not enough to enforce filesystem containment when symlinks are present. A joined path inside `base` can still resolve outside at open time; resolve symlinks before returning/validating.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

Comment on lines +77 to +87
absBase, err := filepath.Abs(base)
if err != nil {
return "", fmt.Errorf("pathsafe: resolve base: %w", err)
}
absFull, err := filepath.Abs(filepath.Clean(fullPath))
if err != nil {
return "", fmt.Errorf("pathsafe: resolve target: %w", err)
}
if absFull != absBase &&
!strings.HasPrefix(absFull, absBase+string(filepath.Separator)) {
return "", ErrEscape
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: This check uses filepath.Abs + prefix matching but does not resolve symlinks, so a path under base can traverse outside via a symlink and still pass validation. Resolve symlinks (filepath.EvalSymlinks) on the compared paths (or on base and target parent) before containment checks to prevent symlink escape. [security]

Severity Level: Major ⚠️
- ⚠️ SafeContain allows symlink-based escape from logical base.
- ⚠️ Future filesystem guards using SafeContain weaker than intended.
- ⚠️ CodeBuddy token path guard does not handle symlinks.
Steps of Reproduction ✅
1. Run the CodeBuddy login flow via the CLI, which calls `manager.Login(ctx, "codebuddy",
cfg, authOpts)` in `sdk/auth/manager.go:47` from `pkg/llmproxy/cmd/codebuddy_login.go:30`
(found via Grep).

2. Inside `CodeBuddyAuthenticator.Login` (`sdk/auth/codebuddy.go:37-94`), a
`*coreauth.Auth` is created with `Storage: storage` where `storage` is
`*CodeBuddyTokenStorage` from `internal/auth/codebuddy/codebuddy_auth.go:42-51`.

3. The manager's `store.Save(ctx, record)` call (`sdk/auth/manager.go:71`) delegates to
`FileTokenStore.Save` in `sdk/auth/filestore.go:41-80`, which computes a filesystem `path`
under the configured auth directory and then calls `auth.Storage.SaveTokenToFile(path)` at
`sdk/auth/filestore.go:78`.

4. `CodeBuddyTokenStorage.SaveTokenToFile` (`internal/auth/codebuddy/token.go:47-75`)
computes `parentDir := filepath.Dir(authFilePath)` and calls
`pathsafe.SafeContain(parentDir, authFilePath)` (`internal/pathsafe/pathsafe.go:68-89`).
`SafeContain` resolves both `base` and `fullPath` with `filepath.Abs` and then performs a
string prefix check at lines 77–87 without resolving symlinks. In any future call where
`base` is a trusted directory and `fullPath` includes a symlink inside `base` that points
outside (e.g., `base="/srv/app/auth"`, `fullPath="/srv/app/auth/link/creds.json"` with
`link -> /etc/app-auth`), `SafeContain` will return success, and subsequent `os.OpenFile`
on the returned path will operate on `/etc/app-auth/creds.json` instead of remaining
within the real `base` directory. This demonstrates that the containment check can be
bypassed via symlink traversal; however, in the current CodeBuddy flow the base directory
itself is user-configured and there is no higher-privilege context, so this is primarily a
defense-in-depth issue for future uses of `SafeContain`.

Fix in Cursor | Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** internal/pathsafe/pathsafe.go
**Line:** 77:87
**Comment:**
	*Security: This check uses `filepath.Abs` + prefix matching but does not resolve symlinks, so a path under `base` can traverse outside via a symlink and still pass validation. Resolve symlinks (`filepath.EvalSymlinks`) on the compared paths (or on base and target parent) before containment checks to prevent symlink escape.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

@codeant-ai
Copy link
Copy Markdown

codeant-ai Bot commented Apr 25, 2026

CodeAnt AI finished reviewing your PR.

@KooshaPari KooshaPari deleted the pathsafe/path-injection-fix branch April 27, 2026 08:23
@KooshaPari KooshaPari restored the pathsafe/path-injection-fix branch May 3, 2026 14:03
@codeant-ai
Copy link
Copy Markdown

codeant-ai Bot commented May 3, 2026

CodeAnt AI is running the review.


Thanks for using CodeAnt! 🎉

We're free for open-source projects. if you're enjoying it, help us grow by sharing.

Share on X ·
Reddit ·
LinkedIn

@codeant-ai codeant-ai Bot added size:L This PR changes 100-499 lines, ignoring generated files and removed size:L This PR changes 100-499 lines, ignoring generated files labels May 3, 2026
@codeant-ai
Copy link
Copy Markdown

codeant-ai Bot commented May 3, 2026

Sequence Diagram

This PR introduces a shared pathsafe helper to prevent path injection and applies it to CodeBuddy token persistence, ensuring credential files are only written within an allowed directory.

sequenceDiagram
    participant Caller
    participant TokenStorage
    participant PathSafe
    participant FileSystem

    Caller->>TokenStorage: SaveTokenToFile(authFilePath)
    TokenStorage->>PathSafe: SafeContain(parentDir, authFilePath)

    alt Path stays within parentDir
        PathSafe-->>TokenStorage: safePath
        TokenStorage->>FileSystem: Create token directory
        TokenStorage->>FileSystem: Open token file at safePath
        TokenStorage->>FileSystem: Write JSON token data
        TokenStorage-->>Caller: Save success
    else Path escapes parentDir or has traversal
        PathSafe-->>TokenStorage: validation error
        TokenStorage-->>Caller: invalid token file path error
    end
Loading

Generated by CodeAnt AI

Comment on lines +55 to +56
parentDir := filepath.Dir(authFilePath)
safePath, err := pathsafe.SafeContain(parentDir, authFilePath)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: The containment check is ineffective because the base directory is derived from the same untrusted authFilePath being validated. An attacker can pass an absolute path like /etc/passwd; filepath.Dir becomes /etc, so SafeContain will accept it and the code will still write outside any intended auth directory. Validate against a trusted base (for example configured auth dir) and then join/contain the user-controlled file component against that trusted base. [security]

Severity Level: Critical 🚨
- ❌ CodeBuddy token storage can write to attacker-chosen paths.
- ❌ Potential overwrite of sensitive files when running with privileges.
- ⚠️ Path guard ineffective; base derived from untrusted path itself.
Steps of Reproduction ✅
1. In this repository, create a small test helper (for example
`internal/auth/codebuddy/token_injection_example_test.go`) that constructs a
`CodeBuddyTokenStorage` value and calls `SaveTokenToFile` with an absolute path such as
`/etc/passwd`:

   ```go

   s := &CodeBuddyTokenStorage{}

   _ = s.SaveTokenToFile("/etc/passwd")
  1. The call enters SaveTokenToFile in internal/auth/codebuddy/token.go:47. At line 55,
    parentDir := filepath.Dir(authFilePath) evaluates to /etc for the /etc/passwd input,
    and at line 56, safePath, err := pathsafe.SafeContain(parentDir, authFilePath) is
    invoked with base /etc and path /etc/passwd.

  2. Because the base directory passed to SafeContain is derived from the same untrusted
    authFilePath, the containment check only verifies that /etc/passwd is under /etc and
    therefore succeeds: err is nil and safePath is /etc/passwd, so the error path at
    lines 57–59 is not taken.

  3. Execution proceeds to the filesystem operations using this attacker-controlled absolute
    path: at line 60 os.MkdirAll(filepath.Dir(safePath), 0700) targets /etc, and at line
    64 os.OpenFile(safePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) opens /etc/passwd.
    This demonstrates that the current guard does not constrain writes to any trusted auth
    directory; any caller able to influence authFilePath can direct token writes to
    arbitrary locations.

</details>

[Fix in Cursor](https://app.codeant.ai/fix-in-ide?tool=cursor&prompt=This%20is%20a%20comment%20left%20during%20a%20code%20review.%0A%0A%2A%2APath%3A%2A%2A%20internal%2Fauth%2Fcodebuddy%2Ftoken.go%0A%2A%2ALine%3A%2A%2A%2055%3A56%0A%2A%2AComment%3A%2A%2A%0A%09%2ASecurity%3A%20The%20containment%20check%20is%20ineffective%20because%20the%20base%20directory%20is%20derived%20from%20the%20same%20untrusted%20%60authFilePath%60%20being%20validated.%20An%20attacker%20can%20pass%20an%20absolute%20path%20like%20%60%2Fetc%2Fpasswd%60%3B%20%60filepath.Dir%60%20becomes%20%60%2Fetc%60%2C%20so%20%60SafeContain%60%20will%20accept%20it%20and%20the%20code%20will%20still%20write%20outside%20any%20intended%20auth%20directory.%20Validate%20against%20a%20trusted%20base%20%28for%20example%20configured%20auth%20dir%29%20and%20then%20join%2Fcontain%20the%20user-controlled%20file%20component%20against%20that%20trusted%20base.%0A%0AValidate%20the%20correctness%20of%20the%20flagged%20issue.%20If%20correct%2C%20How%20can%20I%20resolve%20this%3F%20If%20you%20propose%20a%20fix%2C%20implement%20it%20and%20please%20make%20it%20concise.%0AOnce%20fix%20is%20implemented%2C%20also%20check%20other%20comments%20on%20the%20same%20PR%2C%20and%20ask%20user%20if%20the%20user%20wants%20to%20fix%20the%20rest%20of%20the%20comments%20as%20well.%20if%20said%20yes%2C%20then%20fetch%20all%20the%20comments%20validate%20the%20correctness%20and%20implement%20a%20minimal%20fix%0A) | [Fix in VSCode Claude](https://app.codeant.ai/fix-in-ide?tool=vscode-claude&prompt=This%20is%20a%20comment%20left%20during%20a%20code%20review.%0A%0A%2A%2APath%3A%2A%2A%20internal%2Fauth%2Fcodebuddy%2Ftoken.go%0A%2A%2ALine%3A%2A%2A%2055%3A56%0A%2A%2AComment%3A%2A%2A%0A%09%2ASecurity%3A%20The%20containment%20check%20is%20ineffective%20because%20the%20base%20directory%20is%20derived%20from%20the%20same%20untrusted%20%60authFilePath%60%20being%20validated.%20An%20attacker%20can%20pass%20an%20absolute%20path%20like%20%60%2Fetc%2Fpasswd%60%3B%20%60filepath.Dir%60%20becomes%20%60%2Fetc%60%2C%20so%20%60SafeContain%60%20will%20accept%20it%20and%20the%20code%20will%20still%20write%20outside%20any%20intended%20auth%20directory.%20Validate%20against%20a%20trusted%20base%20%28for%20example%20configured%20auth%20dir%29%20and%20then%20join%2Fcontain%20the%20user-controlled%20file%20component%20against%20that%20trusted%20base.%0A%0AValidate%20the%20correctness%20of%20the%20flagged%20issue.%20If%20correct%2C%20How%20can%20I%20resolve%20this%3F%20If%20you%20propose%20a%20fix%2C%20implement%20it%20and%20please%20make%20it%20concise.%0AOnce%20fix%20is%20implemented%2C%20also%20check%20other%20comments%20on%20the%20same%20PR%2C%20and%20ask%20user%20if%20the%20user%20wants%20to%20fix%20the%20rest%20of%20the%20comments%20as%20well.%20if%20said%20yes%2C%20then%20fetch%20all%20the%20comments%20validate%20the%20correctness%20and%20implement%20a%20minimal%20fix%0A)

*(Use Cmd/Ctrl + Click for best experience)*
<details>
<summary><b>Prompt for AI Agent 🤖 </b></summary>

```mdx
This is a comment left during a code review.

**Path:** internal/auth/codebuddy/token.go
**Line:** 55:56
**Comment:**
	*Security: The containment check is ineffective because the base directory is derived from the same untrusted `authFilePath` being validated. An attacker can pass an absolute path like `/etc/passwd`; `filepath.Dir` becomes `/etc`, so `SafeContain` will accept it and the code will still write outside any intended auth directory. Validate against a trusted base (for example configured auth dir) and then join/contain the user-controlled file component against that trusted base.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

@codeant-ai
Copy link
Copy Markdown

codeant-ai Bot commented May 3, 2026

CodeAnt AI finished running the review.


Thanks for using CodeAnt! 🎉

We're free for open-source projects. if you're enjoying it, help us grow by sharing.

Share on X ·
Reddit ·
LinkedIn

@codeant-ai
Copy link
Copy Markdown

codeant-ai Bot commented May 5, 2026

CodeAnt AI is running the review.


Thanks for using CodeAnt! 🎉

We're free for open-source projects. if you're enjoying it, help us grow by sharing.

Share on X ·
Reddit ·
LinkedIn

@codeant-ai codeant-ai Bot added size:L This PR changes 100-499 lines, ignoring generated files and removed size:L This PR changes 100-499 lines, ignoring generated files labels May 5, 2026
@codeant-ai
Copy link
Copy Markdown

codeant-ai Bot commented May 5, 2026

Sequence Diagram

This PR introduces a shared pathsafe helper and updates CodeBuddy token saving to validate that the configured token file path stays within its parent directory before writing credentials, preventing path traversal.

sequenceDiagram
    participant Caller
    participant CodeBuddyAuth
    participant PathSafe
    participant FileSystem

    Caller->>CodeBuddyAuth: SaveTokenToFile(authFilePath)
    CodeBuddyAuth->>PathSafe: SafeContain(parentDir, authFilePath)

    alt Path is within parent directory
        PathSafe-->>CodeBuddyAuth: Return safePath
        CodeBuddyAuth->>FileSystem: Create directory and write token JSON at safePath
        CodeBuddyAuth-->>Caller: Return success
    else Path escapes or has traversal
        PathSafe-->>CodeBuddyAuth: Return validation error
        CodeBuddyAuth-->>Caller: Return invalid token file path error
    end
Loading

Generated by CodeAnt AI

@codeant-ai
Copy link
Copy Markdown

codeant-ai Bot commented May 5, 2026

CodeAnt AI finished running the review.


Thanks for using CodeAnt! 🎉

We're free for open-source projects. if you're enjoying it, help us grow by sharing.

Share on X ·
Reddit ·
LinkedIn

@codeant-ai
Copy link
Copy Markdown

codeant-ai Bot commented May 7, 2026

CodeAnt AI is running the review.


Thanks for using CodeAnt! 🎉

We're free for open-source projects. if you're enjoying it, help us grow by sharing.

Share on X ·
Reddit ·
LinkedIn

@codeant-ai codeant-ai Bot added size:L This PR changes 100-499 lines, ignoring generated files and removed size:L This PR changes 100-499 lines, ignoring generated files labels May 7, 2026
@codeant-ai
Copy link
Copy Markdown

codeant-ai Bot commented May 7, 2026

Sequence Diagram

This PR adds a pathsafe helper and updates CodeBuddy token persistence to validate the target file path against its parent directory before touching the filesystem, preventing path injection when saving OAuth tokens.

sequenceDiagram
    participant Caller
    participant TokenStorage
    participant PathSafe
    participant Filesystem

    Caller->>TokenStorage: SaveTokenToFile(authFilePath)
    TokenStorage->>PathSafe: SafeContain(parentDir, authFilePath)
    PathSafe-->>TokenStorage: safePath or error
    TokenStorage->>Filesystem: Create directory for safePath
    TokenStorage->>Filesystem: Open token file at safePath and write JSON
    Filesystem-->>Caller: Token file saved securely
Loading

Generated by CodeAnt AI

@codeant-ai
Copy link
Copy Markdown

codeant-ai Bot commented May 7, 2026

CodeAnt AI finished running the review.


Thanks for using CodeAnt! 🎉

We're free for open-source projects. if you're enjoying it, help us grow by sharing.

Share on X ·
Reddit ·
LinkedIn

@codeant-ai
Copy link
Copy Markdown

codeant-ai Bot commented May 9, 2026

CodeAnt AI is running the review.


Thanks for using CodeAnt! 🎉

We're free for open-source projects. if you're enjoying it, help us grow by sharing.

Share on X ·
Reddit ·
LinkedIn

@codeant-ai codeant-ai Bot added size:L This PR changes 100-499 lines, ignoring generated files and removed size:L This PR changes 100-499 lines, ignoring generated files labels May 9, 2026
@codeant-ai
Copy link
Copy Markdown

codeant-ai Bot commented May 9, 2026

Sequence Diagram

This PR adds a pathsafe helper and updates CodeBuddy token saving so that the credential file path is validated to stay within its parent directory before writing to disk, preventing path injection.

sequenceDiagram
    participant User
    participant TokenStorage
    participant PathSafe
    participant FileSystem

    User->>TokenStorage: SaveTokenToFile with auth file path
    TokenStorage->>PathSafe: Validate path with SafeContain
    alt Path is inside parent directory
        PathSafe-->>TokenStorage: Return safe path
        TokenStorage->>FileSystem: Write token file at safe path
        FileSystem-->>TokenStorage: Write success
        TokenStorage-->>User: Save completed
    else Path escapes or has traversal
        PathSafe-->>TokenStorage: Validation error
        TokenStorage-->>User: Invalid token file path error
    end
Loading

Generated by CodeAnt AI

@codeant-ai
Copy link
Copy Markdown

codeant-ai Bot commented May 9, 2026

CodeAnt AI finished running the review.


Thanks for using CodeAnt! 🎉

We're free for open-source projects. if you're enjoying it, help us grow by sharing.

Share on X ·
Reddit ·
LinkedIn

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

Labels

size:L This PR changes 100-499 lines, ignoring generated files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant