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
+}