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, 01:00 UTC
caddyauth: Don't write into shared hash buffer when building cache key (#8278)
HTTPBasicAuth.correctPassword built its hash cache key with append(account.password, plaintextPassword...). account.password is shared by all requests and can have spare capacity, so short passwords were written into the stored hash's backing array, causing a data race between concurrent requests and potentially mixed-up cache keys. Build the key in a fresh buffer instead, and add regression tests.
Venkatesh Panda committed
Nov 3, 2026 at 13:51 UTC
f89626aa55e11f4a29a00b735c7a0de3a9a2ef47
2 files changed
+57
-1
modules/caddyhttp/caddyauth/basicauth.go
+4
-1
@@ -21,6 +21,7 @@ import (
21
"fmt"
22
weakrand "math/rand/v2"
23
"net/http"
24
+ "slices"
25
"strings"
26
"sync"
27
@@ -173,7 +174,9 @@ func (hba HTTPBasicAuth) correctPassword(account Account, plaintextPassword []by
174
}
175
176
// compute a cache key that is unique for these input parameters
176
- cacheKey := hex.EncodeToString(append(account.password, plaintextPassword...))
177
+ // (build it in a new buffer: appending to account.password could write
178
+ // into its spare capacity, which is shared by all concurrent requests)
179
+ cacheKey := hex.EncodeToString(slices.Concat(account.password, plaintextPassword))
180
181
// fast track: if the result of the input is already cached, use it
182
hba.HashCache.mu.RLock()
modules/caddyhttp/caddyauth/basicauth_test.go
+53
@@ -19,6 +19,7 @@ import (
19
"net/http"
20
"net/http/httptest"
21
"sort"
22
+ "sync"
23
"testing"
24
25
"github.com/caddyserver/caddy/v2"
@@ -140,3 +141,55 @@ func accountUsernames(hba HTTPBasicAuth) []string {
141
sort.Strings(names)
142
return names
143
}
144
+
145
+func newCachedBasicAuth(t *testing.T) HTTPBasicAuth {
146
+ t.Helper()
147
+ hba := HTTPBasicAuth{
148
+ AccountList: []Account{{Username: "alice", Password: testBasicAuthHash}},
149
+ HashCache: new(Cache),
150
+ }
151
+ ctx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()})
152
+ t.Cleanup(cancel)
153
+ if err := hba.Provision(ctx); err != nil {
154
+ t.Fatalf("provisioning failed: %v", err)
155
+ }
156
+ return hba
157
+}
158
+
159
+func TestBasicAuthHashCacheDoesNotModifyStoredHash(t *testing.T) {
160
+ hba := newCachedBasicAuth(t)
161
+ account := hba.Accounts["alice"]
162
+
163
+ // inspect the full backing array, including any spare capacity
164
+ before := append([]byte(nil), account.password[:cap(account.password)]...)
165
+
166
+ if _, err := hba.correctPassword(account, []byte("abcd")); err != nil {
167
+ t.Fatalf("unexpected error: %v", err)
168
+ }
169
+
170
+ after := account.password[:cap(account.password)]
171
+ if string(before) != string(after) {
172
+ t.Fatalf("stored password hash buffer was modified while building the cache key")
173
+ }
174
+}
175
+
176
+func TestBasicAuthHashCacheConcurrentRequests(t *testing.T) {
177
+ hba := newCachedBasicAuth(t)
178
+
179
+ passwords := []string{"correct horse", "aaa", "bbb", "ccc"}
180
+ var wg sync.WaitGroup
181
+ for i := range 64 {
182
+ wg.Add(1)
183
+ go func() {
184
+ defer wg.Done()
185
+ pw := passwords[i%len(passwords)]
186
+ req := httptest.NewRequest(http.MethodGet, "/", nil)
187
+ req.SetBasicAuth("alice", pw)
188
+ _, ok, _ := hba.Authenticate(httptest.NewRecorder(), req)
189
+ if want := pw == "correct horse"; ok != want {
190
+ t.Errorf("password %q: authenticated=%v, want %v", pw, ok, want)
191
+ }
192
+ }()
193
+ }
194
+ wg.Wait()
195
+}