fix(09): WR-12 time a missing admin login at the configured bcrypt cost
This commit is contained in:
@@ -9,6 +9,7 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.golem15.com/golem15/summercms/modules/backpack"
|
"git.golem15.com/golem15/summercms/modules/backpack"
|
||||||
@@ -142,7 +143,7 @@ func (s *service) login(w http.ResponseWriter, r *http.Request) {
|
|||||||
WriteError(w, http.StatusInternalServerError, "error", msgServerError)
|
WriteError(w, http.StatusInternalServerError, "error", msgServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
hash := dummyPasswordHash
|
hash := s.missingUserHash()
|
||||||
if found && user.Password != "" {
|
if found && user.Password != "" {
|
||||||
hash = user.Password
|
hash = user.Password
|
||||||
}
|
}
|
||||||
@@ -496,11 +497,23 @@ func adminIssuer(app *backpack.App, prefix string) string {
|
|||||||
return base + prefix + adminAPIVersion + "/auth/login"
|
return base + prefix + adminAPIVersion + "/auth/login"
|
||||||
}
|
}
|
||||||
|
|
||||||
// dummyPasswordHash keeps a missing-user login on the bcrypt path.
|
// missingHashes caches the unknown-login hash per bcrypt cost.
|
||||||
var dummyPasswordHash = func() string {
|
var missingHashes sync.Map
|
||||||
hash, err := bouncer.HashPassword(10, "cabana-invalid-credentials")
|
|
||||||
if err != nil {
|
// missingUserHash is the hash a login for an unknown (or ambiguous) identifier
|
||||||
return ""
|
// is checked against, so it costs the same bcrypt work as a real admin's. It is
|
||||||
|
// built once per cost at the service's configured cost, the cost stored hashes
|
||||||
|
// are rehashed to on login.
|
||||||
|
func (s *service) missingUserHash() string {
|
||||||
|
cost := s.bcryptCost
|
||||||
|
if cached, ok := missingHashes.Load(cost); ok {
|
||||||
|
return cached.(string)
|
||||||
}
|
}
|
||||||
return hash
|
hash, err := bouncer.HashPassword(cost, "cabana-invalid-credentials")
|
||||||
}()
|
if err != nil {
|
||||||
|
// An unusable cost: fall back to the default, still on the bcrypt path.
|
||||||
|
hash, _ = bouncer.HashPassword(10, "cabana-invalid-credentials")
|
||||||
|
}
|
||||||
|
actual, _ := missingHashes.LoadOrStore(cost, hash)
|
||||||
|
return actual.(string)
|
||||||
|
}
|
||||||
|
|||||||
24
modules/cabana/auth_internal_test.go
Normal file
24
modules/cabana/auth_internal_test.go
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
package cabana
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestMissingUserHashUsesConfiguredCost pins WR-12: a login for an unknown
|
||||||
|
// identifier is checked against a hash at the configured bcrypt cost, so it
|
||||||
|
// costs the same as a real admin's (whose hash is rehashed to that cost).
|
||||||
|
func TestMissingUserHashUsesConfiguredCost(t *testing.T) {
|
||||||
|
for _, cost := range []int{4, 6} {
|
||||||
|
s := &service{bcryptCost: cost}
|
||||||
|
hash := s.missingUserHash()
|
||||||
|
got, err := bcrypt.Cost([]byte(hash))
|
||||||
|
if err != nil || got != cost {
|
||||||
|
t.Fatalf("missing-user hash cost = %d, %v; want %d", got, err, cost)
|
||||||
|
}
|
||||||
|
if again := s.missingUserHash(); again != hash {
|
||||||
|
t.Fatal("missing-user hash was rebuilt instead of reused")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user