Skip to content

fix(users): harden password hash handling in ModifyPasswd#1120

Merged
ComixHe merged 2 commits into
linuxdeepin:masterfrom
ComixHe:master
May 25, 2026
Merged

fix(users): harden password hash handling in ModifyPasswd#1120
ComixHe merged 2 commits into
linuxdeepin:masterfrom
ComixHe:master

Conversation

@ComixHe
Copy link
Copy Markdown
Contributor

@ComixHe ComixHe commented May 22, 2026

Validate password crypt hashes and username before invoking chpasswd to reject invalid characters and malformed input per crypt(5) and useradd's username validation rules.

Also improve process isolation and sensitive data handling by clearing the child environment, switching to an explicit stdin pipe flow, and zeroing the temporary password buffer after use.

Additionally, avoid exposing detailed backend errors to callers to reduce information disclosure risks.

Summary by Sourcery

Harden password change handling by validating crypt hashes, isolating the chpasswd subprocess environment, securely piping credentials, and reducing error detail exposure.

New Features:

  • Add validation for password crypt hashes before attempting password modification.

Enhancements:

  • Improve security of ModifyPasswd by clearing the subprocess environment and explicitly writing credentials via a stdin pipe with in-memory zeroing.
  • Simplify and sanitize error reporting for password modification failures to avoid leaking backend details.

@ComixHe ComixHe requested review from UTsweetyfish, fly602 and zccrs May 22, 2026 09:51
@sourcery-ai
Copy link
Copy Markdown

sourcery-ai Bot commented May 22, 2026

Reviewer's Guide

Hardened ModifyPasswd by validating password hash input per crypt(5), tightening process/env handling around chpasswd, explicitly zeroing sensitive buffers, and masking backend errors, plus adding a reusable crypt-hash validator.

File-Level Changes

Change Details Files
Add reusable validator for crypt(5)-style password hashes and apply it to ModifyPasswd input.
  • Introduced IsValidCryptHash to reject empty hashes, non-printable ASCII, and disallowed characters such as spaces, colons, semicolons, asterisks, exclamation marks, and backslashes.
  • Updated ModifyPasswd to treat empty password or username as invalid parameters and to return an explicit error when the password hash fails validation.
accounts1/users/prop.go
Rework ModifyPasswd to improve process isolation, sensitive data handling, and error reporting when invoking chpasswd.
  • Switched ModifyPasswd to use an explicit StdinPipe instead of bytes.NewBufferString for passing "username:hash" to chpasswd, and cleared the temporary input buffer after writing.
  • Cleared the child process environment (cmd.Env = []string{}) before executing pwdCmdModify to reduce ambient-information leakage.
  • Replaced detailed error propagation (including stderr) with a generic error message on command failure to limit information disclosure, while still surfacing failures to callers.
  • Ensured proper sequencing of cmd.Start, stdin writes, stdin.Close, and cmd.Wait, with process kill on write failures to avoid leaving a hanging child process.
accounts1/users/prop.go

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link
Copy Markdown

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

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

Hey - I've found 1 security issue, and left some high level feedback:

Security issues:

  • Detected non-static command inside Write. Audit the input to 'stdin.Write'. If unverified user data can reach this call site, this is a code injection vulnerability. A malicious actor can inject a malicious script to execute arbitrary code. (link)

General comments:

  • Consider making IsValidCryptHash unexported (e.g., isValidCryptHash) since it is only used within this file and does not appear to be part of the public API surface.
  • After calling cmd.Process.Kill() on a write error, you should still call cmd.Wait() to ensure the process is reaped and avoid leaving a zombie process.
  • The buffer zeroization logic on input is unlikely to meaningfully improve secrecy because the password still exists in other allocations (e.g., the original words string), so you may want to either strengthen this pattern consistently or remove it to avoid a false sense of security.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- Consider making `IsValidCryptHash` unexported (e.g., `isValidCryptHash`) since it is only used within this file and does not appear to be part of the public API surface.
- After calling `cmd.Process.Kill()` on a write error, you should still call `cmd.Wait()` to ensure the process is reaped and avoid leaving a zombie process.
- The buffer zeroization logic on `input` is unlikely to meaningfully improve secrecy because the password still exists in other allocations (e.g., the original `words` string), so you may want to either strengthen this pattern consistently or remove it to avoid a false sense of security.

## Individual Comments

### Comment 1
<location path="accounts1/users/prop.go" line_range="208" />
<code_context>
	_, writeErr := stdin.Write(input)
</code_context>
<issue_to_address>
**security (go.lang.security.audit.dangerous-command-write):** Detected non-static command inside Write. Audit the input to 'stdin.Write'. If unverified user data can reach this call site, this is a code injection vulnerability. A malicious actor can inject a malicious script to execute arbitrary code.

*Source: opengrep*
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread accounts1/users/prop.go
@ComixHe ComixHe force-pushed the master branch 2 times, most recently from 9e6c625 to 056874a Compare May 22, 2026 10:02
@deepin-bot
Copy link
Copy Markdown
Contributor

deepin-bot Bot commented May 22, 2026

TAG Bot

New tag: 6.1.91
DISTRIBUTION: unstable
Suggest: synchronizing this PR through rebase #1121

Comment thread accounts1/users/prop.go Outdated
@ComixHe ComixHe requested a review from UTsweetyfish May 25, 2026 03:48
@ComixHe ComixHe force-pushed the master branch 3 times, most recently from d184d3e to d92cc63 Compare May 25, 2026 06:23
Copy link
Copy Markdown
Member

@UTsweetyfish UTsweetyfish left a comment

Choose a reason for hiding this comment

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

不会 golang,但感觉是不是可以直接用正则啊
以及感觉这里可以加点单元测试来 cover

Comment thread accounts1/users/prop.go
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.

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

Copy link
Copy Markdown
Contributor Author

@ComixHe ComixHe May 25, 2026

Choose a reason for hiding this comment

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

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

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 25, 2026

CLA Assistant Lite bot All contributors have signed the CLA ✍️ ✅

@UTsweetyfish
Copy link
Copy Markdown
Member

3. Fuzz 测试中的 t.Fatalf 使用不当
FuzzIsValidUsernameFuzzIsValidCryptHash 中,当发现校验器放行了非法输入时,使用了 t.Fatalf。在 Go 的 Fuzz 引擎中,t.Fatalf 会立即停止当前协程,这可能导致 Fuzz 引擎无法正确记录导致崩溃的输入语料库。
改进建议:将 Fuzz 测试中的 t.Fatalf 替换为 t.Errorft.Fail,确保 Fuzz 引擎能正常捕获和记录错误输入。

これ

@ComixHe ComixHe force-pushed the master branch 2 times, most recently from 429bf52 to c68b6da Compare May 25, 2026 07:12
Comment thread accounts1/users/prop.go
return nil
}

func ModifyPasswd(words, username string) error {
Copy link
Copy Markdown
Member

@UTsweetyfish UTsweetyfish May 25, 2026

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上所以不用(

@ComixHe
Copy link
Copy Markdown
Contributor Author

ComixHe commented May 25, 2026

  1. Fuzz 测试中的 t.Fatalf 使用不当
    FuzzIsValidUsernameFuzzIsValidCryptHash 中,当发现校验器放行了非法输入时,使用了 t.Fatalf。在 Go 的 Fuzz 引擎中,t.Fatalf 会立即停止当前协程,这可能导致 Fuzz 引擎无法正确记录导致崩溃的输入语料库。
    改进建议:将 Fuzz 测试中的 t.Fatalf 替换为 t.Errorft.Fail,确保 Fuzz 引擎能正常捕获和记录错误输入。

これ

这个ai出现幻觉了。

在golang的fuzz的Command line output写了:

While fuzzing is in progress, the fuzzing engine generates new inputs and runs them against the provided fuzz target. By default, it continues to run until a failing input is found, or the user cancels the process (e.g. with Ctrl^C).

failing-input里有一个定义是:

The fuzz target called t.Fail, either directly or through methods such as t.Error or t.Fatal.

所以这里可以直接fatal

@UTsweetyfish
Copy link
Copy Markdown
Member

lgtm,至少我看不出问题了

ComixHe added 2 commits May 25, 2026 15:31
Validate password crypt hashes and username before invoking
chpasswd to reject invalid characters and malformed input
per crypt(5) and useradd's username validation rules.

Also improve process isolation and sensitive data handling by clearing
the child environment, switching to an explicit stdin pipe flow.

Signed-off-by: Yuming He <heyuming@deepin.org>
…tHash

Cover all 16 crypt(5) hash methods, forbidden delimiter
positions, and shadow-utils username rules with table-driven
and fuzz tests.

Signed-off-by: Yuming He <heyuming@deepin.org>
@deepin-ci-robot
Copy link
Copy Markdown

deepin pr auto review

你好!我是CodeGeeX。我已经仔细审查了你提供的Git Diff。这次代码变更主要对用户名和密码哈希的修改逻辑进行了重构,引入了严格的输入验证、环境变量清理以及更安全的进程间通信(IPC)方式,整体上极大地提升了代码的安全性和健壮性

以下是我对语法逻辑、代码质量、代码性能和代码安全四个方面的详细审查意见和改进建议:

一、 语法与逻辑

  1. 用户名验证逻辑漏洞(全为连字符的判断)
    isValidUsername 中,strings.Trim(name, "-") == "" 这行代码的意图是判断用户名是否全部由连字符组成。但是,如果用户名是 "-a-"Trim 之后会变成 "a",不会触发该错误;然而,如果用户名是 "--"Trim 后变成空字符串,会正确触发错误。
    问题在于shadowchkname.c 中,仅仅是不允许以连字符 - 开头,并没有禁止全为连字符的规则(因为只要开头不是 -,后续字符 - 是合法的,比如 "a--")。如果你确实想保留“不能全为连字符”的规则,建议改用遍历判断;如果是为了对齐 shadow 规范,建议删除此判断,仅保留首字符不能为 - 的判断。

  2. 用户名验证逻辑漏洞(全为数字的误判)
    isValidUsername 中,isAllDigit 的判断逻辑存在缺陷。如果首字符是 _.(例如 ".123""_123"),由于首字符不是数字,isAllDigit 初始化为 false,后续循环中即使全为数字也不会将其置为 true,从而绕过了“不能全为数字”的检查。虽然 shadowNAME_REGEX 允许 .123 这样的用户名,但如果你业务上需要严格限制,需修正此逻辑。

  3. CGO 与跨平台编译
    新增的 user.go 使用了 CGO (import "C") 来调用 sysconf。这会导致该包失去 Go 的交叉编译能力(需要 C 工具链)。如果项目仅限 Linux 运行则无妨,但如果需要交叉编译,建议改用纯 Go 实现,如读取 /proc/sys/kernel/ngroups_max 或使用 golang.org/x/sys/unix 包中的相关常量。

二、 代码质量

  1. 错误信息不一致

    • 旧代码中 errInvalidParam 被移除,但新代码中 isValidUsername 返回的错误是硬编码的 errors.New,而 ModifyPasswd 中又用 %w 包装了它。建议将校验错误定义为包级变量(类似旧的 errInvalidParam),或者统一使用 fmt.Errorf,保持错误处理风格一致。
    • ModifyPasswd 中最后的错误信息:"failed to update system password configuration %s" 缺少冒号或换行,与前面的错误风格不统一,建议改为 "failed to update system password configuration: %s"
  2. 魔数
    isValidCryptHash 中的 32126 属于 ASCII 码的可打印范围边界。建议定义为常量,提高可读性:

    const (
        asciiPrintableMin = 32  // 空格
        asciiPrintableMax = 126 // 波浪号 ~
    )
  3. 测试代码中的 t.Fatalf 误用
    在 Fuzz 测试(FuzzIsValidUsername 等)中,当发现非法输入被错误接受时,使用了 t.Fatalf。在 Fuzz 测试中,一旦发现逻辑错误应立即停止引擎,正确的做法是使用 t.Errorft.Fatalf,但 Go Fuzz 引擎更推荐直接 panic 或返回错误,不过这里用 t.Fatalf 也能工作,只是会终止当前协程。更关键的是,Fuzz 测试中的冗余检查逻辑(重新校验一遍规则)增加了维护成本,如果修改了主逻辑,Fuzz 的检查逻辑也要同步修改。

三、 代码性能

  1. strings.Trim 性能开销
    如前所述,strings.Trim(name, "-") == "" 会创建一个新的字符串并分配内存。如果仅为了判断是否全为连字符,或者判断是否以连字符开头,直接遍历或判断 name[0] 性能更高(零内存分配)。

  2. bytes.NewBuffer 内存分配
    ModifyPasswd 中:

    buf := bytes.NewBuffer(make([]byte, 0, len(username)+len(words)+2))

    这里预分配了内存,非常好。但可以更简洁,直接使用 []byte 拼接,避免 Buffer 结构体的额外开销:

    input := append([]byte(username), ':')
    input = append(input, words...)
    input = append(input, '\n')

    这样更直接,且在栈上就能完成部分分配。

四、 代码安全

  1. 优秀的改进点

    • 严格的白名单校验:参考 shadowcrypt(5) 进行字符集限制,极大地防范了命令注入和字段截断(如 :\n)。
    • 环境变量清理cmd.Env = []string{} 是非常亮眼的改进,防止了环境变量劫持(如 PATH 注入)。
    • 管道写入:将 cmd.Stdin = bytes.NewBufferString 改为 StdinPipe,避免了在进程参数或长缓冲区中驻留敏感数据,更安全。
  2. 敏感数据残留
    代码注释中写道:// no need to erase this data, because this hash already exist in go string。这确实符合 Go 的字符串不可变特性,我们无法清零一个 Go string。但是,buf ([]byte) 是可以清零的。为了极致的安全(防止内存转储泄露),建议在写入完毕后清零 buf

    for i := range input {
        input[i] = 0
    }
  3. 进程异常退出时的僵尸进程/资源泄露
    stdin.Write 失败时,代码执行了:

    _ = cmd.Process.Kill()
    _ = cmd.Wait()

    这里的逻辑是正确的,必须调用 Wait 以防止僵尸进程。但有一个时序竞争问题:如果 chpasswd 读取速度很快,进程已经自行退出,此时 Kill 会返回错误(进程不存在),这无伤大雅;但如果 Write 是因为 context 取消或其他原因失败,且进程尚未启动完毕,直接 Kill 可能会失败。更稳妥的做法是忽略 Kill 的错误(你已经用 _ 忽略了,这很好),并确保 Wait 一定被调用。


改进后的代码建议

针对以上审查,我为你优化了 prop.go 中的相关函数:

// prop.go 中的常量与校验函数改进

const (
	asciiPrintableMin = 32
	asciiPrintableMax = 126
)

// isValidCryptHash validates the format of a crypt password hash string.
func isValidCryptHash(hash string) error {
	if hash == "" {
		return errors.New("password hash is empty")
	}

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

		if b < asciiPrintableMin || b > asciiPrintableMax {
			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.
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")
	}

	// 检查首字符,shadow 规范不允许以 '-' 开头
	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 可能在此处保持为 true
		} else {
			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 {
	if words == "" || username == "" {
		return errors.New("password hash or username is empty")
	}

	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")
	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

	if err := cmd.Start(); err != nil {
		return fmt.Errorf("failed to start command: %w", err)
	}

	// 使用更轻量的 []byte 拼接
	input := append([]byte(username), ':')
	input = append(input, words...)
	input = append(input, '\n')

	_, writeErr := stdin.Write(input)
	
	// 安全考虑:清零内存中的密码痕迹
	for i := range input {
		input[i] = 0
	}
	
	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
}

总结:这次代码变更质量很高,安全意识很强,特别是在输入验证和环境清理方面做得很专业。修复上述几个小逻辑和性能细节后,代码将更加健壮可靠。

@deepin-ci-robot
Copy link
Copy Markdown

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by: ComixHe, fly602, UTsweetyfish

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@ComixHe ComixHe merged commit c355768 into linuxdeepin:master May 25, 2026
18 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants