Fix path traversal in template/snapshot names and network cleanup leaks

Add SafeName validator (allowlist regex) to reject directory traversal
in user-supplied template and snapshot names. Validated at both API
handlers (400 response) and sandbox manager (defense in depth).

Refactor CreateNetwork with rollback slice so partially created
resources (namespace, veth, routes, iptables rules) are cleaned up
on any error. Refactor RemoveNetwork to collect and return errors
instead of silently ignoring them.
This commit is contained in:
2026-03-13 08:40:36 +06:00
parent 63e9132d38
commit a0d635ae5e
6 changed files with 180 additions and 13 deletions

24
internal/validate/name.go Normal file
View File

@ -0,0 +1,24 @@
package validate
import (
"fmt"
"regexp"
)
// nameRe matches safe path component names: alphanumeric start, then
// alphanumeric, dash, underscore, or dot. Max 64 characters.
var nameRe = regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9._-]{0,63}$`)
// SafeName checks that name is safe for use as a single filesystem path
// component. It rejects empty strings, path separators, ".." sequences,
// leading dots, and anything outside the alphanumeric+dash+underscore+dot
// allowlist.
func SafeName(name string) error {
if name == "" {
return fmt.Errorf("name must not be empty")
}
if !nameRe.MatchString(name) {
return fmt.Errorf("name %q contains invalid characters or is too long (max 64, must match %s)", name, nameRe.String())
}
return nil
}

View File

@ -0,0 +1,41 @@
package validate
import "testing"
func TestSafeName(t *testing.T) {
tests := []struct {
name string
input string
wantErr bool
}{
{"simple", "minimal", false},
{"with-dash", "template-abc123", false},
{"with-dot", "my-snapshot.v2", false},
{"sandbox-id", "sb-12345678", false},
{"single-char", "a", false},
{"numbers", "123", false},
{"max-length", "abcdefghijklmnopqrstuvwxyz0123456789abcdefghijklmnopqrstuvwxyz01", false},
{"empty", "", true},
{"dot-dot", "..", true},
{"single-dot", ".", true},
{"leading-dot", ".hidden", true},
{"slash", "foo/bar", true},
{"backslash", "foo\\bar", true},
{"traversal", "../etc/passwd", true},
{"embedded-traversal", "foo/../bar", true},
{"space", "foo bar", true},
{"too-long", "abcdefghijklmnopqrstuvwxyz012345678901abcdefghijklmnopqrstuvwxyz01", true},
{"absolute", "/etc/passwd", true},
{"tilde", "~root", true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := SafeName(tt.input)
if (err != nil) != tt.wantErr {
t.Errorf("SafeName(%q) error = %v, wantErr %v", tt.input, err, tt.wantErr)
}
})
}
}