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
45
internal/model/vm_name.go
Normal file
45
internal/model/vm_name.go
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
package model
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// MaxVMNameLen is the upper bound on a user-provided VM name. DNS
|
||||
// labels (RFC 1123) allow up to 63 octets; the name ends up as the
|
||||
// first label of `<name>.vm` records served by banger's vmdns, and
|
||||
// also as the guest's /etc/hostname — so fitting both invariants in
|
||||
// a single ceiling keeps the model simple.
|
||||
const MaxVMNameLen = 63
|
||||
|
||||
// ValidateVMName rejects names that aren't safe to use as a DNS
|
||||
// label, a Linux hostname, a kernel-command-line token, or a
|
||||
// file-path component. Concretely: lowercase ASCII letters, digits,
|
||||
// and '-', 1..MaxVMNameLen chars, no leading or trailing hyphen.
|
||||
//
|
||||
// No normalization (trimming, case folding) — the VM name becomes
|
||||
// the user-visible identifier (store lookup key, `ssh <name>.vm`,
|
||||
// `vm show <name>`), and a silent rewrite would hand the user back
|
||||
// a different name than they typed. Reject early with an explicit
|
||||
// message instead.
|
||||
func ValidateVMName(name string) error {
|
||||
if name == "" {
|
||||
return errors.New("vm name is required")
|
||||
}
|
||||
if len(name) > MaxVMNameLen {
|
||||
return fmt.Errorf("vm name %q is %d characters; max is %d (DNS label limit)", name, len(name), MaxVMNameLen)
|
||||
}
|
||||
if name[0] == '-' || name[len(name)-1] == '-' {
|
||||
return fmt.Errorf("vm name %q cannot start or end with '-'", name)
|
||||
}
|
||||
for i, r := range name {
|
||||
switch {
|
||||
case r >= 'a' && r <= 'z':
|
||||
case r >= '0' && r <= '9':
|
||||
case r == '-':
|
||||
default:
|
||||
return fmt.Errorf("vm name %q has invalid character %q at position %d (allowed: lowercase a-z, 0-9, '-')", name, r, i)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue