diff --git a/user/user.go b/user/user.go index e7477677..5cdb3889 100644 --- a/user/user.go +++ b/user/user.go @@ -56,13 +56,15 @@ type IDMap struct { Count int64 } -func parseLine(line []byte, v ...any) { - parseParts(bytes.Split(line, []byte(":")), v...) +func parseLine(line []byte, v ...any) bool { + return parseParts(bytes.Split(line, []byte(":")), v...) } -func parseParts(parts [][]byte, v ...any) { +func parseParts(parts [][]byte, v ...any) bool { + // Report invalid numeric fields so callers do not accept them as ID zero. + valid := true if len(parts) == 0 { - return + return true } for i, p := range parts { @@ -78,10 +80,17 @@ func parseParts(parts [][]byte, v ...any) { case *string: *e = string(p) case *int: - // "numbers", with conversion errors ignored because of some misbehaving configuration files. - *e, _ = strconv.Atoi(string(p)) + id, err := strconv.Atoi(string(p)) + *e = id + if err != nil { + valid = false + } case *int64: - *e, _ = strconv.ParseInt(string(p), 10, 64) + id, err := strconv.ParseInt(string(p), 10, 64) + *e = id + if err != nil { + valid = false + } case *[]string: // Comma-separated lists. if len(p) != 0 { @@ -94,6 +103,8 @@ func parseParts(parts [][]byte, v ...any) { panic(fmt.Sprintf("parseLine only accepts {*string, *int, *int64, *[]string} as arguments! %#v is not a pointer!", e)) } } + + return valid } func ParsePasswdFile(path string) ([]User, error) { @@ -135,7 +146,9 @@ func ParsePasswdFilter(r io.Reader, filter func(User) bool) ([]User, error) { // root:x:0:0:root:/root:/bin/bash // adm:x:3:4:adm:/var/adm:/bin/false p := User{} - parseLine(line, &p.Name, &p.Pass, &p.Uid, &p.Gid, &p.Gecos, &p.Home, &p.Shell) + if !parseLine(line, &p.Name, &p.Pass, &p.Uid, &p.Gid, &p.Gecos, &p.Home, &p.Shell) { + continue + } if filter == nil || filter(p) { out = append(out, p) @@ -194,7 +207,9 @@ func ParseGroupFilter(r io.Reader, filter func(Group) bool) ([]Group, error) { // root:x:0:root // adm:x:4:root,adm,daemon p := Group{} - parseLine(line, &p.Name, &p.Pass, &p.Gid, &p.List) + if !parseLine(line, &p.Name, &p.Pass, &p.Gid, &p.List) { + continue + } if filter == nil || filter(p) { out = append(out, p) @@ -501,7 +516,9 @@ func ParseSubIDFilter(r io.Reader, filter func(SubID) bool) ([]SubID, error) { // see: man 5 subuid p := SubID{} - parseLine(line, &p.Name, &p.SubID, &p.Count) + if !parseLine(line, &p.Name, &p.SubID, &p.Count) { + continue + } if filter == nil || filter(p) { out = append(out, p) @@ -549,7 +566,9 @@ func ParseIDMapFilter(r io.Reader, filter func(IDMap) bool) ([]IDMap, error) { // see: man 7 user_namespaces p := IDMap{} - parseParts(bytes.Fields(line), &p.ID, &p.ParentID, &p.Count) + if !parseParts(bytes.Fields(line), &p.ID, &p.ParentID, &p.Count) { + continue + } if filter == nil || filter(p) { out = append(out, p) diff --git a/user/user_test.go b/user/user_test.go index 8d08e1c5..3b7362a4 100644 --- a/user/user_test.go +++ b/user/user_test.go @@ -707,3 +707,32 @@ func largeGroup() (res string) { } return b.String() } + +func TestParsePasswdFilterSkipsInvalidIDs(t *testing.T) { + users, err := ParsePasswdFilter(strings.NewReader(` +root:x:0:0:root:/root:/bin/sh +bad-uid:x:not-a-uid:10::/:/bin/sh +bad-gid:x:11:not-a-gid::/:/bin/sh +overflow:x:999999999999999999999:12::/:/bin/sh +`), nil) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + if len(users) != 1 || users[0].Name != "root" || users[0].Uid != 0 || users[0].Gid != 0 { + t.Fatalf("Expected only the valid root entry, got %+v", users) + } +} + +func TestParseGroupFilterSkipsInvalidIDs(t *testing.T) { + groups, err := ParseGroupFilter(strings.NewReader(` +root:x:0:root +bad-gid:x:not-a-gid:root +overflow:x:999999999999999999999:root +`), nil) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + if len(groups) != 1 || groups[0].Name != "root" || groups[0].Gid != 0 { + t.Fatalf("Expected only the valid root entry, got %+v", groups) + } +}