diff options
| author | James O'Doherty <james@theodohertyfamily.com> | 2026-05-22 10:46:02 -0400 |
|---|---|---|
| committer | James O'Doherty <james@theodohertyfamily.com> | 2026-05-22 10:46:02 -0400 |
| commit | 9131b0004e7c640cc028179e1d049a4c62210d94 (patch) | |
| tree | 7efb5612b61240105851cb5d8ac8f05263644db4 /internal | |
| parent | 401683a6b11e5a7810c949147a12f2c4bbfba48a (diff) | |
Security hardening: prevent shell injection and null-byte crashes, implement 8-bit clean argument fuzzing and portable E2E binary discovery
Diffstat (limited to 'internal')
| -rw-r--r-- | internal/cli/cli.go | 9 | ||||
| -rw-r--r-- | internal/namespace/namespace.go | 41 | ||||
| -rw-r--r-- | internal/namespace/namespace_test.go | 2 |
3 files changed, 39 insertions, 13 deletions
diff --git a/internal/cli/cli.go b/internal/cli/cli.go index b315fba..eba7f68 100644 --- a/internal/cli/cli.go +++ b/internal/cli/cli.go @@ -18,6 +18,15 @@ func NewApp(args []string) *App { } func (a *App) Run() error { + // 1. Validate arguments for null bytes to prevent exec failures in the C launcher + for i, arg := range a.Args { + for j := 0; j < len(arg); j++ { + if arg[j] == 0 { + return fmt.Errorf("argument %d contains null byte at position %d", i, j) + } + } + } + // Handle the internal diagnostic commands first if len(a.Args) > 1 { switch a.Args[1] { diff --git a/internal/namespace/namespace.go b/internal/namespace/namespace.go index 1a09b78..b0794a4 100644 --- a/internal/namespace/namespace.go +++ b/internal/namespace/namespace.go @@ -2,7 +2,6 @@ package namespace import ( _ "embed" - "encoding/json" "fmt" "os" "os/exec" @@ -55,15 +54,13 @@ func VerifyIsolation() (bool, string) { return true, "Isolated and root" } -// VerifyArguments prints the current process arguments as a JSON array. -// This is used for E2E testing to verify that argument splitting and -// shell injection are not occurring during the bootstrap loop. +// VerifyArguments prints the current process arguments as hex-encoded strings. +// This is used for E2E testing to verify that the data path is 8-bit clean +// and that no bytes are mutated during the bootstrap loop. func VerifyArguments(args []string) error { - out, err := json.Marshal(args) - if err != nil { - return fmt.Errorf("failed to marshal arguments: %w", err) + for i, arg := range args { + fmt.Printf("%d:%x\n", i, arg) } - fmt.Println(string(out)) return nil } @@ -74,6 +71,16 @@ func Bootstrap() error { return nil } + // 0. Validate current arguments for null bytes before proceeding. + // If any argument contains a null byte, syscall.Exec will fail with 'invalid argument'. + for i, arg := range os.Args { + for j := 0; j < len(arg); j++ { + if arg[j] == 0 { + return fmt.Errorf("argument %d contains null byte at position %d", i, j) + } + } + } + self, err := os.Executable() if err != nil { return fmt.Errorf("failed to get executable path: %w", err) @@ -89,16 +96,16 @@ func Bootstrap() error { // 2. Write the embedded launcher binary to the temp file. if _, err := tmpFile.Write(launcherBytes); err != nil { - tmpFile.Close() + _ = tmpFile.Close() return fmt.Errorf("failed to write launcher binary: %w", err) } - + // Ensure the binary is executable (0700) if err := tmpFile.Chmod(0700); err != nil { - tmpFile.Close() + _ = tmpFile.Close() return fmt.Errorf("failed to set launcher permissions: %w", err) } - tmpFile.Close() + _ = tmpFile.Close() // 3. Prepare arguments for the launcher. // The launcher expects: launcher <command_to_run> [args...] @@ -106,6 +113,16 @@ func Bootstrap() error { args = append(args, os.Args[1:]...) // 4. Replace the current process with the launcher. + // We must check for null bytes in the arguments here because syscall.Exec + // (which calls execve) will return 'invalid argument' (EINVAL) if any + // string in the argv array contains a null byte. + for i, arg := range args { + for j := 0; j < len(arg); j++ { + if arg[j] == 0 { + return fmt.Errorf("launcher argument %d contains null byte at position %d", i, j) + } + } + } err = syscall.Exec(launcherPath, args, os.Environ()) if err != nil { return fmt.Errorf("launcher exec failed: %w", err) diff --git a/internal/namespace/namespace_test.go b/internal/namespace/namespace_test.go index 10511dd..54e3c93 100644 --- a/internal/namespace/namespace_test.go +++ b/internal/namespace/namespace_test.go @@ -6,7 +6,7 @@ import ( "testing" ) -// We move the complex isolation testing to tests/e2e to avoid +// We move the complex isolation testing to tests/e2e to avoid // issues with Go's temporary test binaries and process replacement. func TestNamespacePackage(t *testing.T) { t.Skip("Namespace isolation tests moved to tests/e2e") |
