Scert/backend/internal/cli/secret_input_linux_test.go
xuans 0788b6e41d 完成Linux敏感输入安全整改
拒绝通过命令行参数和管道传入SMTP敏感值,改用Linux交互式终端隐藏输入,并增加伪终端回显关闭、状态恢复和首尾空格保留测试。
2026-10-05 13:06:34 +08:00

105 lines
2.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//go:build linux
package cli
import (
"bytes"
"fmt"
"io"
"os"
"strings"
"testing"
"time"
"golang.org/x/sys/unix"
)
func openTestPTY(t *testing.T) (master, slave *os.File) {
t.Helper()
masterFD, err := unix.Open("/dev/ptmx", unix.O_RDWR|unix.O_NOCTTY, 0)
if err != nil {
t.Skipf("当前 Linux 环境无法打开伪终端:%v", err)
}
master = os.NewFile(uintptr(masterFD), "pty-master")
t.Cleanup(func() { _ = master.Close() })
if err := unix.IoctlSetInt(masterFD, unix.TIOCSPTLCK, 0); err != nil {
t.Fatal(err)
}
ptyNumber, err := unix.IoctlGetInt(masterFD, unix.TIOCGPTN)
if err != nil {
t.Fatal(err)
}
slave, err = os.OpenFile(fmt.Sprintf("/dev/pts/%d", ptyNumber), os.O_RDWR|unix.O_NOCTTY, 0)
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = slave.Close() })
return master, slave
}
func TestReadSecretInputRejectsPipe(t *testing.T) {
_, err := readSecretInput(strings.NewReader("not-a-secret\n"), io.Discard, "请输入密码")
if err == nil || !strings.Contains(err.Error(), "交互式终端") {
t.Fatalf("管道输入未被拒绝:%v", err)
}
}
func TestReadSecretInputDisablesAndRestoresEcho(t *testing.T) {
master, slave := openTestPTY(t)
before, err := unix.IoctlGetTermios(int(slave.Fd()), unix.TCGETS)
if err != nil {
t.Fatal(err)
}
var output bytes.Buffer
result := make(chan struct {
value string
err error
}, 1)
go func() {
value, err := readSecretInput(slave, &output, "请输入密码")
result <- struct {
value string
err error
}{value: value, err: err}
}()
deadline := time.Now().Add(2 * time.Second)
for {
current, ioctlErr := unix.IoctlGetTermios(int(slave.Fd()), unix.TCGETS)
if ioctlErr == nil && current.Lflag&(unix.ECHO|unix.ECHONL) == 0 {
break
}
if time.Now().After(deadline) {
t.Fatal("读取敏感输入时未关闭终端回显")
}
time.Sleep(5 * time.Millisecond)
}
if _, err := master.Write([]byte(" valid-secret \n")); err != nil {
t.Fatal(err)
}
select {
case got := <-result:
if got.err != nil {
t.Fatalf("伪终端读取失败:%v", got.err)
}
if got.value != " valid-secret " {
t.Fatalf("合法首尾空格未保留:%q", got.value)
}
case <-time.After(2 * time.Second):
t.Fatal("伪终端读取超时")
}
after, err := unix.IoctlGetTermios(int(slave.Fd()), unix.TCGETS)
if err != nil {
t.Fatal(err)
}
if after.Lflag != before.Lflag {
t.Fatalf("终端状态未恢复:before=%#x after=%#x", before.Lflag, after.Lflag)
}
if strings.Contains(output.String(), "valid-secret") {
t.Fatal("敏感值出现在提示输出中")
}
}