Files
Jay Hemnani be5fba4ff2 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.
2026-10-16 22:29:54 +10:00

98 lines
2.9 KiB
Go

// Copyright 2015 Matthew Holt and The Caddy Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package caddyauth
import (
"io"
"os"
"strings"
"testing"
"github.com/spf13/pflag"
"golang.org/x/crypto/bcrypt"
caddycmd "github.com/caddyserver/caddy/v2/cmd"
)
func TestHashPasswordFromStdin(t *testing.T) {
for _, algorithm := range []string{bcryptName, argon2idName} {
for _, tc := range []struct{ input, password string }{
{"secret\n", "secret"},
{"secret\r\n", "secret"},
{"secret", "secret"},
{"secret\r", "secret\r"}, // no line ending, so the \r is part of the password
} {
name := algorithm + "/" + strings.NewReplacer("\r", `\r`, "\n", `\n`).Replace(tc.input)
t.Run(name, func(t *testing.T) {
hash := hashPasswordWithStdin(t, algorithm, tc.input)
var ok bool
var err error
if algorithm == bcryptName {
ok, err = BcryptHash{}.Compare([]byte(hash), []byte(tc.password))
} else {
ok, err = Argon2idHash{}.Compare([]byte(hash), []byte(tc.password))
}
if err != nil || !ok {
t.Fatalf("hash %q does not match the password (err: %v)", hash, err)
}
})
}
}
}
// hashPasswordWithStdin runs the hash-password command with input
// piped to stdin and returns what it wrote to stdout.
func hashPasswordWithStdin(t *testing.T, algorithm, input string) string {
t.Helper()
stdinR, stdinW, err := os.Pipe()
if err != nil {
t.Fatal(err)
}
stdoutR, stdoutW, err := os.Pipe()
if err != nil {
t.Fatal(err)
}
origStdin, origStdout := os.Stdin, os.Stdout
os.Stdin, os.Stdout = stdinR, stdoutW
defer func() { os.Stdin, os.Stdout = origStdin, origStdout }()
if _, err := stdinW.WriteString(input); err != nil {
t.Fatal(err)
}
stdinW.Close()
fs := pflag.NewFlagSet("hash-password", pflag.ContinueOnError)
fs.String("plaintext", "", "")
fs.String("algorithm", algorithm, "")
fs.Int("bcrypt-cost", bcrypt.MinCost, "")
fs.Uint32("argon2id-time", defaultArgon2idTime, "")
fs.Uint32("argon2id-memory", defaultArgon2idMemory, "")
fs.Uint8("argon2id-threads", defaultArgon2idThreads, "")
fs.Uint32("argon2id-keylen", defaultArgon2idKeylen, "")
code, err := cmdHashPassword(caddycmd.Flags{FlagSet: fs})
stdoutW.Close()
if err != nil {
t.Fatalf("hash-password failed (exit code %d): %v", code, err)
}
out, err := io.ReadAll(stdoutR)
if err != nil {
t.Fatal(err)
}
return strings.TrimSpace(string(out))
}