mirror of
https://github.com/bitechdev/ResolveSpec.git
synced 2026-10-06 13:26:28 +00:00
fix(security): verify passwords, bind row-security args, fail closed on panic
Verify bcrypt passwords in Direct mode and the shipped procedures, hash on register/reset, ignore client-supplied roles and level at registration and drop the password from the jwt_login payload. Legacy cleartext upgrade is opt-in. Row security templates now bind the user as a parameter, validate identifiers, attach via common.SelectQuery and fail the request if the filter cannot be attached. ApplyColumnSecurity and GetRowSecurityTemplate convert panics to errors and the hooks fail closed. Update audit status.
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
package security
|
||||
|
||||
import (
|
||||
"crypto/subtle"
|
||||
"errors"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
// bcrypt only considers the first 72 bytes of input; longer passwords are
|
||||
// rejected rather than silently truncated.
|
||||
const maxPasswordBytes = 72
|
||||
|
||||
var errPasswordTooLong = errors.New("password must be at most 72 bytes")
|
||||
|
||||
func hashPassword(password string) (string, error) {
|
||||
if len(password) > maxPasswordBytes {
|
||||
return "", errPasswordTooLong
|
||||
}
|
||||
h, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(h), nil
|
||||
}
|
||||
|
||||
func isBcryptHash(s string) bool {
|
||||
return strings.HasPrefix(s, "$2a$") || strings.HasPrefix(s, "$2b$") || strings.HasPrefix(s, "$2y$")
|
||||
}
|
||||
|
||||
// verifyPassword checks supplied against the stored value. A stored bcrypt hash
|
||||
// is compared with bcrypt. A legacy cleartext value (written before hashing was
|
||||
// implemented) is compared in constant time and, on a match, needsRehash is true
|
||||
// so the caller can upgrade the row to a bcrypt hash. An empty stored value
|
||||
// (e.g. an OAuth2-only user) never matches.
|
||||
func verifyPassword(stored, supplied string) (ok, needsRehash bool) {
|
||||
if stored == "" || supplied == "" || len(supplied) > maxPasswordBytes {
|
||||
return false, false
|
||||
}
|
||||
if isBcryptHash(stored) {
|
||||
return bcrypt.CompareHashAndPassword([]byte(stored), []byte(supplied)) == nil, false
|
||||
}
|
||||
if subtle.ConstantTimeCompare([]byte(stored), []byte(supplied)) == 1 {
|
||||
return true, true
|
||||
}
|
||||
return false, false
|
||||
}
|
||||
|
||||
var (
|
||||
dummyHashOnce sync.Once
|
||||
dummyHash string
|
||||
)
|
||||
|
||||
// burnPasswordCheck spends roughly one bcrypt comparison so an unknown username
|
||||
// costs about the same as a wrong password.
|
||||
func burnPasswordCheck(supplied string) {
|
||||
dummyHashOnce.Do(func() {
|
||||
h, _ := bcrypt.GenerateFromPassword([]byte("resolvespec-dummy"), bcrypt.DefaultCost)
|
||||
dummyHash = string(h)
|
||||
})
|
||||
if len(supplied) > maxPasswordBytes {
|
||||
supplied = supplied[:maxPasswordBytes]
|
||||
}
|
||||
_ = bcrypt.CompareHashAndPassword([]byte(dummyHash), []byte(supplied))
|
||||
}
|
||||
Reference in New Issue
Block a user