From 2a99b14f9e5e2568447b6d719157c4be0e5d1cce Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Fri, 6 Feb 2026 17:56:16 +0100 Subject: [PATCH 1/2] cli/command/registry: refactor reading from stdin Extract the code as a utility function, and add some GoDoc to describe the behavior. Signed-off-by: Sebastiaan van Stijn --- cli/command/registry/login.go | 41 ++++++++++++++++++++++++++++++----- 1 file changed, 35 insertions(+), 6 deletions(-) diff --git a/cli/command/registry/login.go b/cli/command/registry/login.go index 913c000c4cc2..364107a238ba 100644 --- a/cli/command/registry/login.go +++ b/cli/command/registry/login.go @@ -1,11 +1,11 @@ package registry import ( + "bytes" "context" "errors" "fmt" "io" - "strings" "github.com/containerd/errdefs" "github.com/docker/cli/cli" @@ -88,6 +88,38 @@ func verifyLoginFlags(flags *pflag.FlagSet, opts loginOptions) error { return nil } +// readSecretFromStdin reads the secret from r and returns it as a string. +// It trims terminal line-endings (LF, CRLF, or CR), which may be added when +// inputting interactively or piping input. The value is otherwise treated as +// opaque, preserving any other whitespace, including newlines, per [NIST SP 800-63B §5.1.1.2]. +// Note that trimming whitespace may still happen elsewhere (see [NIST SP 800-63B (revision 4) §3.1.1.2]); +// +// > Verifiers **MAY** make limited allowances for mistyping (e.g., removing +// > leading and trailing whitespace characters before verification, allowing +// > the verification of passwords with differing cases for the leading character) +// +// [NIST SP 800-63B §5.1.1.2]: https://pages.nist.gov/800-63-3/sp800-63b.html#memsecretver +// [NIST SP 800-63B (revision 4) §3.1.1.2]: https://pages.nist.gov/800-63-4/sp800-63b.html#passwordver +func readSecretFromStdin(r io.Reader) (string, error) { + b, err := io.ReadAll(r) + if err != nil { + return "", err + } + if len(b) == 0 { + return "", nil + } + + for _, eol := range [][]byte{[]byte("\r\n"), []byte("\n"), []byte("\r")} { + var ok bool + b, ok = bytes.CutSuffix(b, eol) + if ok { + break + } + } + + return string(b), nil +} + func verifyLoginOptions(dockerCLI command.Streams, opts *loginOptions) error { if opts.password != "" { _, _ = fmt.Fprintln(dockerCLI.Err(), "WARNING! Using --password via the CLI is insecure. Use --password-stdin.") @@ -97,14 +129,11 @@ func verifyLoginOptions(dockerCLI command.Streams, opts *loginOptions) error { if opts.user == "" { return errors.New("username is empty") } - - contents, err := io.ReadAll(dockerCLI.In()) + p, err := readSecretFromStdin(dockerCLI.In()) if err != nil { return err } - - opts.password = strings.TrimSuffix(string(contents), "\n") - opts.password = strings.TrimSuffix(opts.password, "\r") + opts.password = p } return nil } From 12275568caf412646fb188de5904b1ff9772f8f7 Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Fri, 6 Feb 2026 17:56:16 +0100 Subject: [PATCH 2/2] cli/command/registry: preserve all whitespace in secrets MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Preserve all whitespace and treat the secret as an opaque value, leaving it to the registry to (in)validate. We still check for empty values in some places. This partially reverts a21a5f42433267052441cdb7ebe7604dfcc7f159, but checks for empty (whitespace-only) passwords without mutating the value. This better aligns with [NIST SP 800-63B §5.1.1.2], which describes that the value should be treated as opaque, preserving any other whitespace, including newlines. Note that trimming whitespace may still happen elsewhere (see [NIST SP 800-63B (revision 4) §3.1.1.2]); > Verifiers **MAY** make limited allowances for mistyping (e.g., removing > leading and trailing whitespace characters before verification, allowing > the verification of passwords with differing cases for the leading character) [NIST SP 800-63B §5.1.1.2]: https://pages.nist.gov/800-63-3/sp800-63b.html#memsecretver [NIST SP 800-63B (revision 4) §3.1.1.2]: https://pages.nist.gov/800-63-4/sp800-63b.html#passwordver Signed-off-by: Sebastiaan van Stijn --- cli/command/registry.go | 4 +-- cli/command/registry/login_test.go | 50 ++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 2 deletions(-) diff --git a/cli/command/registry.go b/cli/command/registry.go index 452e2d7354cc..75b979e18561 100644 --- a/cli/command/registry.go +++ b/cli/command/registry.go @@ -144,8 +144,8 @@ func PromptUserForCredentials(ctx context.Context, cli Cli, argUser, argPassword } } - argPassword = strings.TrimSpace(argPassword) - if argPassword == "" { + isEmpty := strings.TrimSpace(argPassword) == "" + if isEmpty { restoreInput, err := prompt.DisableInputEcho(cli.In()) if err != nil { return registrytypes.AuthConfig{}, err diff --git a/cli/command/registry/login_test.go b/cli/command/registry/login_test.go index 072a72feabf6..6f4fe6c81af3 100644 --- a/cli/command/registry/login_test.go +++ b/cli/command/registry/login_test.go @@ -306,6 +306,56 @@ func TestRunLogin(t *testing.T) { }, }, }, + { + doc: "password with leading and trailing spaces", + priorCredentials: map[string]configtypes.AuthConfig{}, + input: loginOptions{ + serverAddress: "reg1", + user: "my-username", + password: " my password with spaces ", + }, + expectedCredentials: map[string]configtypes.AuthConfig{ + "reg1": { + Username: "my-username", + Password: " my password with spaces ", + ServerAddress: "reg1", + }, + }, + }, + { + doc: "password stdin with line-endings", + priorCredentials: map[string]configtypes.AuthConfig{}, + stdIn: " my password with spaces \r\n", + input: loginOptions{ + serverAddress: "reg1", + user: "my-username", + passwordStdin: true, + }, + expectedCredentials: map[string]configtypes.AuthConfig{ + "reg1": { + Username: "my-username", + Password: " my password with spaces ", + ServerAddress: "reg1", + }, + }, + }, + { + doc: "password stdin with multiple line-endings", + priorCredentials: map[string]configtypes.AuthConfig{}, + stdIn: " my password\nwith spaces \r\n\r\n", + input: loginOptions{ + serverAddress: "reg1", + user: "my-username", + passwordStdin: true, + }, + expectedCredentials: map[string]configtypes.AuthConfig{ + "reg1": { + Username: "my-username", + Password: " my password\nwith spaces \r\n", + ServerAddress: "reg1", + }, + }, + }, } for _, tc := range testCases {