Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
141 changes: 128 additions & 13 deletions accounts1/users/prop.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,7 @@ import (
libdate "github.com/rickb777/date"
)

var (
errInvalidParam = fmt.Errorf("Invalid or empty parameter")
)
var errInvalidParam = fmt.Errorf("Invalid or empty parameter")

var (
groupFileTimestamp int64 = 0
Expand Down Expand Up @@ -157,25 +155,142 @@ func ModifyShell(shell, username string) error {
return doAction(userCmdModify, []string{"-s", shell, username})
}

// isValidCryptHash validates the format of a crypt password hash string.
// It enforces printable ASCII boundaries and blocks high-risk delimiters.
// from: https://manpages.debian.org/unstable/libcrypt-dev/crypt.5.en.html
func isValidCryptHash(hash string) error {
if hash == "" {
return errors.New("password hash is empty")
}

for i := 0; i < len(hash); i++ {
b := hash[i]

// This keeps the error generic and avoids leaking hash structure details.
if b < 32 || b > 126 {
return errors.New("password hash contains non-printable ASCII characters")
}

switch b {
case ' ', ':', ';', '*', '!', '\\':
return errors.New("password hash contains forbidden characters")
}
}

return nil
}

// isValidUsername validates the input username.
// It follows useradd's strict rules instead of adduser's NAME_REGEX
// to prevent control flow injection (e.g., line/field truncation)
// when feeding "username:password" into chpasswd via stdin.
// from: https://github.com/shadow-maint/shadow/blob/710c4d4f88fa32dfc4c4d1f714e935d8bff6ae00/lib/chkname.c#L103
func isValidUsername(name string) error {
if name == "" || name == "." || name == ".." {
return errors.New("username can't be '.' or '..' or empty")
}

if len(name) > LoginNameMaxSize() {
return errors.New("username too long")
}

if strings.Trim(name, "-") == "" {
return errors.New("username cannot consist entirely of hyphens")
}

// below check follows BRE: [a-zA-Z0-9_.][a-zA-Z0-9_.-]*$\?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

话说这里不能直接正则吗(

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

可以用正则,这里我是想做的方便提供诊断信息,正则没法知道为什么失配(

@ComixHe ComixHe May 25, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

测试用例正在加,我单独提一个test的commit~

first := name[0]
isFirstValid := (first >= 'a' && first <= 'z') ||
(first >= 'A' && first <= 'Z') ||
(first >= '0' && first <= '9') ||
first == '_' ||
first == '.'
if !isFirstValid {
return errors.New("first character must be alphanumeric, underscore, or dot")
}

isAllDigit := (first >= '0' && first <= '9')
for i := 1; i < len(name); i++ {
ch := name[i]

if ch < '0' || ch > '9' {
isAllDigit = false
}

isValidChar := (ch >= 'a' && ch <= 'z') ||
(ch >= 'A' && ch <= 'Z') ||
(ch >= '0' && ch <= '9') ||
ch == '_' ||
ch == '.' ||
ch == '-'

if isValidChar {
continue
}

if ch == '$' && i == len(name)-1 {
continue
}

return errors.New("username contains invalid characters or '$' is not at the end")
}

if isAllDigit {
return errors.New("username cannot consist entirely of digits")
}

return nil
}

func ModifyPasswd(words, username string) error {

@UTsweetyfish UTsweetyfish May 25, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这里有必要加个注释,说明调用者吗?
还是该在 D-Bus 那边加?还是不加(?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

如果要加的话,感觉在dbus那边加好一点?因为我感觉这个ModifyPasswd没暴露到dbus上所以不用(

if len(words) == 0 {
return errInvalidParam
if words == "" || username == "" {
return errors.New("password hash or username is empty")
}
// 防止命令注入
if strings.ContainsAny(words, "\n\r") || strings.ContainsAny(username, "\n\r:") {
return errInvalidParam

if err := isValidUsername(username); err != nil {
return fmt.Errorf("username is invalid: %w", err)
}

if err := isValidCryptHash(words); err != nil {
return fmt.Errorf("invalid password hash: %w", err)
}

cmd := exec.Command(pwdCmdModify, "-e")
input := fmt.Sprintf("%s:%s\n", username, words)
cmd.Stdin = bytes.NewBufferString(input)
// clear environments for security, if it works unexpectedly then add env which chpasswd needs
cmd.Env = []string{}

stdin, err := cmd.StdinPipe()
if err != nil {
return fmt.Errorf("failed to create stdin pipe: %w", err)
}

var stderr bytes.Buffer
cmd.Stderr = &stderr

err := cmd.Run()
if err != nil {
return fmt.Errorf("failed to modify password: %v, %s", err, stderr.String())
if err := cmd.Start(); err != nil {
return fmt.Errorf("failed to start command: %w", err)
}

// Write the password hash to stdin
// no need to erase this data, because this hash already exist in go string
buf := bytes.NewBuffer(make([]byte, 0, len(username)+len(words)+2))
buf.WriteString(username)
buf.WriteString(":")
buf.WriteString(words)
buf.WriteString("\n")

input := buf.Bytes()
_, writeErr := stdin.Write(input)
Comment thread
ComixHe marked this conversation as resolved.
stdin.Close()

if writeErr != nil {
_ = cmd.Process.Kill()
_ = cmd.Wait()
return fmt.Errorf("failed to write to stdin: %w", writeErr)
}

if err := cmd.Wait(); err != nil {
return fmt.Errorf("failed to update system password configuration %s", stderr.String())
}

return nil
Expand Down
36 changes: 36 additions & 0 deletions accounts1/users/user.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd.
//
// SPDX-License-Identifier: GPL-3.0-or-later

package users

/*
#include <unistd.h>
#include <limits.h>

#ifndef LOGIN_NAME_MAX
#define LOGIN_NAME_MAX 256
#endif

long get_login_name_max() {
long conf = -1;
conf = sysconf(_SC_LOGIN_NAME_MAX);

if (conf == -1) {
conf = LOGIN_NAME_MAX;
}

return conf;
}
*/
import "C"

var loginNameMaxSize int

func init() {
loginNameMaxSize = int(C.get_login_name_max())
}

func LoginNameMaxSize() int {
return loginNameMaxSize
}
Loading
Loading