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, an empty one included, is checked after the welcome burst and gets the same NOTICE. Model: opus-5-5
This commit was merged in pull request #173.
This commit is contained in:
@@ -2906,32 +2906,73 @@ func TestPassCommand(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestPassCommandShortPassword(t *testing.T) {
|
||||
tserver := newTestServer(t)
|
||||
token := tserver.createSession("shortpw")
|
||||
// The shortest and longest password PASS accepts; bcrypt
|
||||
// cannot hash more than 72 bytes.
|
||||
const (
|
||||
minPasswordBytes = 8
|
||||
maxPasswordBytes = 72
|
||||
)
|
||||
|
||||
// Drain initial messages.
|
||||
_, lastID := tserver.pollMessages(token, 0)
|
||||
|
||||
// Try short password — should fail.
|
||||
status, _ := tserver.sendCommand(
|
||||
token,
|
||||
map[string]any{
|
||||
commandKey: irc.CmdPass,
|
||||
bodyKey: []string{"short"},
|
||||
},
|
||||
)
|
||||
if status != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d", status)
|
||||
// TestPassCommandLength checks that PASS stores a password of
|
||||
// 8 to 72 bytes and refuses any other with 461. The limit
|
||||
// counts bytes: "é" is two bytes in UTF-8, so 72 of them are
|
||||
// refused.
|
||||
func TestPassCommandLength(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
password string
|
||||
stored bool
|
||||
}{
|
||||
{"empty", "", false},
|
||||
{"7 bytes", strings.Repeat("a", minPasswordBytes-1), false},
|
||||
{"8 bytes", strings.Repeat("a", minPasswordBytes), true},
|
||||
{"72 bytes", strings.Repeat("a", maxPasswordBytes), true},
|
||||
{"73 bytes", strings.Repeat("a", maxPasswordBytes+1), false},
|
||||
{"72 characters", strings.Repeat("é", maxPasswordBytes), false},
|
||||
}
|
||||
|
||||
msgs, _ := tserver.pollMessages(token, lastID)
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
tserver := newTestServer(t)
|
||||
token := tserver.createSession("pwlength")
|
||||
|
||||
if !findNumeric(msgs, "461") {
|
||||
t.Fatalf(
|
||||
"expected ERR_NEEDMOREPARAMS (461), got %v",
|
||||
msgs,
|
||||
)
|
||||
_, lastID := tserver.pollMessages(token, 0)
|
||||
|
||||
status, result := tserver.sendCommand(
|
||||
token,
|
||||
map[string]any{
|
||||
commandKey: irc.CmdPass,
|
||||
bodyKey: []string{tc.password},
|
||||
},
|
||||
)
|
||||
if status != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d: %v", status, result)
|
||||
}
|
||||
|
||||
if !tc.stored {
|
||||
msgs, _ := tserver.pollMessages(token, lastID)
|
||||
if !findNumeric(msgs, "461") {
|
||||
t.Fatalf(
|
||||
"expected ERR_NEEDMOREPARAMS (461), got %v",
|
||||
msgs,
|
||||
)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if result[statusKey] != "ok" {
|
||||
t.Fatalf("expected ok, got %v", result)
|
||||
}
|
||||
|
||||
postJSONExpectStatus(
|
||||
t, tserver, "/api/v1/login",
|
||||
map[string]string{
|
||||
nickKey: "pwlength", passwordKey: tc.password,
|
||||
},
|
||||
http.StatusOK,
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user