Author: Julien Pivotto <roidelapluie@o11y.eu>
Date:   Tue Nov 29 10:22:49 2022 +0100
Forwarded: not-needed
Last-Updated: Mon, 19 Dec 2022 20:11:12 +0000
Description:
 Backport of upstream commits 2528877 and 0af5c3f:

    Merge pull request from GHSA-7rg2-cxvp-9p7p
    
    * Fix authentication bypass if stored password hash is known
    
    Signed-off-by: Julien Pivotto <roidelapluie@o11y.eu>
    
    * Add test for CVE-2022-46146
    
    Signed-off-by: Julien Pivotto <roidelapluie@o11y.eu>

    * Fix tests
    
    Signed-off-by: Julien Pivotto <roidelapluie@o11y.eu>

--- a/web/users.go
+++ b/web/users.go
@@ -18,6 +18,7 @@
 import (
 	"encoding/hex"
 	"net/http"
+	"strings"
 	"sync"
 
 	"github.com/go-kit/kit/log"
@@ -74,7 +75,12 @@
 			hashedPassword = "$2y$10$QOauhQNbBCuQDKes6eFzPeMqBSjb7Mr5DUmpZ/VcEd00UAV/LDeSi"
 		}
 
-		cacheKey := hex.EncodeToString(append(append([]byte(user), []byte(hashedPassword)...), []byte(pass)...))
+		cacheKey := strings.Join(
+			[]string{
+				hex.EncodeToString([]byte(user)),
+				hex.EncodeToString([]byte(hashedPassword)),
+				hex.EncodeToString([]byte(pass)),
+			}, ":")
 		authOk, ok := u.cache.get(cacheKey)
 
 		if !ok {
@@ -83,7 +89,7 @@
 			err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(pass))
 			u.bcryptMtx.Unlock()
 
-			authOk = err == nil
+			authOk = validUser && err == nil
 			u.cache.set(cacheKey, authOk)
 		}
 
--- a/web/users_test.go
+++ b/web/users_test.go
@@ -131,3 +131,47 @@
 	// Login with the response cached.
 	login()
 }
+
+// TestByPassBasicAuthVuln tests for CVE-2022-46146.
+func TestByPassBasicAuthVuln(t *testing.T) {
+	server := &http.Server{
+		Addr: port,
+		Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+			w.Write([]byte("Hello World!"))
+		}),
+	}
+
+	done := make(chan struct{})
+	t.Cleanup(func() {
+		if err := server.Shutdown(context.Background()); err != nil {
+			t.Fatal(err)
+		}
+		<-done
+	})
+
+	go func() {
+		ListenAndServe(server, "testdata/web_config_users_noTLS.good.yml", testlogger)
+		close(done)
+	}()
+
+	login := func(username, password string) {
+		client := &http.Client{}
+		req, err := http.NewRequest("GET", "http://localhost"+port, nil)
+		if err != nil {
+			t.Fatal(err)
+		}
+		req.SetBasicAuth(username, password)
+		r, err := client.Do(req)
+		if err != nil {
+			t.Fatal(err)
+		}
+		if r.StatusCode != 401 {
+			t.Fatalf("bad return code, expected %d, got %d", 401, r.StatusCode)
+		}
+	}
+
+	// Poison the cache.
+	login("alice$2y$12$1DpfPeqF9HzHJt.EWswy1exHluGfbhnn3yXhR7Xes6m3WJqFg0Wby", "fakepassword")
+	// Login with a wrong password.
+	login("alice", "$2y$10$QOauhQNbBCuQDKes6eFzPeMqBSjb7Mr5DUmpZ/VcEd00UAV/LDeSifakepassword")
+}
--- /dev/null
+++ b/web/testdata/web_config_users_noTLS.good.yml
@@ -0,0 +1,5 @@
+basic_auth_users:
+  alice: $2y$12$1DpfPeqF9HzHJt.EWswy1exHluGfbhnn3yXhR7Xes6m3WJqFg0Wby
+  bob: $2y$18$4VeFDzXIoPHKnKTU3O3GH.N.vZu06CVqczYZ8WvfzrddFU6tGqjR.
+  carol: $2y$10$qRTBuFoULoYNA7AQ/F3ck.trZBPyjV64.oA4ZsSBCIWvXuvQlQTuu
+  dave: $2y$10$2UXri9cIDdgeKjBo4Rlpx.U3ZLDV8X1IxKmsfOvhcM5oXQt/mLmXq
