Refuse passwords over 72 bytes and confirm only stored ones (closes #144)
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:
2026-10-21 05:02:00 +02:00
parent 0e6d06a7fc
commit c267cd215c
6 changed files with 280 additions and 46 deletions
+34 -8
View File
@@ -27,6 +27,7 @@ const (
pingInterval = 90 * time.Second
pongDeadline = 30 * time.Second
minPasswordLen = 8
maxPasswordLen = 72 // bcrypt cannot hash a longer one
maxHashcashBits = 40
)
@@ -62,6 +63,7 @@ type Conn struct {
registered bool
gotNick bool
gotUser bool
gotPass bool
passWord string
lastQueueID int64
@@ -436,6 +438,7 @@ func (c *Conn) handlePreRegistration(
}
c.passWord = msg.Params[0]
c.gotPass = true
case irc.CmdNick:
if len(msg.Params) < 1 {
c.sendNumeric(
@@ -550,17 +553,20 @@ func (c *Conn) completeRegistration(ctx context.Context) {
// So that KILL, from either transport, can close it.
c.svc.RegisterWireConn(sessionID, c)
// If PASS was provided before registration, set the
// session password.
if c.passWord != "" && len(c.passWord) >= minPasswordLen {
c.setPassword(ctx, c.passWord)
}
// Send welcome burst.
c.deliverWelcome()
c.deliverLusers(ctx)
c.deliverMOTD()
// If PASS was given before registration, store its
// password now, after the welcome burst, so that a
// NOTICE refusing it reaches a registered client. A
// failure to store it is only logged, as it is for PASS
// after registration.
if c.gotPass && c.checkPasswordLength(c.passWord) {
_ = c.setPassword(ctx, c.passWord)
}
// Start the message relay goroutine.
go c.relayMessages(ctx)
}
@@ -643,13 +649,33 @@ func (c *Conn) deliverMOTD() {
)
}
// setPassword sets a bcrypt password on the session.
func (c *Conn) setPassword(ctx context.Context, pw string) {
// checkPasswordLength reports whether pw is 8 to 72 bytes
// long, and sends the client a NOTICE saying so when it is
// not.
func (c *Conn) checkPasswordLength(pw string) bool {
if len(pw) >= minPasswordLen && len(pw) <= maxPasswordLen {
return true
}
c.sendFromServer("NOTICE", c.nick,
"Password must be 8 to 72 bytes",
)
return false
}
// setPassword sets a bcrypt password on the session, and
// logs and returns the error when it was not stored.
func (c *Conn) setPassword(ctx context.Context, pw string) error {
// Use the database's auth module to hash and store.
err := c.database.SetPassword(ctx, c.sessionID, pw)
if err != nil {
c.log.Error(
"failed to set password", "error", err,
)
return err
}
return nil
}