CVE-2023-27582 (Go, published 2023-03-13T22:15Z)
Description: value: maddy is a composable, all-in-one mail server. Starting with version 0.2.0 and prior to version 0.6.3, maddy allows a full authentication bypass if SASL authorization username is specified when using the PLAIN authentication mechanisms. Instead of validating the specified username, it is accepted as is after checking the credentials for the authentication username. maddy 0.6.3 includes the fix for the bug. There are no known workarounds.
Severity: nan (CVSS v3 base score: 9.8)
Weakness: CWE-287 \u2014 Improper Authentication
When an actor claims to have a given identity, the product does not prove or insufficiently proves that the claim is correct.
Repository: https://github.com/foxcpp/maddy
Files changed:
docs/man/maddy-auth.5.scd
internal/auth/external/externalauth.go
internal/auth/pam/module.go
internal/auth/plain_separate/plain_separate.go
internal/auth/plain_separate/plain_separate_test.go
internal/auth/sasl.go
internal/auth/sasl_test.go
internal/auth/shadow/module.go
internal/config/module/interfaces.go
internal/endpoint/imap/imap.go
internal/endpoint/smtp/smtp.go
internal/module/auth.go
internal/module/dummy.go
internal/storage/sql/sql.go
Commit message: Revert authorization/authentication split

Authentication provider module is responsible only for authentication.
Nothing more. Access control (authorization) should be kept separate.


Vulnerable code:
The authorization identity is required to be a valid RFC 5321 e-mail address.
It is returned as the authorization identity.
The authorization identtity is the same as authorization identity.

The authorization identtity is the same as authorization identity.

	expect_address yes
TL;DR (assuming you have maddy group):
The authorization identtity is the same as authorization identity.

   First match wins same as in step 2.

The set of authorization identities estabilished by plain_separate is the list of
strings found in 'user' tables. If there are no 'user' tables (only 'pass'
directives are specified), the list of authorization identities returned by
auth. providers is used.
*Note*: If a username table returns empty string, the authorization identity is
assumed to be the same as authentication identity.
func (ea *ExternalAuth) AuthPlain(username, password string) ([]string, error) {
		return nil, module.ErrUnknownCredentials
	// TODO: Extend process protocol to support multiple authorization identities.
	return []string{username}, AuthUsingHelper(ea.helperPath, accountName, password)
func (a *Auth) AuthPlain(username, password string) ([]string, error) {
			return nil, err
		return nil, err
	return []string{username}, nil
	"golang.org/x/text/secure/precis"
func (a *Auth) AuthPlain(username, password string) ([]string, error) {
	key, err := precis.UsernameCaseMapped.CompareKey(username)
	if err != nil {
		return nil, err
	}

	identities := make([]string, 0, 1)
	if len(a.userTbls) != 0 {
		for _, tbl := range a.userTbls {
			repl, ok, err := tbl.Lookup(key)
			if err != nil {
				return nil, err
			}
			if !ok {
				continue
			}
			if repl != "" {
				identities = append(identities, repl)
			} else {
				identities = append(identities, key)
			}
			if a.onlyFirstID && len(identities) != 0 {
				break
			}
		if len(identities) == 0 {
			return nil, errors.New("plain_separate: unknown credentials")
	var (
		lastErr error
		ok      bool
	)
	for _, pass := range a.passwd {
		passIDs, err := pass.AuthPlain(username, password)
		if err != nil {
		if len(a.userTbls) == 0 {
			identities = append(identities, passIDs...)
		}
		ok = true
	}
	if !ok {
		return nil, lastErr
	}
	return identities, nil
	"reflect"
	db map[string][]string
func (m mockAuth) AuthPlain(username, _ string) ([]string, error) {
	ids, ok := m.db[username]
		return nil, errors.New("invalid creds")
	return ids, nil
				db: map[string][]string{
					"user1": []string{"user1a", "user1b"},
	ids, err := a.AuthPlain("user1", "aaa")
	if !reflect.DeepEqual(ids, []string{"user1a", "user1b"}) {
		t.Fatal("Wrong ids returned:", ids)
	}
				db: map[string][]string{
					"user2": []string{"user2a", "user2b"},
				db: map[string][]string{
					"user1": []string{"user1a", "user1b"},
	ids, err := a.AuthPlain("user1", "aaa")
	if !reflect.DeepEqual(ids, []string{"user1a", "user1b"}) {
		t.Fatal("Wrong ids returned:", ids)
	}
					"user1": "user2",
				db: map[string][]string{
					"user2": []string{"user2a", "user2b"},
				db: map[string][]string{
					"user1": []string{"user1a", "user1b"},
	ids, err := a.AuthPlain("user1", "aaa")
	if !reflect.DeepEqual(ids, []string{"user2"}) {
		t.Fatal("Wrong ids returned:", ids)
	}
					"userWH": "user1",
					"user1": "user2",
				db: map[string][]string{
					"user2": []string{"user2a", "user2b"},
				db: map[string][]string{
					"user1": []string{"user1a", "user1b"},
	ids, err := a.AuthPlain("user1", "aaa")
	if !reflect.DeepEqual(ids, []string{"user2"}) {
		t.Fatal("Wrong ids returned:", ids)
	}
	"golang.org/x/text/secure/precis"
func (s *SASLAuth) AuthPlain(username, password string) ([]string, error) {
		return nil, ErrUnsupportedMech
	accounts := make([]string, 0, 1)
		pAccs, err := p.AuthPlain(username, password)

		if s.OnlyFirstID {
			return pAccs, nil
		}
		accounts = append(accounts, pAccs...)
	if len(accounts) == 0 {
		return nil, fmt.Errorf("no auth. provider accepted creds, last err: %w", lastErr)
	}
	return accounts, nil
}

func filterIdentity(accounts []string, identity string) ([]string, error) {
	if identity == "" {
		return accounts, nil
	}

	matchFound := false
	for _, acc := range accounts {
		if precis.UsernameCaseMapped.Compare(acc, identity) {
			accounts = []string{identity}
			matchFound = true
			break
		}
	}
	if !matchFound {
		return nil, errors.New("auth: invalid credentials")
	}
	return accounts, nil
//
// successCb will be called with the slice of authorization identities
// associated with credentials used.
// If it fails - authentication will fail too.
func (s *SASLAuth) CreateSASL(mech string, remoteAddr net.Addr, successCb func([]string) error) sasl.Server {
			accounts, err := s.AuthPlain(username, password)
			if err != nil {
				s.Log.Error("authentication failed", err, "username", username, "identity", identity, "src_ip", remoteAddr)
				return errors.New("auth: invalid credentials")
			if len(accounts) == 0 {
				accounts = []string{username}
			}
			accounts, err = filterIdentity(accounts, identity)
				s.Log.Error("not authorized", err, "username", username, "identity", identity, "src_ip", remoteAddr)
			return successCb(accounts)
			accounts, err := s.AuthPlain(username, password)
			return successCb(accounts)
	mod, err := modconfig.SASLAuthDirective(m, node)
	if err != nil {
	saslAuth := mod.(module.SASLProvider)
	for _, mech := range saslAuth.SASLMechanisms() {
		switch mech {
		case sasl.Login, sasl.Plain:
			plainAuth, ok := saslAuth.(module.PlainAuth)
			if !ok {
				return m.MatchErr("auth: provider does not implement PlainAuth even though it reports PLAIN/LOGIN mechanism")
			}
			s.Plain = append(s.Plain, plainAuth)
		default:
			return m.MatchErr("auth: unknown SASL mechanism")
		}
	"reflect"
	"github.com/emersion/go-sasl"
	db map[string][]string
func (mockAuth) SASLMechanisms() []string {
	return []string{sasl.Plain, sasl.Login}
}

func (m mockAuth) AuthPlain(username, _ string) ([]string, error) {
	ids, ok := m.db[username]
		return nil, errors.New("invalid creds")
	return ids, nil
				db: map[string][]string{
					"user1": []string{"user1a", "user1b"},
		srv := a.CreateSASL("XWHATEVER", &net.TCPAddr{}, func([]string) error { return nil })
		var ids []string
		srv := a.CreateSASL("PLAIN", &net.TCPAddr{}, func(passed []string) error {
			ids = passed
		if !reflect.DeepEqual(ids, []string{"user1a", "user1b"}) {
			t.Error("Wrong auth. identities passed to callback:", ids)
		}
	t.Run("PLAIN with autorization identity", func(t *testing.T) {
		var ids []string
		srv := a.CreateSASL("PLAIN", &net.TCPAddr{}, func(passed []string) error {
			ids = passed
		if !reflect.DeepEqual(ids, []string{"user1a"}) {
			t.Error("Wrong auth. identities passed to callback:", ids)
		}
	})

	t.Run("PLAIN with wrong authorization identity", func(t *testing.T) {
		srv := a.CreateSASL("PLAIN", &net.TCPAddr{}, func(passed []string) error {
			return nil
		})

		_, _, err := srv.Next([]byte("user1c\x00user1\x00aa"))
		if err == nil {
			t.Error("Next should fail")
		}
	})

	t.Run("PLAIN with wrong authentication identity", func(t *testing.T) {
		srv := a.CreateSASL("PLAIN", &net.TCPAddr{}, func(passed []string) error {
			return nil
		})

		_, _, err := srv.Next([]byte("\x00user2\x00aa"))
		if err == nil {
			t.Error("Next should fail")
		}
func (a *Auth) AuthPlain(username, password string) ([]string, error) {
		return []string{username}, external.AuthUsingHelper(a.helperPath, username, password)
		return nil, err
		return nil, fmt.Errorf("shadow: account is expired")
		return nil, fmt.Errorf("shadow: password is expired")
			return nil, module.ErrUnknownCredentials
		return nil, err
	return []string{username}, nil
func SASLAuthDirective(m *config.Map, node *config.Node) (interface{}, error) {
	var provider module.SASLProvider
	if err := ModuleFromNode(node.Args, node, m.Globals, &provider); err != nil {
		return nil, err
	}
	return provider, nil
}

			return endp.saslAuth.CreateSASL(mech, c.Info().RemoteAddr, func(ids []string) error {
				return endp.openAccount(c, ids)
func (endp *Endpoint) openAccount(c imapserver.Conn, identities []string) error {
	u, err := endp.Store.GetOrCreateUser(identities[0])
	_, err := endp.saslAuth.AuthPlain(username, password)
	// TODO: Wrap GetOrCreateUser and possibly implement INBOXES extension
	// (though it is draft 00 for quite some time so it likely has no future).
			return endp.saslAuth.CreateSASL(mech, state.RemoteAddr, func(ids []string) error {
				c.SetSession(endp.newSession(false, ids[0], "", &state))
	_, err := endp.saslAuth.AuthPlain(username, password)
	// TODO: Pass valid identifies to SMTP pipeline.
	AuthPlain(username, password string) ([]string, error)
}

// SASLProvider is the interface implemented by modules and used by protocol
// endpoints that rely on SASL framework for user authentication.
//
// This actual interface is only used to indicate that the module is a
// SASL-compatible auth. provider. For each unique value returned by
// SASLMechanisms, the module object should also implement the coresponding
// mechanism-specific interface.
//
// *Rationale*: There is no single generic interface that would handle any SASL
// mechanism while permiting the use of a credentials set estabilished once with
// multiple auth. providers at once.
//
// Per-mechanism interfaces:
// - PLAIN => PlainAuth
type SASLProvider interface {
	SASLMechanisms() []string
	"github.com/emersion/go-sasl"
func (d *Dummy) SASLMechanisms() []string {
	return []string{sasl.Plain, sasl.Login}
}

func (d *Dummy) AuthPlain(username, _ string) ([]string, error) {
	return []string{username}, nil
func (store *Storage) AuthPlain(username, password string) ([]string, error) {
		return nil, err
		return nil, err
		return nil, module.ErrUnknownCredentials
	return []string{username}, nil

Fixed code:
The username is required to be a valid RFC 5321 e-mail address.
TL;DR (assuming you have the maddy group):
   If authentication with all providers fails - an error is returned.
func (ea *ExternalAuth) AuthPlain(username, password string) error {
		return module.ErrUnknownCredentials
	return AuthUsingHelper(ea.helperPath, accountName, password)
func (a *Auth) AuthPlain(username, password string) error {
			return err
		return err
	return nil
func (a *Auth) AuthPlain(username, password string) error {
	ok := len(a.userTbls) == 0
	for _, tbl := range a.userTbls {
		_, tblOk, err := tbl.Lookup(username)
		if err != nil {
			return err
		if tblOk {
			ok = true
			break
	if !ok {
		return errors.New("user not found in tables")
	}
	var lastErr error
	for _, p := range a.passwd {
		if err := p.AuthPlain(username, password); err != nil {
		return nil
	}
	return lastErr
	db map[string]bool
func (m mockAuth) AuthPlain(username, _ string) error {
	ok := m.db[username]
		return errors.New("invalid creds")
	return nil
				db: map[string]bool{
					"user1": true,
	err := a.AuthPlain("user1", "aaa")
				db: map[string]bool{
					"user2": true,
				db: map[string]bool{
					"user1": true,
	err := a.AuthPlain("user1", "aaa")
					"user1": "",
				db: map[string]bool{
					"user2": true,
				db: map[string]bool{
					"user1": true,
	err := a.AuthPlain("user1", "aaa")
					"userWH": "",
					"user1": "",
				db: map[string]bool{
					"user2": true,
				db: map[string]bool{
					"user1": true,
	err := a.AuthPlain("user1", "aaa")
func (s *SASLAuth) AuthPlain(username, password string) error {
		return ErrUnsupportedMech
		err := p.AuthPlain(username, password)
		if err == nil {
			return nil
		}
	return fmt.Errorf("no auth. provider accepted creds, last err: %w", lastErr)
func (s *SASLAuth) CreateSASL(mech string, remoteAddr net.Addr, successCb func(identity string) error) sasl.Server {
			if identity == "" {
				identity = username

			err := s.AuthPlain(username, password)
				s.Log.Error("authentication failed", err, "username", username, "src_ip", remoteAddr)
			return successCb(identity)
			err := s.AuthPlain(username, password)
			return successCb(username)
	var any interface{}
	if err := modconfig.ModuleFromNode(node.Args, node, m.Globals, &any); err != nil {

	hasAny := false
	if plainAuth, ok := any.(module.PlainAuth); ok {
		s.Plain = append(s.Plain, plainAuth)
		hasAny = true
	if !hasAny {
		return m.MatchErr("auth: specified module does not provide any SASL mechanism")
	}
	db map[string]bool
func (m mockAuth) AuthPlain(username, _ string) error {
	ok := m.db[username]
		return errors.New("invalid creds")
	return nil
				db: map[string]bool{
					"user1": true,
		srv := a.CreateSASL("XWHATEVER", &net.TCPAddr{}, func(string) error { return nil })
		srv := a.CreateSASL("PLAIN", &net.TCPAddr{}, func(id string) error {
			if id != "user1" {
				t.Fatal("Wrong auth. identities passed to callback:", id)
			}
	t.Run("PLAIN with authorization identity", func(t *testing.T) {
		srv := a.CreateSASL("PLAIN", &net.TCPAddr{}, func(id string) error {
			if id != "user1a" {
				t.Fatal("Wrong authorization identity passed:", id)
			}
func (a *Auth) AuthPlain(username, password string) error {
		return external.AuthUsingHelper(a.helperPath, username, password)
		return err
		return fmt.Errorf("shadow: account is expired")
		return fmt.Errorf("shadow: password is expired")
			return module.ErrUnknownCredentials
		return err
	return nil
			return endp.saslAuth.CreateSASL(mech, c.Info().RemoteAddr, func(identity string) error {
				return endp.openAccount(c, identity)
func (endp *Endpoint) openAccount(c imapserver.Conn, identity string) error {
	u, err := endp.Store.GetOrCreateUser(identity)
	err := endp.saslAuth.AuthPlain(username, password)
			return endp.saslAuth.CreateSASL(mech, state.RemoteAddr, func(id string) error {
				c.SetSession(endp.newSession(false, id, "", &state))
	err := endp.saslAuth.AuthPlain(username, password)
	AuthPlain(username, password string) error
func (d *Dummy) AuthPlain(username, _ string) error {
	return nil
func (store *Storage) AuthPlain(username, password string) error {
		return err
		return err
		return module.ErrUnknownCredentials
	return nil