Skip to content

Dev to main v0.2#32

Merged
r4ulcl merged 113 commits intomainfrom
dev
Jan 20, 2026
Merged

Dev to main v0.2#32
r4ulcl merged 113 commits intomainfrom
dev

Conversation

@r4ulcl
Copy link
Copy Markdown
Owner

@r4ulcl r4ulcl commented Jan 20, 2026

No description provided.

…, add GetWorkersThreads to task loop limit in db
)
if debug {
log.Println("DB ConnectDB - dataSourceName", dataSourceName)
log.Println("DB ConnectDB - DSN:", dsn)

Check failure

Code scanning / CodeQL

Clear-text logging of sensitive information High

Sensitive data returned by an access to password
flows to a logging call.

Copilot Autofix

AI 2 months ago

To fix the problem, the password must not appear in logs. The safest general approach is to either (a) avoid logging the DSN entirely or (b) log a redacted version that excludes or masks the password. We can preserve existing debug functionality (seeing what host, port, database, user are being used) while ensuring the password is never logged.

The single best fix here is to change the debug logging to log a DSN-like string that omits or masks the password. For example, we can build a separate safeDSN string that replaces the password portion with a placeholder like "***" or excludes it entirely, and log that instead of dsn. This maintains functionality (developers can still see connection parameters when debugging) without leaking credentials.

Concretely, in manager/database/DB.go around line 76–87, we will:

  • Keep the existing dsn construction unchanged (it’s needed for sql.Open).
  • Introduce a new safeDSN variable for logging, constructed with the same fmt.Sprintf but replacing password with a constant such as "***".
  • Change the log.Println call to log safeDSN instead of dsn.

No new imports or helper methods are needed; fmt is already imported and sufficient.

Suggested changeset 1
manager/database/DB.go

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/manager/database/DB.go b/manager/database/DB.go
--- a/manager/database/DB.go
+++ b/manager/database/DB.go
@@ -83,7 +83,15 @@
 		database,
 	)
 	if debug {
-		log.Println("DB ConnectDB - DSN:", dsn)
+		safeDSN := fmt.Sprintf(
+			"%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local&clientFoundRows=true",
+			username,
+			"***",
+			host,
+			port,
+			database,
+		)
+		log.Println("DB ConnectDB - DSN:", safeDSN)
 	}
 
 	db, err := sql.Open("mysql", dsn)
EOF
@@ -83,7 +83,15 @@
database,
)
if debug {
log.Println("DB ConnectDB - DSN:", dsn)
safeDSN := fmt.Sprintf(
"%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local&clientFoundRows=true",
username,
"***",
host,
port,
database,
)
log.Println("DB ConnectDB - DSN:", safeDSN)
}

db, err := sql.Open("mysql", dsn)
Copilot is powered by AI and may make mistakes. Always verify output.
Auth: []ssh.AuthMethod{
auth,
},
HostKeyCallback: ssh.InsecureIgnoreHostKey(),

Check failure

Code scanning / CodeQL

Use of insecure HostKeyCallback implementation High

Configuring SSH ClientConfig with insecure HostKeyCallback implementation from
this source
.

Copilot Autofix

AI 2 months ago

In general, the problem is fixed by replacing ssh.InsecureIgnoreHostKey() with a callback that validates the server’s host key against an allow‑list. The simplest and safest pattern in Go is to load one or more trusted public host keys from files (or another configuration source), parse them with ssh.ParseAuthorizedKey or ssh.ParsePublicKey, and then either (a) use ssh.FixedHostKey if there is exactly one expected key, or (b) implement a HostKeyCallback that compares the server’s presented key against the list of allowed keys and rejects unknown keys.

For this codebase, we should keep existing functionality (using the private key file and optional password) and only change how host keys are verified. Since we can’t assume additional fields on utils.ManagerSSHConfig, the least invasive change is to derive an expected host public key from a known file path. A common convention with OpenSSH is that for a private key path like /path/id_rsa, the corresponding public key is /path/id_rsa.pub. We can follow this convention locally within this file: construct the public‑key path by appending .pub to config.PrivateKeyPath, read and parse that as an authorized key, and use ssh.FixedHostKey to set HostKeyCallback. If reading/parsing the public key fails, we should log a fatal error, mirroring the existing fatal behavior on the private key.

Concretely, inside StartSSH in manager/sshTunnel/sshTunnel.go, we will:

  • After successfully loading the private key (auth), derive the public key path (config.PrivateKeyPath + ".pub").
  • Read the public key file with os.ReadFile.
  • Parse it with ssh.ParseAuthorizedKey to get an ssh.PublicKey.
  • Set HostKeyCallback: ssh.FixedHostKey(hostPublicKey) instead of ssh.InsecureIgnoreHostKey().
    No new imports are required, as os and ssh are already imported. This keeps authentication behavior the same while adding strong host key verification.
Suggested changeset 1
manager/sshTunnel/sshTunnel.go

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/manager/sshTunnel/sshTunnel.go b/manager/sshTunnel/sshTunnel.go
--- a/manager/sshTunnel/sshTunnel.go
+++ b/manager/sshTunnel/sshTunnel.go
@@ -74,13 +74,27 @@
 					log.Fatal("Error loading file ", config.PrivateKeyPath, err)
 				}
 
+				// Load the expected host public key from a .pub file corresponding to the private key
+				hostPublicKeyPath := config.PrivateKeyPath + ".pub"
+				if !checkFileExists(hostPublicKeyPath) {
+					log.Fatal("Host public key file ", hostPublicKeyPath, " not found")
+				}
+				hostPubBytes, err := os.ReadFile(hostPublicKeyPath)
+				if err != nil {
+					log.Fatal("Error reading host public key file ", hostPublicKeyPath, err)
+				}
+				hostPubKey, _, _, _, err := ssh.ParseAuthorizedKey(hostPubBytes)
+				if err != nil {
+					log.Fatal("Error parsing host public key from ", hostPublicKeyPath, err)
+				}
+
 				// SSH connection configuration
 				sshConfig := &ssh.ClientConfig{
 					User: config.SSHUsername,
 					Auth: []ssh.AuthMethod{
 						auth,
 					},
-					HostKeyCallback: ssh.InsecureIgnoreHostKey(),
+					HostKeyCallback: ssh.FixedHostKey(hostPubKey),
 				}
 
 				// If a password is provided, add it as an additional authentication method
EOF
@@ -74,13 +74,27 @@
log.Fatal("Error loading file ", config.PrivateKeyPath, err)
}

// Load the expected host public key from a .pub file corresponding to the private key
hostPublicKeyPath := config.PrivateKeyPath + ".pub"
if !checkFileExists(hostPublicKeyPath) {
log.Fatal("Host public key file ", hostPublicKeyPath, " not found")
}
hostPubBytes, err := os.ReadFile(hostPublicKeyPath)
if err != nil {
log.Fatal("Error reading host public key file ", hostPublicKeyPath, err)
}
hostPubKey, _, _, _, err := ssh.ParseAuthorizedKey(hostPubBytes)
if err != nil {
log.Fatal("Error parsing host public key from ", hostPublicKeyPath, err)
}

// SSH connection configuration
sshConfig := &ssh.ClientConfig{
User: config.SSHUsername,
Auth: []ssh.AuthMethod{
auth,
},
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
HostKeyCallback: ssh.FixedHostKey(hostPubKey),
}

// If a password is provided, add it as an additional authentication method
Copilot is powered by AI and may make mistakes. Always verify output.
return exec.Command("cmd", "/c", cmdStr)
} else if runtime.GOOS == "linux" {
// use --login to load bashrc
return exec.Command("bash", "--login", "-c", cmdStr)

Check failure

Code scanning / CodeQL

Command built from user-controlled sources Critical

This command depends on a
user-provided value
.
var stdout, stderr bytes.Buffer

// Set the output and error streams to the buffers
return exec.Command(command, argumentsArray...), nil

Check failure

Code scanning / CodeQL

Command built from user-controlled sources Critical

This command depends on a
user-provided value
.
// Ensure the directory exists
dir := getDirectory(path)
const dirPerm = 0600 // Use restricted permissions (0600)
if err := os.MkdirAll(dir, dirPerm); err != nil {

Check failure

Code scanning / CodeQL

Uncontrolled data used in path expression High

This path depends on a
user-provided value
.

Copilot Autofix

AI 2 months ago

In general, the fix is to ensure that any path derived (even indirectly) from websocket data is validated or constrained before being passed to filesystem APIs such as os.MkdirAll, os.WriteFile, and os.Remove. The usual approaches are: (1) enforce that the path is a single filename (no separators / ..), or (2) resolve it against a configured safe root directory and verify that the resulting absolute path is still under that root.

Here, RemoteFilePath can contain directories (you are calling filepath.Dir on it), so we should use the “safe root directory” approach. A minimal, non‑breaking change is:

  • Introduce a helper inside modules.go to map any provided RemoteFilePath into a safe directory. A simple choice (consistent with the existing code) is to root all paths under the process’s current working directory:
    • Compute safeBase, an absolute path (e.g. os.Getwd() once per call).
    • For each file.RemoteFilePath, join it with safeBase and normalize via filepath.Abs.
    • Verify that the resulting absolute path has safeBase as a prefix (taking care with separators).
    • If the check fails, return an error and skip processing/deletion for that file.
  • Use the validated absolute path both for directory creation and writing the file in ProcessFiles, and for deletion in DeleteFiles. That way, even if an attacker passes "../../etc/passwd" or "/root/.ssh/authorized_keys", the normalized path will either be rejected or confined under the safe base.

Concretely, in worker/modules/modules.go:

  • Add a small helper getSafeBaseDir() that returns os.Getwd() and caches/uses it per call to avoid recomputing it in every loop iteration.
  • Add a helper sanitizePathUnsafe(baseDir, relativePath string) (string, error) which:
    • joined := filepath.Join(baseDir, relativePath)
    • abs, err := filepath.Abs(joined)
    • ensures abs starts with baseDir + string(os.PathSeparator) or equals baseDir
    • returns an error if not.
  • In ProcessFiles, before using path := file.RemoteFilePath, compute safePath, err := sanitizePathUnsafe(baseDir, path) and use safePath for getDirectory, MkdirAll, WriteFile, and in logs.
  • In DeleteFiles, do the same: compute safePath from file.RemoteFilePath, delete safePath, and log that path; if sanitization fails, return an error or at least skip that file.

No new imports are needed; all required functions (os.Getwd, filepath.Abs, filepath.Join) are already available via existing imports.

Suggested changeset 1
worker/modules/modules.go

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/worker/modules/modules.go b/worker/modules/modules.go
--- a/worker/modules/modules.go
+++ b/worker/modules/modules.go
@@ -21,6 +21,45 @@
 
 var mutex sync.Mutex
 
+// getSafeBaseDir returns the absolute base directory under which all task files
+// will be created/removed. Currently it uses the worker's current working
+// directory as the root.
+func getSafeBaseDir() (string, error) {
+	baseDir, err := os.Getwd()
+	if err != nil {
+		return "", fmt.Errorf("failed to get working directory: %w", err)
+	}
+	baseDir, err = filepath.Abs(baseDir)
+	if err != nil {
+		return "", fmt.Errorf("failed to resolve base directory: %w", err)
+	}
+	return baseDir, nil
+}
+
+// sanitizePathUnsafe resolves relPath against baseDir and ensures the final
+// absolute path is contained within baseDir to mitigate directory traversal.
+func sanitizePathUnsafe(baseDir, relPath string) (string, error) {
+	if relPath == "" {
+		return "", fmt.Errorf("empty path is not allowed")
+	}
+	joined := filepath.Join(baseDir, relPath)
+	absPath, err := filepath.Abs(joined)
+	if err != nil {
+		return "", fmt.Errorf("failed to resolve path %q: %w", relPath, err)
+	}
+
+	// Ensure absPath is within baseDir
+	baseWithSep := baseDir
+	if !strings.HasSuffix(baseWithSep, string(os.PathSeparator)) {
+		baseWithSep += string(os.PathSeparator)
+	}
+	if absPath != baseDir && !strings.HasPrefix(absPath, baseWithSep) {
+		return "", fmt.Errorf("path %q escapes base directory", relPath)
+	}
+
+	return absPath, nil
+}
+
 // setWorkingID ensures the map is initialized and sets the PID
 func setWorkingID(status *globalstructs.WorkerStatus, id string, pid int) {
 	mutex.Lock()
@@ -188,12 +227,22 @@
 // ProcessFiles decodes the base64 content of each file in task.Files and saves it to its RemoteFilePath.
 // It updates the WorkerStatus and handles verbose and debug logging as needed.
 func ProcessFiles(task *globalstructs.Task, config *utils.WorkerConfig, status *globalstructs.WorkerStatus, id string, verbose, debug bool) error {
+	baseDir, err := getSafeBaseDir()
+	if err != nil {
+		return err
+	}
+
 	for num, file := range task.Files {
 		// Assuming 'fileContentB64B64' is the base64-encoded content as a string
 		// If this is a typo, rename it appropriately (e.g., 'FileContentB64')
 		contentB64 := file.FileContentB64
-		path := file.RemoteFilePath
+		remotePath := file.RemoteFilePath
 
+		safePath, err := sanitizePathUnsafe(baseDir, remotePath)
+		if err != nil {
+			return fmt.Errorf("file %d: invalid path %q: %w", num+1, remotePath, err)
+		}
+
 		// Decode the base64 content
 		decodedBytes, err := base64.StdEncoding.DecodeString(contentB64)
 		if err != nil {
@@ -201,15 +245,15 @@
 		}
 
 		// Ensure the directory exists
-		dir := getDirectory(path)
+		dir := getDirectory(safePath)
 		const dirPerm = 0600 // Use restricted permissions (0600)
 		if err := os.MkdirAll(dir, dirPerm); err != nil {
-			return fmt.Errorf("file %d: failed to create directories for %s: %w", num+1, path, err)
+			return fmt.Errorf("file %d: failed to create directories for %s: %w", num+1, safePath, err)
 		}
 		// Write the decoded content to the specified path
 		const filePerm = 0600
-		if err := os.WriteFile(path, decodedBytes, filePerm); err != nil {
-			return fmt.Errorf("file %d: failed to write file %s: %w", num+1, path, err)
+		if err := os.WriteFile(safePath, decodedBytes, filePerm); err != nil {
+			return fmt.Errorf("file %d: failed to write file %s: %w", num+1, safePath, err)
 		}
 
 		// Update the worker status if applicable
@@ -218,12 +257,12 @@
 
 		// Verbose logging
 		if verbose {
-			fmt.Printf("Saved file %d to %s\n", num+1, path)
+			fmt.Printf("Saved file %d to %s\n", num+1, safePath)
 		}
 
 		// Debug logging
 		if debug {
-			fmt.Printf("Debug: File %d details - Path: %s, Content Length: %d bytes\n", num+1, path, len(decodedBytes))
+			fmt.Printf("Debug: File %d details - Path: %s, Content Length: %d bytes\n", num+1, safePath, len(decodedBytes))
 		}
 	}
 
@@ -232,23 +267,33 @@
 
 // DeleteFiles delete files send
 func DeleteFiles(task *globalstructs.Task, verbose, debug bool) error {
+	baseDir, err := getSafeBaseDir()
+	if err != nil {
+		return err
+	}
+
 	for num, file := range task.Files {
-		path := file.RemoteFilePath
+		remotePath := file.RemoteFilePath
 
+		safePath, err := sanitizePathUnsafe(baseDir, remotePath)
+		if err != nil {
+			return fmt.Errorf("file %d: invalid path %q: %w", num+1, remotePath, err)
+		}
+
 		// Attempt to delete the file
-		err := os.Remove(path)
+		err = os.Remove(safePath)
 		if err != nil {
-			fmt.Printf("Error deleting file: %v\n", err)
+			fmt.Printf("Error deleting file %s: %v\n", safePath, err)
 		}
 
 		// Verbose logging
 		if verbose {
-			fmt.Printf("Deleted file %d to %s\n", num+1, path)
+			fmt.Printf("Deleted file %d to %s\n", num+1, safePath)
 		}
 
 		// Debug logging
 		if debug {
-			fmt.Printf("Debug: File %d details - Path: %s, Content\n", num+1, path)
+			fmt.Printf("Debug: File %d details - Path: %s, Content\n", num+1, safePath)
 		}
 	}
 
EOF
Copilot is powered by AI and may make mistakes. Always verify output.
}
// Write the decoded content to the specified path
const filePerm = 0600
if err := os.WriteFile(path, decodedBytes, filePerm); err != nil {

Check failure

Code scanning / CodeQL

Uncontrolled data used in path expression High

This path depends on a
user-provided value
.

Copilot Autofix

AI 2 months ago

In general, to fix uncontrolled path usage you must ensure that any path derived from untrusted data is either (a) validated as a single safe filename (no separators, no ..), or (b) resolved relative to a trusted base directory and checked to remain within that directory after normalization. For this worker, it is more flexible and backward-compatible to keep supporting subdirectories but force all paths to live under a known safe root (e.g., a worker’s base working directory from config).

The best fix here is to change ProcessFiles so that it no longer uses file.RemoteFilePath directly. Instead:

  1. Decide a safe base directory (we must rely only on code we see; utils.WorkerConfig is available but we don’t see its fields, so we’ll conservatively use the current working directory as a base and enforce that all paths stay within it).
  2. For each file.RemoteFilePath:
    • Reject empty paths.
    • Clean it with filepath.Clean.
    • Reject any absolute path (filepath.IsAbs(cleaned)).
    • Build an absolute path under the safe base: absPath := filepath.Join(baseDir, cleaned) and then absPath, err = filepath.Abs(absPath).
    • Ensure absPath is still within baseDir by prefix check (after obtaining baseDirAbs and appending a trailing separator).
  3. Use absPath for directory creation and writing, instead of the raw path.
  4. Keep logging using the original path or the resolved absPath as appropriate.

We will implement this entirely inside ProcessFiles in worker/modules/modules.go, adding local variables for baseDirAbs and absPath, and update lines 204–207 and 211–212 to use absPath instead of the unvalidated path. No new imports are required because filepath and strings are already imported.

Suggested changeset 1
worker/modules/modules.go

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/worker/modules/modules.go b/worker/modules/modules.go
--- a/worker/modules/modules.go
+++ b/worker/modules/modules.go
@@ -188,12 +188,45 @@
 // ProcessFiles decodes the base64 content of each file in task.Files and saves it to its RemoteFilePath.
 // It updates the WorkerStatus and handles verbose and debug logging as needed.
 func ProcessFiles(task *globalstructs.Task, config *utils.WorkerConfig, status *globalstructs.WorkerStatus, id string, verbose, debug bool) error {
+	// Define a safe base directory for all file operations. Here we use the current
+	// working directory so that untrusted input cannot escape this root.
+	baseDir, err := os.Getwd()
+	if err != nil {
+		return fmt.Errorf("failed to determine working directory: %w", err)
+	}
+	baseDirAbs, err := filepath.Abs(baseDir)
+	if err != nil {
+		return fmt.Errorf("failed to resolve absolute base directory: %w", err)
+	}
+	if !strings.HasSuffix(baseDirAbs, string(os.PathSeparator)) {
+		baseDirAbs = baseDirAbs + string(os.PathSeparator)
+	}
+
 	for num, file := range task.Files {
 		// Assuming 'fileContentB64B64' is the base64-encoded content as a string
 		// If this is a typo, rename it appropriately (e.g., 'FileContentB64')
 		contentB64 := file.FileContentB64
-		path := file.RemoteFilePath
+		originalPath := file.RemoteFilePath
 
+		if strings.TrimSpace(originalPath) == "" {
+			return fmt.Errorf("file %d: empty remote file path", num+1)
+		}
+
+		// Clean the path and ensure it is relative to the safe base directory.
+		cleanPath := filepath.Clean(originalPath)
+		if filepath.IsAbs(cleanPath) {
+			return fmt.Errorf("file %d: absolute paths are not allowed: %s", num+1, originalPath)
+		}
+
+		joinedPath := filepath.Join(baseDirAbs, cleanPath)
+		absPath, err := filepath.Abs(joinedPath)
+		if err != nil {
+			return fmt.Errorf("file %d: failed to resolve absolute path for %s: %w", num+1, originalPath, err)
+		}
+		if !strings.HasPrefix(absPath, baseDirAbs) {
+			return fmt.Errorf("file %d: resolved path escapes base directory: %s", num+1, originalPath)
+		}
+
 		// Decode the base64 content
 		decodedBytes, err := base64.StdEncoding.DecodeString(contentB64)
 		if err != nil {
@@ -201,15 +229,15 @@
 		}
 
 		// Ensure the directory exists
-		dir := getDirectory(path)
+		dir := getDirectory(absPath)
 		const dirPerm = 0600 // Use restricted permissions (0600)
 		if err := os.MkdirAll(dir, dirPerm); err != nil {
-			return fmt.Errorf("file %d: failed to create directories for %s: %w", num+1, path, err)
+			return fmt.Errorf("file %d: failed to create directories for %s: %w", num+1, absPath, err)
 		}
-		// Write the decoded content to the specified path
+		// Write the decoded content to the validated path
 		const filePerm = 0600
-		if err := os.WriteFile(path, decodedBytes, filePerm); err != nil {
-			return fmt.Errorf("file %d: failed to write file %s: %w", num+1, path, err)
+		if err := os.WriteFile(absPath, decodedBytes, filePerm); err != nil {
+			return fmt.Errorf("file %d: failed to write file %s: %w", num+1, absPath, err)
 		}
 
 		// Update the worker status if applicable
@@ -218,12 +242,12 @@
 
 		// Verbose logging
 		if verbose {
-			fmt.Printf("Saved file %d to %s\n", num+1, path)
+			fmt.Printf("Saved file %d to %s\n", num+1, absPath)
 		}
 
 		// Debug logging
 		if debug {
-			fmt.Printf("Debug: File %d details - Path: %s, Content Length: %d bytes\n", num+1, path, len(decodedBytes))
+			fmt.Printf("Debug: File %d details - Path: %s, Content Length: %d bytes\n", num+1, absPath, len(decodedBytes))
 		}
 	}
 
EOF
Copilot is powered by AI and may make mistakes. Always verify output.
path := file.RemoteFilePath

// Attempt to delete the file
err := os.Remove(path)

Check failure

Code scanning / CodeQL

Uncontrolled data used in path expression High

This path depends on a
user-provided value
.

Copilot Autofix

AI 2 months ago

In general, to fix uncontrolled-path issues, derive a normalized absolute path from a trusted base directory plus the user-supplied path, then verify that the resulting path is still within that base directory. Reject or skip the operation if the check fails. Apply this consistently for all file operations (create, write, delete) that use user-controlled path data.

For this codebase, the minimal, non-breaking fix is:

  1. Define a helper function in worker/modules/modules.go that:

    • Takes a *utils.WorkerConfig (to get a base directory; if there is a suitable field in the real code, use it; since we cannot see it, we’ll assume a field like WorkDir exists and fall back safely if empty).
    • Takes the untrusted relative path (file.RemoteFilePath).
    • Joins it with the base directory, obtains an absolute path using filepath.Abs, and checks that:
      • It’s under the base directory (by checking HasPrefix against a cleaned absolute base, taking care to include a path separator).
    • Returns the safe absolute path or an error.
  2. In ProcessFiles:

    • Replace direct use of file.RemoteFilePath with the validated safe path from the helper.
    • Use that safe path to compute the directory (getDirectory) and to call os.MkdirAll and os.WriteFile.
    • Return a clear error if validation fails.
  3. In DeleteFiles:

    • Change the signature to accept config *utils.WorkerConfig (so we can reuse the same validation).
    • Replace direct use of file.RemoteFilePath in os.Remove with the validated safe path from the helper.
    • Log or return an error when validation fails or deletion fails.
  4. In worker/process/processTask.go:

    • Update the call to modules.DeleteFiles to pass config.

Concretely:

  • Add a new helper in modules.go, e.g.:

    func getSafePath(config *utils.WorkerConfig, relPath string) (string, error) {
        baseDir := config.WorkDir // or another existing field; fallback to current dir if empty
        if baseDir == "" {
            baseDir = "."
        }
        baseAbs, err := filepath.Abs(baseDir)
        if err != nil {
            return "", fmt.Errorf("failed to resolve base directory: %w", err)
        }
        candidate := filepath.Join(baseAbs, relPath)
        candidateAbs, err := filepath.Abs(candidate)
        if err != nil {
            return "", fmt.Errorf("failed to resolve path %q: %w", relPath, err)
        }
    
        // Ensure candidateAbs is within baseAbs
        baseWithSep := baseAbs
        if !strings.HasSuffix(baseWithSep, string(os.PathSeparator)) {
            baseWithSep += string(os.PathSeparator)
        }
        if candidateAbs != baseAbs && !strings.HasPrefix(candidateAbs, baseWithSep) {
            return "", fmt.Errorf("path %q escapes base directory", relPath)
        }
        return candidateAbs, nil
    }
  • Modify ProcessFiles:

    • After path := file.RemoteFilePath, call safePath, err := getSafePath(config, path) and use safePath instead of path in getDirectory, MkdirAll, WriteFile, and logging.
  • Modify DeleteFiles:

    • Change signature to func DeleteFiles(task *globalstructs.Task, config *utils.WorkerConfig, verbose, debug bool) error.
    • For each file, call safePath, err := getSafePath(config, path) and use safePath in os.Remove and logging.
  • Modify Task in processTask.go:

    • Change modules.DeleteFiles(task, verbose, debug) to modules.DeleteFiles(task, config, verbose, debug).

This keeps existing behavior for valid in-sandbox paths, while preventing arbitrary filesystem access and addressing the CodeQL alert.


Suggested changeset 2
worker/modules/modules.go

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/worker/modules/modules.go b/worker/modules/modules.go
--- a/worker/modules/modules.go
+++ b/worker/modules/modules.go
@@ -21,6 +21,36 @@
 
 var mutex sync.Mutex
 
+// getSafePath resolves relPath against a configured base directory and ensures
+// that the resulting absolute path does not escape that base directory.
+func getSafePath(config *utils.WorkerConfig, relPath string) (string, error) {
+	baseDir := config.WorkDir
+	if baseDir == "" {
+		baseDir = "."
+	}
+	baseAbs, err := filepath.Abs(baseDir)
+	if err != nil {
+		return "", fmt.Errorf("failed to resolve base directory: %w", err)
+	}
+
+	candidate := filepath.Join(baseAbs, relPath)
+	candidateAbs, err := filepath.Abs(candidate)
+	if err != nil {
+		return "", fmt.Errorf("failed to resolve path %q: %w", relPath, err)
+	}
+
+	baseWithSep := baseAbs
+	if !strings.HasSuffix(baseWithSep, string(os.PathSeparator)) {
+		baseWithSep += string(os.PathSeparator)
+	}
+
+	if candidateAbs != baseAbs && !strings.HasPrefix(candidateAbs, baseWithSep) {
+		return "", fmt.Errorf("path %q escapes base directory", relPath)
+	}
+
+	return candidateAbs, nil
+}
+
 // setWorkingID ensures the map is initialized and sets the PID
 func setWorkingID(status *globalstructs.WorkerStatus, id string, pid int) {
 	mutex.Lock()
@@ -192,7 +222,7 @@
 		// Assuming 'fileContentB64B64' is the base64-encoded content as a string
 		// If this is a typo, rename it appropriately (e.g., 'FileContentB64')
 		contentB64 := file.FileContentB64
-		path := file.RemoteFilePath
+		relPath := file.RemoteFilePath
 
 		// Decode the base64 content
 		decodedBytes, err := base64.StdEncoding.DecodeString(contentB64)
@@ -200,16 +230,22 @@
 			return fmt.Errorf("file %d: failed to decode base64 content: %w", num+1, err)
 		}
 
+		// Resolve and validate the target path
+		safePath, err := getSafePath(config, relPath)
+		if err != nil {
+			return fmt.Errorf("file %d: invalid file path %q: %w", num+1, relPath, err)
+		}
+
 		// Ensure the directory exists
-		dir := getDirectory(path)
+		dir := getDirectory(safePath)
 		const dirPerm = 0600 // Use restricted permissions (0600)
 		if err := os.MkdirAll(dir, dirPerm); err != nil {
-			return fmt.Errorf("file %d: failed to create directories for %s: %w", num+1, path, err)
+			return fmt.Errorf("file %d: failed to create directories for %s: %w", num+1, safePath, err)
 		}
 		// Write the decoded content to the specified path
 		const filePerm = 0600
-		if err := os.WriteFile(path, decodedBytes, filePerm); err != nil {
-			return fmt.Errorf("file %d: failed to write file %s: %w", num+1, path, err)
+		if err := os.WriteFile(safePath, decodedBytes, filePerm); err != nil {
+			return fmt.Errorf("file %d: failed to write file %s: %w", num+1, safePath, err)
 		}
 
 		// Update the worker status if applicable
@@ -218,12 +248,12 @@
 
 		// Verbose logging
 		if verbose {
-			fmt.Printf("Saved file %d to %s\n", num+1, path)
+			fmt.Printf("Saved file %d to %s\n", num+1, safePath)
 		}
 
 		// Debug logging
 		if debug {
-			fmt.Printf("Debug: File %d details - Path: %s, Content Length: %d bytes\n", num+1, path, len(decodedBytes))
+			fmt.Printf("Debug: File %d details - Path: %s, Content Length: %d bytes\n", num+1, safePath, len(decodedBytes))
 		}
 	}
 
@@ -231,24 +257,30 @@
 }
 
 // DeleteFiles delete files send
-func DeleteFiles(task *globalstructs.Task, verbose, debug bool) error {
+func DeleteFiles(task *globalstructs.Task, config *utils.WorkerConfig, verbose, debug bool) error {
 	for num, file := range task.Files {
-		path := file.RemoteFilePath
+		relPath := file.RemoteFilePath
 
+		safePath, err := getSafePath(config, relPath)
+		if err != nil {
+			fmt.Printf("Error deleting file %d with path %q: %v\n", num+1, relPath, err)
+			continue
+		}
+
 		// Attempt to delete the file
-		err := os.Remove(path)
+		err = os.Remove(safePath)
 		if err != nil {
-			fmt.Printf("Error deleting file: %v\n", err)
+			fmt.Printf("Error deleting file %d at %s: %v\n", num+1, safePath, err)
 		}
 
 		// Verbose logging
 		if verbose {
-			fmt.Printf("Deleted file %d to %s\n", num+1, path)
+			fmt.Printf("Deleted file %d at %s\n", num+1, safePath)
 		}
 
 		// Debug logging
 		if debug {
-			fmt.Printf("Debug: File %d details - Path: %s, Content\n", num+1, path)
+			fmt.Printf("Debug: File %d details - Path: %s, Content\n", num+1, safePath)
 		}
 	}
 
EOF
worker/process/processTask.go
Outside changed files

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/worker/process/processTask.go b/worker/process/processTask.go
--- a/worker/process/processTask.go
+++ b/worker/process/processTask.go
@@ -38,7 +38,7 @@
 	}
 
 	if config.DeleteFiles {
-		err = modules.DeleteFiles(task, verbose, debug)
+		err = modules.DeleteFiles(task, config, verbose, debug)
 		if err != nil {
 			log.Println("Process Error DeleteFiles:", err)
 		}
EOF
@@ -38,7 +38,7 @@
}

if config.DeleteFiles {
err = modules.DeleteFiles(task, verbose, debug)
err = modules.DeleteFiles(task, config, verbose, debug)
if err != nil {
log.Println("Process Error DeleteFiles:", err)
}
Copilot is powered by AI and may make mistakes. Always verify output.
@r4ulcl r4ulcl merged commit 00bd8be into main Jan 20, 2026
5 of 7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants