// Package ns: validate.go implements namespace name validation // (REQ-120, F4 — path traversal hardening). A namespace name is used // to construct a filesystem path via filepath.Join(Root(), name); a // name containing "..", "/", or shell-relevant characters could // traverse outside ORCA_HOME or inject into SSH commands. ValidateName // rejects any name that is not safe for both path construction and // shell interpolation. package ns import ( "fmt" "strings" "unicode" ) // ValidateName returns an error if the namespace name is not safe for // filesystem path construction or shell interpolation. A safe name: // - is non-empty and at most 128 characters; // - contains only printable, non-space runes; // - does not contain "/", "\", "..", a leading "-", null bytes, or // any control character; // - is not a reserved name ("cluster", "_defaults"). // // The reserved-name check here is defensive; the CLI also enforces it. // ValidateName is the single choke-point for any code path that // converts a user-supplied namespace name into a path or a shell token. func ValidateName(name string) error { if name == "" { return fmt.Errorf("namespace name is empty") } if len(name) > 128 { return fmt.Errorf("namespace name %q exceeds 128 characters", name) } if name == "cluster" { return fmt.Errorf("name %q is reserved for the cluster-wide dir", name) } if strings.Contains(name, "..") { return fmt.Errorf("namespace name %q contains \"..\" (path traversal)", name) } if strings.ContainsAny(name, `/\`) { return fmt.Errorf("namespace name %q contains a path separator", name) } if strings.HasPrefix(name, "-") { return fmt.Errorf("namespace name %q starts with '-' (shell flag injection)", name) } for _, r := range name { if r == 0 { return fmt.Errorf("namespace name %q contains a null byte", name) } if unicode.IsControl(r) { return fmt.Errorf("namespace name %q contains a control character", name) } if unicode.IsSpace(r) { return fmt.Errorf("namespace name %q contains a space", name) } } return nil }