-
Notifications
You must be signed in to change notification settings - Fork 121
fix(users): harden password hash handling in ModifyPasswd #1120
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 | ||
|
|
@@ -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_.-]*$\? | ||
| 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 { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 这里有必要加个注释,说明调用者吗?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
|
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 | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
话说这里不能直接正则吗(
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
可以用正则,这里我是想做的方便提供诊断信息,正则没法知道为什么失配(
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
测试用例正在加,我单独提一个test的commit~