model: validate VM names as DNS labels at CLI + daemon
A VM name flows into five places that all have narrower grammars
than "arbitrary string":
- the guest's /etc/hostname (vm_disk.patchRootOverlay)
- the guest's /etc/hosts (same)
- the <name>.vm DNS record (vmdns.RecordName)
- the kernel command line (system.BuildBootArgs*)
- VM-dir file-path fragments (layout.VMsDir/<id>, etc.)
Nothing in the chain was validating the input. A name with
whitespace, newline, dot, slash, colon, or = would produce broken
hostnames, weird DNS labels, smuggled kernel cmdline tokens, or
(in the worst case) surprising traversal through the on-disk
layout. Not host shell injection — we already avoid shelling out
with the raw name — but a real correctness and supportability bug.
New: model.ValidateVMName. Rules:
- 1..63 chars (DNS label max per RFC 1123; also a comfortable
/etc/hostname cap)
- lowercase ASCII letters, digits, '-' only
- no leading or trailing '-'
- no normalization — the name is the user-visible identifier
(store key, `ssh <name>.vm`, `vm show`); silently rewriting
"MyVM" → "myvm" would hand the user back something different
than they typed
Called from two places:
- internal/cli/commands_vm.go vmCreateParamsFromFlags — rejects
bad `--name` values before any RPC. Empty name still passes
through so the daemon can generate one.
- internal/daemon/vm_create.go reserveVM — defense in depth for
any non-CLI RPC caller (SDK, direct JSON over the socket).
Tests:
- internal/model/vm_name_test.go — exhaustive character-class
matrix (space, newline, tab, dot, slash, colon, equals, quote,
control chars, unicode letters, uppercase, leading/trailing
hyphen, over-length, max-length-exact, digits-only).
- internal/cli TestVMCreateParamsFromFlagsRejectsInvalidName —
CLI wire-through + empty-name passthrough.
- internal/daemon TestReserveVMRejectsInvalidName — daemon
defense-in-depth (including `box/../evil` path-traversal).
- scripts/smoke.sh — end-to-end rejection + no-leaked-row
assertion.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
700a1e6e60
commit
caa6a2b996
7 changed files with 225 additions and 0 deletions
68
internal/model/vm_name_test.go
Normal file
68
internal/model/vm_name_test.go
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
package model
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestValidateVMName(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
input string
|
||||
wantOK bool
|
||||
wantErrSub string
|
||||
}{
|
||||
// Happy path.
|
||||
{"simple", "mybox", true, ""},
|
||||
{"with-hyphen", "my-box", true, ""},
|
||||
{"digits", "box-123", true, ""},
|
||||
{"digits-only", "1234", true, ""},
|
||||
{"single-char", "a", true, ""},
|
||||
{"max length", strings.Repeat("a", MaxVMNameLen), true, ""},
|
||||
{"namegen style", "ace-fox", true, ""},
|
||||
|
||||
// Empty / length.
|
||||
{"empty", "", false, "required"},
|
||||
{"over max length", strings.Repeat("a", MaxVMNameLen+1), false, "max is"},
|
||||
|
||||
// Hyphen position.
|
||||
{"leading hyphen", "-box", false, "cannot start or end with '-'"},
|
||||
{"trailing hyphen", "box-", false, "cannot start or end with '-'"},
|
||||
{"lone hyphen", "-", false, "cannot start or end with '-'"},
|
||||
|
||||
// Character class.
|
||||
{"uppercase", "MyBox", false, "invalid character"},
|
||||
{"space", "my box", false, "invalid character"},
|
||||
{"newline", "my\nbox", false, "invalid character"},
|
||||
{"tab", "my\tbox", false, "invalid character"},
|
||||
{"dot", "my.box", false, "invalid character"},
|
||||
{"dot-vm suffix", "box.vm", false, "invalid character"},
|
||||
{"slash", "my/box", false, "invalid character"},
|
||||
{"underscore", "my_box", false, "invalid character"},
|
||||
{"at sign", "user@box", false, "invalid character"},
|
||||
{"colon (kernel cmdline separator)", "my:box", false, "invalid character"},
|
||||
{"equals (kernel cmdline)", "a=b", false, "invalid character"},
|
||||
{"quote", "my\"box", false, "invalid character"},
|
||||
{"unicode letter", "box-α", false, "invalid character"},
|
||||
{"leading space", " box", false, "invalid character"},
|
||||
{"trailing space", "box ", false, "invalid character"},
|
||||
{"control char NUL", "my\x00box", false, "invalid character"},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
err := ValidateVMName(tc.input)
|
||||
if tc.wantOK {
|
||||
if err != nil {
|
||||
t.Fatalf("ValidateVMName(%q) = %v, want nil", tc.input, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
if err == nil {
|
||||
t.Fatalf("ValidateVMName(%q) = nil, want error containing %q", tc.input, tc.wantErrSub)
|
||||
}
|
||||
if !strings.Contains(err.Error(), tc.wantErrSub) {
|
||||
t.Fatalf("ValidateVMName(%q) = %v, want error containing %q", tc.input, err, tc.wantErrSub)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue