Skip to content

Commit 5799a2a

Browse files
committed
Add validation for all path.* values in network.yaml
Path must not include whitespace to be safe for in sudoers files. All files and directories can only be writable by root or daemon user, or wheel group. Signed-off-by: Jan Dubois <[email protected]>
1 parent e652c39 commit 5799a2a

File tree

4 files changed

+97
-2
lines changed

4 files changed

+97
-2
lines changed

pkg/networks/config.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ func load() {
5656
if cache.err != nil {
5757
cache.err = fmt.Errorf("cannot parse %q: %w", configFile, cache.err)
5858
}
59+
cache.err = cache.config.validate()
5960
})
6061
}
6162

pkg/networks/networks.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ type Paths struct {
1616
VDESwitch string `yaml:"vdeSwitch"`
1717
VDEVMNet string `yaml:"vdeVMNet"`
1818
VarRun string `yaml:"varRun"`
19-
Sudoers string `yaml:"sudoers"`
19+
Sudoers string `yaml:"sudoers,omitempty"`
2020
}
2121

2222
const (

pkg/networks/networks.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
paths:
99
vdeSwitch: /opt/vde/bin/vde_switch
1010
vdeVMNet: /opt/vde/bin/vde_vmnet
11-
varRun: /var/run/lima
11+
varRun: /private/var/run/lima
1212
sudoers: /etc/sudoers.d/lima
1313

1414
group: staff

pkg/networks/validate.go

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
package networks
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"io/fs"
7+
"os"
8+
"path/filepath"
9+
"reflect"
10+
"strings"
11+
"syscall"
12+
)
13+
14+
func (config *NetworksConfig) validate() error {
15+
// validate all paths.* values
16+
paths := reflect.ValueOf(&config.Paths).Elem()
17+
for i := 0; i < paths.NumField(); i++ {
18+
// extract YAML name from struct tag; strip options like "omitempty"
19+
name := paths.Type().Field(i).Tag.Get("yaml")
20+
if i := strings.IndexRune(name, ','); i > -1 {
21+
name = name[:i]
22+
}
23+
path := paths.Field(i).Interface().(string)
24+
// varPath will be created securely, but any existing parent directories must already be secure
25+
if name == "varRun" {
26+
path = findBaseDirectory(path)
27+
}
28+
err := validatePath(path)
29+
if err != nil {
30+
// sudoers file does not need to exist; otherwise `limactl sudoers` couldn't bootstrap
31+
if name == "sudoers" && errors.Is(err, os.ErrNotExist) {
32+
continue
33+
}
34+
return fmt.Errorf("network.yaml field `paths.%s` error: %w", name, err)
35+
}
36+
}
37+
// TODO: validate network definitions
38+
return nil
39+
}
40+
41+
// findBaseDirectory removes non-existing directories from the end of the path.
42+
func findBaseDirectory(path string) string {
43+
if _, err := os.Lstat(path); errors.Is(err, os.ErrNotExist) {
44+
if path != "/" {
45+
return findBaseDirectory(filepath.Dir(path))
46+
}
47+
}
48+
return path
49+
}
50+
51+
func validatePath(path string) error {
52+
if path == "" {
53+
return nil
54+
}
55+
if path[0] != '/' {
56+
return fmt.Errorf("path %q is not an absolute path", path)
57+
}
58+
if strings.ContainsRune(path, ' ') {
59+
return fmt.Errorf("path %q contains whitespace", path)
60+
}
61+
fi, err := os.Lstat(path)
62+
if err != nil {
63+
return err
64+
}
65+
file := "file"
66+
if fi.Mode().IsDir() {
67+
file = "dir"
68+
}
69+
// TODO: should we allow symlinks when both the link and the target are secure?
70+
// E.g. on macOS /var is a symlink to /private/var
71+
if (fi.Mode() & fs.ModeSymlink) != 0 {
72+
return fmt.Errorf("%s %q is a symlink", file, path)
73+
}
74+
if stat, ok := fi.Sys().(*syscall.Stat_t); ok {
75+
if stat.Uid != 0 {
76+
return fmt.Errorf("%s %q is not owned by 0 (root), but by %d", file, path, stat.Uid)
77+
}
78+
if (fi.Mode() & 020) != 0 {
79+
if stat.Gid != 0 && stat.Gid != 1 {
80+
return fmt.Errorf("%s %q is group-writable and group is neither 0 (wheel) nor 1 (daemon), but is %d", file, path, stat.Gid)
81+
}
82+
}
83+
if (fi.Mode() & 002) != 0 {
84+
return fmt.Errorf("%s %q is world-writable", file, path)
85+
}
86+
} else {
87+
// should never happen
88+
return fmt.Errorf("could not retrieve stat buffer for %q", path)
89+
}
90+
if path != "/" {
91+
return validatePath(filepath.Dir(path))
92+
}
93+
return nil
94+
}

0 commit comments

Comments
 (0)