@marcel91200 / caddy Mirror · read-only / commits / be5fba4f
Fork 0

Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS

Mirrored from https://github.com/Slazee/caddy.git

Synced Nov 11, 02:15 UTC

caddyauth: handle CRLF and unterminated hash-password input (#8227)

A password piped with a Windows line ending was hashed with the trailing \r, so basic_auth rejected the real password, and input without a final newline failed with EOF.

Jay Hemnani committed Oct 16, 2026 at 17:59 UTC be5fba4ff25f403ce4435395a7de4a9dace65ae9
2 files changed +106 -3
modules/caddyhttp/caddyauth/command.go
+9 -3
@@ -17,7 +17,9 @@ package caddyauth
17 import (
18 "bufio"
19 "bytes"
20 + "errors"
21 "fmt"
22 + "io"
23 "os"
24 "os/signal"
25
@@ -126,12 +128,16 @@ func cmdHashPassword(fs caddycmd.Flags) (int, error) {
128 }
129 } else {
130 rd := bufio.NewReader(os.Stdin)
129 - plaintext, err = rd.ReadBytes('\n')
130 - if err != nil {
131 + line, err := rd.ReadBytes('\n')
132 + if err != nil && !errors.Is(err, io.EOF) {
133 return caddy.ExitCodeFailedStartup, err
134 }
135
134 - plaintext = plaintext[:len(plaintext)-1] // Trailing newline
136 + // drop the line ending (\n or \r\n), if any; a lone trailing \r is kept
137 + if trimmed, ok := bytes.CutSuffix(line, []byte("\n")); ok {
138 + line = bytes.TrimSuffix(trimmed, []byte("\r"))
139 + }
140 + plaintext = line
141 }
142
143 if len(plaintext) == 0 {
modules/caddyhttp/caddyauth/command_test.go new
+97
@@ -0,0 +1,97 @@
1 +// Copyright 2015 Matthew Holt and The Caddy Authors
2 +//
3 +// Licensed under the Apache License, Version 2.0 (the "License");
4 +// you may not use this file except in compliance with the License.
5 +// You may obtain a copy of the License at
6 +//
7 +// http://www.apache.org/licenses/LICENSE-2.0
8 +//
9 +// Unless required by applicable law or agreed to in writing, software
10 +// distributed under the License is distributed on an "AS IS" BASIS,
11 +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 +// See the License for the specific language governing permissions and
13 +// limitations under the License.
14 +
15 +package caddyauth
16 +
17 +import (
18 + "io"
19 + "os"
20 + "strings"
21 + "testing"
22 +
23 + "github.com/spf13/pflag"
24 + "golang.org/x/crypto/bcrypt"
25 +
26 + caddycmd "github.com/caddyserver/caddy/v2/cmd"
27 +)
28 +
29 +func TestHashPasswordFromStdin(t *testing.T) {
30 + for _, algorithm := range []string{bcryptName, argon2idName} {
31 + for _, tc := range []struct{ input, password string }{
32 + {"secret\n", "secret"},
33 + {"secret\r\n", "secret"},
34 + {"secret", "secret"},
35 + {"secret\r", "secret\r"}, // no line ending, so the \r is part of the password
36 + } {
37 + name := algorithm + "/" + strings.NewReplacer("\r", `\r`, "\n", `\n`).Replace(tc.input)
38 + t.Run(name, func(t *testing.T) {
39 + hash := hashPasswordWithStdin(t, algorithm, tc.input)
40 + var ok bool
41 + var err error
42 + if algorithm == bcryptName {
43 + ok, err = BcryptHash{}.Compare([]byte(hash), []byte(tc.password))
44 + } else {
45 + ok, err = Argon2idHash{}.Compare([]byte(hash), []byte(tc.password))
46 + }
47 + if err != nil || !ok {
48 + t.Fatalf("hash %q does not match the password (err: %v)", hash, err)
49 + }
50 + })
51 + }
52 + }
53 +}
54 +
55 +// hashPasswordWithStdin runs the hash-password command with input
56 +// piped to stdin and returns what it wrote to stdout.
57 +func hashPasswordWithStdin(t *testing.T, algorithm, input string) string {
58 + t.Helper()
59 +
60 + stdinR, stdinW, err := os.Pipe()
61 + if err != nil {
62 + t.Fatal(err)
63 + }
64 + stdoutR, stdoutW, err := os.Pipe()
65 + if err != nil {
66 + t.Fatal(err)
67 + }
68 + origStdin, origStdout := os.Stdin, os.Stdout
69 + os.Stdin, os.Stdout = stdinR, stdoutW
70 + defer func() { os.Stdin, os.Stdout = origStdin, origStdout }()
71 +
72 + if _, err := stdinW.WriteString(input); err != nil {
73 + t.Fatal(err)
74 + }
75 + stdinW.Close()
76 +
77 + fs := pflag.NewFlagSet("hash-password", pflag.ContinueOnError)
78 + fs.String("plaintext", "", "")
79 + fs.String("algorithm", algorithm, "")
80 + fs.Int("bcrypt-cost", bcrypt.MinCost, "")
81 + fs.Uint32("argon2id-time", defaultArgon2idTime, "")
82 + fs.Uint32("argon2id-memory", defaultArgon2idMemory, "")
83 + fs.Uint8("argon2id-threads", defaultArgon2idThreads, "")
84 + fs.Uint32("argon2id-keylen", defaultArgon2idKeylen, "")
85 +
86 + code, err := cmdHashPassword(caddycmd.Flags{FlagSet: fs})
87 + stdoutW.Close()
88 + if err != nil {
89 + t.Fatalf("hash-password failed (exit code %d): %v", code, err)
90 + }
91 +
92 + out, err := io.ReadAll(stdoutR)
93 + if err != nil {
94 + t.Fatal(err)
95 + }
96 + return strings.TrimSpace(string(out))
97 +}