@marcel91200 / caddy Mirror · read-only / commits / 69ec5dfe
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, 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 +}