Refuse passwords over 72 bytes and confirm only stored ones (closes #144)
check / check (push) Waiting to run
check / check (push) Waiting to run
bcrypt cannot hash a password longer than 72 bytes. PASS over the HTTP API answered one with a 500, and the IRC listener confirmed it without storing it. Both transports now refuse a password outside 8 to 72 bytes with the reply a short one already got: 461 over the HTTP API, a NOTICE over the IRC listener. setPassword returns its error, and "Password set" is sent only when the password was stored. A PASS given before registration is checked after the welcome burst and gets the same NOTICE. Model: opus-5-5
This commit is contained in:
@@ -1158,6 +1158,131 @@ func TestHandlePassPostRegistration(t *testing.T) {
|
||||
)
|
||||
}
|
||||
|
||||
// maxPasswordBytes is the longest password bcrypt can hash.
|
||||
const maxPasswordBytes = 72
|
||||
|
||||
// passwordLengthNotice is the NOTICE text for a password that
|
||||
// is too short or too long.
|
||||
const passwordLengthNotice = "Password must be 8 to 72 bytes"
|
||||
|
||||
// assertCanLogin fails the test if nick cannot log in with
|
||||
// password.
|
||||
func assertCanLogin(t *testing.T, env *testEnv, nick, password string) {
|
||||
t.Helper()
|
||||
|
||||
_, _, token, err := env.database.LoginUser(
|
||||
t.Context(), nick, password, "127.0.0.1", "localhost",
|
||||
)
|
||||
if err != nil || token == "" {
|
||||
t.Errorf("log in as %s: token %q, %v", nick, token, err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestPassPostRegistrationLength checks that a PASS over
|
||||
// maxPasswordBytes after registration is refused with a
|
||||
// NOTICE, and that one of exactly maxPasswordBytes is stored
|
||||
// and confirmed.
|
||||
func TestPassPostRegistrationLength(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
env := newTestEnv(t)
|
||||
client := env.dial(t)
|
||||
|
||||
client.register("longpass")
|
||||
|
||||
client.sendAndExpect(
|
||||
"PASS :"+strings.Repeat("a", maxPasswordBytes+1),
|
||||
passwordLengthNotice,
|
||||
)
|
||||
|
||||
longest := strings.Repeat("a", maxPasswordBytes)
|
||||
client.sendAndExpect("PASS :"+longest, "Password set")
|
||||
|
||||
assertCanLogin(t, env, "longpass", longest)
|
||||
}
|
||||
|
||||
// TestPassPostRegistrationNotStored checks that PASS is not
|
||||
// confirmed when the password could not be stored.
|
||||
func TestPassPostRegistrationNotStored(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
env := newTestEnv(t)
|
||||
client := env.dial(t)
|
||||
|
||||
client.register("failpass")
|
||||
|
||||
_, err := env.database.GetDB().ExecContext(t.Context(),
|
||||
`CREATE TRIGGER refuse_password
|
||||
BEFORE UPDATE OF password_hash ON sessions
|
||||
BEGIN SELECT RAISE(ABORT, 'refused'); END`,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("create trigger: %v", err)
|
||||
}
|
||||
|
||||
client.send("PASS :mypassword123")
|
||||
|
||||
// The server answers commands in order, so anything it
|
||||
// sent about PASS comes before the PONG.
|
||||
lines := client.sendAndExpect("PING :after", "PONG")
|
||||
|
||||
assertNotContains(
|
||||
t, lines, "Password set",
|
||||
"confirmation of a password that was not stored",
|
||||
)
|
||||
}
|
||||
|
||||
// TestPassBeforeRegistrationLength checks that a PASS before
|
||||
// registration that is too short or too long is refused with
|
||||
// a NOTICE once registration completes.
|
||||
func TestPassBeforeRegistrationLength(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cases := []struct {
|
||||
nick, password string
|
||||
}{
|
||||
{"preshort", "short"},
|
||||
{"prelong", strings.Repeat("a", maxPasswordBytes+1)},
|
||||
}
|
||||
|
||||
env := newTestEnv(t)
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.nick, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
client := env.dial(t)
|
||||
|
||||
client.send("PASS :" + tc.password)
|
||||
client.register(tc.nick)
|
||||
|
||||
client.readUntil(func(line string) bool {
|
||||
return strings.Contains(line, passwordLengthNotice)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestPassBeforeRegistrationStored checks that a PASS of
|
||||
// exactly maxPasswordBytes before registration is stored.
|
||||
func TestPassBeforeRegistrationStored(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
env := newTestEnv(t)
|
||||
client := env.dial(t)
|
||||
|
||||
longest := strings.Repeat("a", maxPasswordBytes)
|
||||
|
||||
client.send("PASS :" + longest)
|
||||
client.register("prestored")
|
||||
|
||||
// The password is stored after the welcome burst; the
|
||||
// PONG comes after that.
|
||||
client.sendAndExpect("PING :after", "PONG")
|
||||
|
||||
assertCanLogin(t, env, "prestored", longest)
|
||||
}
|
||||
|
||||
func TestPreRegistrationNotRegistered(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user