mirror of
https://github.com/tailscale/tailscale.git
synced 2026-11-09 02:53:09 -05:00
Before invoking 'id -Gz <user>', apply the same input validation as the getent path (checkGetentInput), so a crafted username like '-u' can't be interpreted as an option by the id command. parseGetentUser now rejects multi-line (multiple or corrupted records) output and validates the username and shell fields (no leading dashes, control characters, or invalid UTF-8), the fields that are used to look up and run a session. Also add the missing leading-dash regression test for the 'id' call path. The passwd format has no escaping — gecos sub-fields are comma-separated — so an eighth colon-separated field means a stray colon in some field, shifting every field after it: home parses as a gecos fragment and the shell as the real home directory. Short records keep the existing treatment (padded, with the default-shell fallback); over-long records now fail closed like the other corrupt-output cases. Updates tailscale/corp#50380 Signed-off-by: James Tucker <james@tailscale.com>
75 lines
2.0 KiB
Go
75 lines
2.0 KiB
Go
// Copyright (c) Tailscale Inc & contributors
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
package osuser
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"os/user"
|
|
"runtime"
|
|
"strings"
|
|
"time"
|
|
|
|
"tailscale.com/version/distro"
|
|
)
|
|
|
|
// GetGroupIds returns the list of group IDs that the user is a member of, or
|
|
// an error. It will first try to use the 'id' command to get the group IDs,
|
|
// and if that fails, it will fall back to the user.GroupIds method.
|
|
func GetGroupIds(user *user.User) ([]string, error) {
|
|
if runtime.GOOS == "plan9" {
|
|
return nil, nil
|
|
}
|
|
|
|
if runtime.GOOS != "linux" && runtime.GOOS != "freebsd" {
|
|
return user.GroupIds()
|
|
}
|
|
|
|
if distro.Get() == distro.Gokrazy {
|
|
// Gokrazy is a single-user appliance with ~no userspace.
|
|
// There aren't users to look up (no /etc/passwd, etc)
|
|
// so rather than fail below, just hardcode root.
|
|
// TODO(bradfitz): fix os/user upstream instead?
|
|
return []string{"0"}, nil
|
|
}
|
|
|
|
if ids, err := getGroupIdsWithId(user.Username); err == nil {
|
|
return ids, nil
|
|
}
|
|
return user.GroupIds()
|
|
}
|
|
|
|
func getGroupIdsWithId(usernameOrUID string) ([]string, error) {
|
|
// Do some basic validation before passing this string to the "id"
|
|
// command, with the same rules as getent (see checkGetentInput), so a
|
|
// crafted username like "-u" can't be interpreted as an option.
|
|
if !checkGetentInput(usernameOrUID) {
|
|
return nil, errors.New("invalid username or UID")
|
|
}
|
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
defer cancel()
|
|
|
|
idArgs := []string{"-Gz", usernameOrUID}
|
|
if runtime.GOOS == "freebsd" {
|
|
idArgs = []string{"-G", usernameOrUID}
|
|
}
|
|
out, err := execId(ctx, idArgs...)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("running 'id' command: %w", err)
|
|
}
|
|
|
|
return parseGroupIds(out), nil
|
|
}
|
|
|
|
func parseGroupIds(cmdOutput []byte) []string {
|
|
s := strings.TrimSpace(string(cmdOutput))
|
|
// Parse NUL-delimited output.
|
|
if strings.ContainsRune(s, '\x00') {
|
|
return strings.Split(strings.Trim(s, "\x00"), "\x00")
|
|
}
|
|
// Parse whitespace-delimited output.
|
|
return strings.Fields(s)
|
|
}
|