fix(P02): namespace path traversal (REQ-120, F4)
---ci--- project: orca phase: 2 milestone: v0.12 status: execute ---/ci--- Add ns.ValidateName rejecting .., /, \, leading -, null bytes, control chars, spaces, >128 chars, and reserved 'cluster'. Wire into ns create/delete/inspect/validate/inherit/set-constraint + --parent flag. Fuzz test + 14 traversal regression tests. No namespace dir can escape ORCA_HOME.
This commit is contained in:
+21
-3
@@ -129,12 +129,12 @@ repeated to declare inheritance; _defaults is always appended last.`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
name := args[0]
|
||||
if err := ns.ValidateName(name); err != nil {
|
||||
return err
|
||||
}
|
||||
if name == paths.DefaultNamespace() {
|
||||
return fmt.Errorf("cannot create the implicit root namespace %q with `ns create` (it is auto-managed)", name)
|
||||
}
|
||||
if name == "cluster" {
|
||||
return fmt.Errorf("name %q is reserved for the cluster-wide dir", name)
|
||||
}
|
||||
if nsCreateParent == "" {
|
||||
nsCreateParent = paths.DefaultNamespace()
|
||||
}
|
||||
@@ -181,6 +181,9 @@ cannot be deleted.`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
name := args[0]
|
||||
if err := ns.ValidateName(name); err != nil {
|
||||
return err
|
||||
}
|
||||
if name == paths.DefaultNamespace() {
|
||||
return fmt.Errorf("cannot delete the implicit root namespace %q", name)
|
||||
}
|
||||
@@ -212,6 +215,9 @@ var nsInspectCmd = &cobra.Command{
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
name := args[0]
|
||||
if err := ns.ValidateName(name); err != nil {
|
||||
return err
|
||||
}
|
||||
root := paths.Root()
|
||||
cfgs, err := ns.ParseNSMdDir(root)
|
||||
if err != nil {
|
||||
@@ -265,6 +271,9 @@ set).`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
name := args[0]
|
||||
if err := ns.ValidateName(name); err != nil {
|
||||
return err
|
||||
}
|
||||
root := paths.Root()
|
||||
cfgs, err := ns.ParseNSMdDir(root)
|
||||
if err != nil {
|
||||
@@ -307,12 +316,18 @@ _defaults is always appended last (D-185).`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
name := args[0]
|
||||
if err := ns.ValidateName(name); err != nil {
|
||||
return err
|
||||
}
|
||||
if name == paths.DefaultNamespace() {
|
||||
return fmt.Errorf("cannot set parent on the implicit root namespace %q", name)
|
||||
}
|
||||
if nsInheritParent == "" {
|
||||
return fmt.Errorf("--parent is required")
|
||||
}
|
||||
if err := ns.ValidateName(nsInheritParent); err != nil {
|
||||
return fmt.Errorf("--parent: %w", err)
|
||||
}
|
||||
if nsInheritParent == name {
|
||||
return fmt.Errorf("namespace %q cannot inherit from itself", name)
|
||||
}
|
||||
@@ -358,6 +373,9 @@ across the inheritance chain by the resolver.`,
|
||||
Args: cobra.ExactArgs(2),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
name := args[0]
|
||||
if err := ns.ValidateName(name); err != nil {
|
||||
return err
|
||||
}
|
||||
kv := args[1]
|
||||
if name == paths.DefaultNamespace() {
|
||||
return fmt.Errorf("cannot set a constraint on the implicit root namespace %q with set-constraint; edit ns.md directly", name)
|
||||
|
||||
@@ -595,3 +595,67 @@ func TestNSSetConstraintDuplicate(t *testing.T) {
|
||||
t.Errorf("error = %q, want contains 'already set'", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// --- REQ-120 / F4 path traversal regression tests ---
|
||||
|
||||
// TestNSCreateTraversalRefused verifies that ns create rejects names
|
||||
// that would traverse outside ORCA_HOME via ".." or "/".
|
||||
func TestNSCreateTraversalRefused(t *testing.T) {
|
||||
bad := []string{
|
||||
"..",
|
||||
"../etc",
|
||||
"foo/../bar",
|
||||
"/etc",
|
||||
"etc/",
|
||||
"foo/bar",
|
||||
"-x",
|
||||
"--flag",
|
||||
"with space",
|
||||
"tab\there",
|
||||
"newline\nname",
|
||||
}
|
||||
for _, name := range bad {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
t.Setenv("ORCA_HOME", root)
|
||||
resetRootFlags(t)
|
||||
resetNSFlags()
|
||||
rootCmd.SetArgs([]string{"ns", "create", name})
|
||||
err := rootCmd.Execute()
|
||||
if err == nil {
|
||||
t.Errorf("ns create %q should fail, got nil", name)
|
||||
}
|
||||
// Verify no directory was created outside ORCA_HOME.
|
||||
// For ".." and "../etc", the danger is a dir was created
|
||||
// outside root. Check root's parent has no new orca dirs.
|
||||
parent := filepath.Dir(root)
|
||||
entries, _ := os.ReadDir(parent)
|
||||
for _, e := range entries {
|
||||
// The temp dir itself is fine; anything else that looks
|
||||
// like an orca namespace (has ns.md) outside root is a
|
||||
// leak.
|
||||
if e.Name() == filepath.Base(root) {
|
||||
continue
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(parent, e.Name(), "ns.md")); err == nil {
|
||||
t.Errorf("namespace dir leaked outside ORCA_HOME: %s", filepath.Join(parent, e.Name()))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestNSInheritTraversalRefused verifies --parent rejects traversal.
|
||||
func TestNSInheritTraversalRefused(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
t.Setenv("ORCA_HOME", root)
|
||||
resetRootFlags(t)
|
||||
resetNSFlags()
|
||||
writeDefaultsNS(t, root)
|
||||
writeCustomNS(t, root, "prod", "")
|
||||
rootCmd.SetArgs([]string{"ns", "inherit", "prod", "--parent", "../../etc"})
|
||||
err := rootCmd.Execute()
|
||||
if err == nil {
|
||||
t.Fatal("ns inherit with traversal --parent should fail")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user