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, 04:45 UTC
caddyauth: only replace known placeholders in basic auth credentials (#8017)
The basic auth provider expanded account usernames and passwords with repl.ReplaceAll, which blanks any {...} the replacer does not recognize Credentials are config data, so a username like alice-{bogus} was stored as alice- and a password like ab{cd}ef was quietly rewritten into different valid base64, turning a config mistake into a working password the admin never set Use ReplaceKnown, matching the respond headers fix in #8014 and the header handler fix in #4880, so unknown braces survive and real placeholders like {env.*} still expand
Abdellatif Anaflous committed
Sep 26, 2026 at 09:52 UTC
69ec5dfedf5ba8f7d042f8f2ff41baa5ce576ce8
2 files changed
+111
-2
modules/caddyhttp/caddyauth/basicauth.go
+2
-2
@@ -106,8 +106,8 @@ func (hba *HTTPBasicAuth) Provision(ctx caddy.Context) error {
106
return fmt.Errorf("account %d: username is not unique: %s", i, acct.Username)
107
}
108
109
- acct.Username = repl.ReplaceAll(acct.Username, "")
110
- acct.Password = repl.ReplaceAll(acct.Password, "")
109
+ acct.Username = repl.ReplaceKnown(acct.Username, "")
110
+ acct.Password = repl.ReplaceKnown(acct.Password, "")
111
112
if acct.Username == "" || acct.Password == "" {
113
return fmt.Errorf("account %d: username and password are required", i)
modules/caddyhttp/caddyauth/basicauth_test.go
new
+109
@@ -0,0 +1,109 @@
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
+ "context"
19
+ "net/http"
20
+ "net/http/httptest"
21
+ "sort"
22
+ "testing"
23
+
24
+ "github.com/caddyserver/caddy/v2"
25
+)
26
+
27
+// testBasicAuthHash is a bcrypt hash (cost 4) of the plaintext "correct horse".
28
+const testBasicAuthHash = "$2a$04$XziKIb5ayw0suaNwlK.1vOqOj9gEveGnkq.anOrdVrnkN5z1bA2A."
29
+
30
+func TestBasicAuthProvisionKeepsUnknownPlaceholdersInUsername(t *testing.T) {
31
+ hba := HTTPBasicAuth{
32
+ AccountList: []Account{
33
+ {Username: "alice-{not-a-placeholder}", Password: testBasicAuthHash},
34
+ },
35
+ }
36
+ ctx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()})
37
+ defer cancel()
38
+ if err := hba.Provision(ctx); err != nil {
39
+ t.Fatalf("provisioning failed: %v", err)
40
+ }
41
+
42
+ if _, ok := hba.Accounts["alice-{not-a-placeholder}"]; !ok {
43
+ t.Fatalf("expected an account for the username as configured, got accounts %q", accountUsernames(hba))
44
+ }
45
+
46
+ req := httptest.NewRequest(http.MethodGet, "/", nil)
47
+ req.SetBasicAuth("alice-{not-a-placeholder}", "correct horse")
48
+ user, authenticated, err := hba.Authenticate(httptest.NewRecorder(), req)
49
+ if err != nil {
50
+ t.Fatalf("unexpected authentication error: %v", err)
51
+ }
52
+ if !authenticated {
53
+ t.Fatalf("expected authentication to succeed for the username as configured, got accounts %q", accountUsernames(hba))
54
+ }
55
+ if user.ID != "alice-{not-a-placeholder}" {
56
+ t.Fatalf("expected user ID %q, got %q", "alice-{not-a-placeholder}", user.ID)
57
+ }
58
+}
59
+
60
+func TestBasicAuthProvisionExpandsKnownPlaceholdersInUsername(t *testing.T) {
61
+ t.Setenv("CADDYTEST_BASICAUTH_USER", "bob")
62
+
63
+ hba := HTTPBasicAuth{
64
+ AccountList: []Account{
65
+ {Username: "{env.CADDYTEST_BASICAUTH_USER}", Password: testBasicAuthHash},
66
+ },
67
+ }
68
+ ctx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()})
69
+ defer cancel()
70
+ if err := hba.Provision(ctx); err != nil {
71
+ t.Fatalf("provisioning failed: %v", err)
72
+ }
73
+
74
+ req := httptest.NewRequest(http.MethodGet, "/", nil)
75
+ req.SetBasicAuth("bob", "correct horse")
76
+ _, authenticated, err := hba.Authenticate(httptest.NewRecorder(), req)
77
+ if err != nil {
78
+ t.Fatalf("unexpected authentication error: %v", err)
79
+ }
80
+ if !authenticated {
81
+ t.Fatalf("expected the known placeholder to expand, got accounts %q", accountUsernames(hba))
82
+ }
83
+}
84
+
85
+func TestBasicAuthProvisionRejectsBracedPasswordInsteadOfRewritingIt(t *testing.T) {
86
+ hba := HTTPBasicAuth{
87
+ AccountList: []Account{
88
+ // a password field carries a hash (MCF or base64), neither of which
89
+ // can contain braces, so a braced value can only be a mistake and
90
+ // must not be quietly rewritten into different valid base64
91
+ {Username: "alice", Password: "ab{cd}ef"},
92
+ },
93
+ }
94
+ ctx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()})
95
+ defer cancel()
96
+ err := hba.Provision(ctx)
97
+ if err == nil {
98
+ t.Fatalf("expected provisioning to fail for a password with literal braces, instead it accepted the rewritten password %q", hba.Accounts["alice"].password)
99
+ }
100
+}
101
+
102
+func accountUsernames(hba HTTPBasicAuth) []string {
103
+ names := make([]string, 0, len(hba.Accounts))
104
+ for name := range hba.Accounts {
105
+ names = append(names, name)
106
+ }
107
+ sort.Strings(names)
108
+ return names
109
+}