Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion internal/shell/clink.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package shell

import (
"fmt"
"strings"

"github.com/version-fox/vfox/internal/env"
)
Expand Down Expand Up @@ -48,5 +49,16 @@ func (b clink) Export(envs env.Vars) (out string) {
}

func (b clink) set(key, value string) string {
return fmt.Sprintf("set \"%s=%s\"\n", key, value)
return fmt.Sprintf("set \"%s=%s\"\n", escapeCmdSet(key), escapeCmdSet(value))
}

func escapeCmdSet(value string) string {
value = strings.ReplaceAll(value, "\r", " ")
value = strings.ReplaceAll(value, "\n", " ")
return strings.NewReplacer(
"^", "^^",
"\"", "^\"",
"%", "%%",
"!", "^!",
).Replace(value)
Comment on lines 51 to +63
Copy link

Copilot AI Apr 29, 2026

Choose a reason for hiding this comment

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

clink.set() previously interpolated key and value directly into set "KEY=VALUE", which is a common injection vector in cmd/batch contexts (quotes/newlines/%/delayed expansion). Please add unit tests covering escapeCmdSet() for cases like embedded ", %VAR%, !VAR! (delayed expansion), and CR/LF to ensure the generated activation script can't be broken out of.

Copilot uses AI. Check for mistakes.
Comment on lines +55 to +63
Copy link

Copilot AI Apr 29, 2026

Choose a reason for hiding this comment

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

escapeCmdSet() allocates a new strings.Replacer on every call. Since Export() may run frequently and processes multiple env vars, consider moving the replacer to a package-level var (and just calling .Replace) to reduce per-export allocations.

Copilot uses AI. Check for mistakes.
}
6 changes: 0 additions & 6 deletions internal/shell/zsh.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,6 @@
package shell

import (
"fmt"

"github.com/version-fox/vfox/internal/env"
)

Expand Down Expand Up @@ -76,10 +74,6 @@ func (z zsh) Export(envs env.Vars) (out string) {
}

func (z zsh) export(key, value string) string {
// Use double quotes for PATH-like variables to avoid unnecessary ANSI-C quoting
if key == "PATH" {
return fmt.Sprintf("export %s=\"%s\";", key, value)
}
return "export " + z.escape(key) + "=" + z.escape(value) + ";"
}
Comment on lines 76 to 78
Copy link

Copilot AI Apr 29, 2026

Choose a reason for hiding this comment

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

This change fixes PATH escaping for zsh, but the same unescaped PATH interpolation pattern still exists in the bash shell exporter (internal/shell/bash.go:75-80 uses export PATH="%s";). That leaves the overall activation surface vulnerable for bash users; consider applying the same escaping approach there (or consolidating PATH handling across bash/zsh to avoid drift).

Copilot uses AI. Check for mistakes.

Expand Down