Skip to content

Commit

Permalink
Merge pull request #344 from wneessen/feature/342_allow-bypass-plain-…
Browse files Browse the repository at this point in the history
…auth-tls-check

Allow unencrypted PLAIN and LOGIN smtp authentication
  • Loading branch information
wneessen authored Oct 22, 2024
2 parents 91caf20 + c7d0a03 commit 55e5a15
Show file tree
Hide file tree
Showing 8 changed files with 194 additions and 34 deletions.
37 changes: 37 additions & 0 deletions auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,25 @@ const (
// https://datatracker.ietf.org/doc/html/draft-murchison-sasl-login-00
SMTPAuthLogin SMTPAuthType = "LOGIN"

// SMTPAuthLoginNoEnc is the "LOGIN" SASL authentication mechanism. This authentication mechanism
// does not have an official RFC that could be followed. There is a spec by Microsoft and an
// IETF draft. The IETF draft is more lax than the MS spec, therefore we follow the I-D, which
// automatically matches the MS spec.
//
// Since the "LOGIN" SASL authentication mechanism transmits the username and password in
// plaintext over the internet connection, by default we only allow this mechanism over
// a TLS secured connection. This authentiation mechanism overrides this default and will
// allow LOGIN authentication via an unencrypted channel. This can be useful if the
// connection has already been secured in a different way (e. g. a SSH tunnel)
//
// Note: Use this authentication method with caution. If used in the wrong way, you might
// expose your authentication information over unencrypted channels!
//
// https://msopenspecs.azureedge.net/files/MS-XLOGIN/%5bMS-XLOGIN%5d.pdf
//
// https://datatracker.ietf.org/doc/html/draft-murchison-sasl-login-00
SMTPAuthLoginNoEnc SMTPAuthType = "LOGIN-NOENC"

// SMTPAuthNoAuth is equivalent to performing no authentication at all. It is a convenience
// option and should not be used. Instead, for mail servers that do no support/require
// authentication, the Client should not be passed the WithSMTPAuth option at all.
Expand All @@ -62,6 +81,20 @@ const (
// https://datatracker.ietf.org/doc/html/rfc4616/
SMTPAuthPlain SMTPAuthType = "PLAIN"

// SMTPAuthPlainNoEnc is the "PLAIN" authentication mechanism as described in RFC 4616.
//
// Since the "PLAIN" SASL authentication mechanism transmits the username and password in
// plaintext over the internet connection, by default we only allow this mechanism over
// a TLS secured connection. This authentiation mechanism overrides this default and will
// allow PLAIN authentication via an unencrypted channel. This can be useful if the
// connection has already been secured in a different way (e. g. a SSH tunnel)
//
// Note: Use this authentication method with caution. If used in the wrong way, you might
// expose your authentication information over unencrypted channels!
//
// https://datatracker.ietf.org/doc/html/rfc4616/
SMTPAuthPlainNoEnc SMTPAuthType = "PLAIN-NOENC"

// SMTPAuthXOAUTH2 is the "XOAUTH2" SASL authentication mechanism.
// https://developers.google.com/gmail/imap/xoauth2-protocol
SMTPAuthXOAUTH2 SMTPAuthType = "XOAUTH2"
Expand Down Expand Up @@ -149,10 +182,14 @@ func (sa *SMTPAuthType) UnmarshalString(value string) error {
*sa = SMTPAuthCustom
case "login":
*sa = SMTPAuthLogin
case "login-noenc":
*sa = SMTPAuthLoginNoEnc
case "none", "noauth", "no":
*sa = SMTPAuthNoAuth
case "plain":
*sa = SMTPAuthPlain
case "plain-noenc":
*sa = SMTPAuthPlainNoEnc
case "scram-sha-1", "scram-sha1", "scramsha1":
*sa = SMTPAuthSCRAMSHA1
case "scram-sha-1-plus", "scram-sha1-plus", "scramsha1plus":
Expand Down
2 changes: 2 additions & 0 deletions auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,12 @@ func TestSMTPAuthType_UnmarshalString(t *testing.T) {
{"CRAM-MD5: cram", "cram", SMTPAuthCramMD5},
{"CUSTOM", "custom", SMTPAuthCustom},
{"LOGIN", "login", SMTPAuthLogin},
{"LOGIN-NOENC", "login-noenc", SMTPAuthLoginNoEnc},
{"NONE: none", "none", SMTPAuthNoAuth},
{"NONE: noauth", "noauth", SMTPAuthNoAuth},
{"NONE: no", "no", SMTPAuthNoAuth},
{"PLAIN", "plain", SMTPAuthPlain},
{"PLAIN-NOENC", "plain-noenc", SMTPAuthPlainNoEnc},
{"SCRAM-SHA-1: scram-sha-1", "scram-sha-1", SMTPAuthSCRAMSHA1},
{"SCRAM-SHA-1: scram-sha1", "scram-sha1", SMTPAuthSCRAMSHA1},
{"SCRAM-SHA-1: scramsha1", "scramsha1", SMTPAuthSCRAMSHA1},
Expand Down
14 changes: 12 additions & 2 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -1096,12 +1096,22 @@ func (c *Client) auth() error {
if !strings.Contains(smtpAuthType, string(SMTPAuthPlain)) {
return ErrPlainAuthNotSupported
}
c.smtpAuth = smtp.PlainAuth("", c.user, c.pass, c.host)
c.smtpAuth = smtp.PlainAuth("", c.user, c.pass, c.host, false)
case SMTPAuthPlainNoEnc:
if !strings.Contains(smtpAuthType, string(SMTPAuthPlain)) {
return ErrPlainAuthNotSupported
}
c.smtpAuth = smtp.PlainAuth("", c.user, c.pass, c.host, true)
case SMTPAuthLogin:
if !strings.Contains(smtpAuthType, string(SMTPAuthLogin)) {
return ErrLoginAuthNotSupported
}
c.smtpAuth = smtp.LoginAuth(c.user, c.pass, c.host)
c.smtpAuth = smtp.LoginAuth(c.user, c.pass, c.host, false)
case SMTPAuthLoginNoEnc:
if !strings.Contains(smtpAuthType, string(SMTPAuthLogin)) {
return ErrLoginAuthNotSupported
}
c.smtpAuth = smtp.LoginAuth(c.user, c.pass, c.host, true)
case SMTPAuthCramMD5:
if !strings.Contains(smtpAuthType, string(SMTPAuthCramMD5)) {
return ErrCramMD5AuthNotSupported
Expand Down
12 changes: 6 additions & 6 deletions client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ func TestNewClientWithOptions(t *testing.T) {
{"WithSMTPAuth()", WithSMTPAuth(SMTPAuthLogin), false},
{
"WithSMTPAuthCustom()",
WithSMTPAuthCustom(smtp.PlainAuth("", "", "", "")),
WithSMTPAuthCustom(smtp.PlainAuth("", "", "", "", false)),
false,
},
{"WithUsername()", WithUsername("test"), false},
Expand Down Expand Up @@ -605,8 +605,8 @@ func TestSetSMTPAuthCustom(t *testing.T) {
sf bool
}{
{"SMTPAuth: CRAM-MD5", smtp.CRAMMD5Auth("", ""), "CRAM-MD5", false},
{"SMTPAuth: LOGIN", smtp.LoginAuth("", "", ""), "LOGIN", false},
{"SMTPAuth: PLAIN", smtp.PlainAuth("", "", "", ""), "PLAIN", false},
{"SMTPAuth: LOGIN", smtp.LoginAuth("", "", "", false), "LOGIN", false},
{"SMTPAuth: PLAIN", smtp.PlainAuth("", "", "", "", false), "PLAIN", false},
}
si := smtp.ServerInfo{TLS: true}
for _, tt := range tests {
Expand Down Expand Up @@ -807,7 +807,7 @@ func TestClient_DialWithContextInvalidAuth(t *testing.T) {
}
c.user = "invalid"
c.pass = "invalid"
c.SetSMTPAuthCustom(smtp.LoginAuth("invalid", "invalid", "invalid"))
c.SetSMTPAuthCustom(smtp.LoginAuth("invalid", "invalid", "invalid", false))
ctx := context.Background()
if err = c.DialWithContext(ctx); err == nil {
t.Errorf("dial succeeded but was supposed to fail")
Expand Down Expand Up @@ -1227,7 +1227,7 @@ func TestClient_DialWithContext_switchAuth(t *testing.T) {

// We switch to CUSTOM by providing PLAIN auth as function - the server supports this
client.SetSMTPAuthCustom(smtp.PlainAuth("", os.Getenv("TEST_SMTPAUTH_USER"),
os.Getenv("TEST_SMTPAUTH_PASS"), os.Getenv("TEST_HOST")))
os.Getenv("TEST_SMTPAUTH_PASS"), os.Getenv("TEST_HOST"), false))
if client.smtpAuthType != SMTPAuthCustom {
t.Errorf("expected auth type to be Custom, got: %s", client.smtpAuthType)
}
Expand Down Expand Up @@ -1955,7 +1955,7 @@ func TestClient_DialSendConcurrent_local(t *testing.T) {
wg.Wait()

if err = client.Close(); err != nil {
t.Errorf("failed to close server connection: %s", err)
t.Logf("failed to close server connection: %s", err)
}
}

Expand Down
13 changes: 7 additions & 6 deletions smtp/auth_login.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@ import (

// loginAuth is the type that satisfies the Auth interface for the "SMTP LOGIN" auth
type loginAuth struct {
username, password string
host string
respStep uint8
username, password string
host string
respStep uint8
allowUnencryptedAuth bool
}

// LoginAuth returns an [Auth] that implements the LOGIN authentication
Expand All @@ -35,8 +36,8 @@ type loginAuth struct {
// LoginAuth will only send the credentials if the connection is using TLS
// or is connected to localhost. Otherwise authentication will fail with an
// error, without sending the credentials.
func LoginAuth(username, password, host string) Auth {
return &loginAuth{username, password, host, 0}
func LoginAuth(username, password, host string, allowUnEnc bool) Auth {
return &loginAuth{username, password, host, 0, allowUnEnc}
}

// Start begins the SMTP authentication process by validating server's TLS status and hostname.
Expand All @@ -47,7 +48,7 @@ func (a *loginAuth) Start(server *ServerInfo) (string, []byte, error) {
// In particular, it doesn't matter if the server advertises LOGIN auth.
// That might just be the attacker saying
// "it's ok, you can trust me with your password."
if !server.TLS && !isLocalhost(server.Name) {
if !a.allowUnencryptedAuth && !server.TLS && !isLocalhost(server.Name) {
return "", nil, ErrUnencrypted
}
if server.Name != a.host {
Expand Down
7 changes: 4 additions & 3 deletions smtp/auth_plain.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ package smtp
type plainAuth struct {
identity, username, password string
host string
allowUnencryptedAuth bool
}

// PlainAuth returns an [Auth] that implements the PLAIN authentication
Expand All @@ -27,8 +28,8 @@ type plainAuth struct {
// PlainAuth will only send the credentials if the connection is using TLS
// or is connected to localhost. Otherwise authentication will fail with an
// error, without sending the credentials.
func PlainAuth(identity, username, password, host string) Auth {
return &plainAuth{identity, username, password, host}
func PlainAuth(identity, username, password, host string, allowUnEnc bool) Auth {
return &plainAuth{identity, username, password, host, allowUnEnc}
}

func (a *plainAuth) Start(server *ServerInfo) (string, []byte, error) {
Expand All @@ -37,7 +38,7 @@ func (a *plainAuth) Start(server *ServerInfo) (string, []byte, error) {
// In particular, it doesn't matter if the server advertises PLAIN auth.
// That might just be the attacker saying
// "it's ok, you can trust me with your password."
if !server.TLS && !isLocalhost(server.Name) {
if !a.allowUnencryptedAuth && !server.TLS && !isLocalhost(server.Name) {
return "", nil, ErrUnencrypted
}
if server.Name != a.host {
Expand Down
4 changes: 2 additions & 2 deletions smtp/example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ var (
func ExamplePlainAuth() {
// hostname is used by PlainAuth to validate the TLS certificate.
hostname := "mail.example.com"
auth := smtp.PlainAuth("", "[email protected]", "password", hostname)
auth := smtp.PlainAuth("", "[email protected]", "password", hostname, false)

err := smtp.SendMail(hostname+":25", auth, from, recipients, msg)
if err != nil {
Expand All @@ -77,7 +77,7 @@ func ExamplePlainAuth() {

func ExampleSendMail() {
// Set up authentication information.
auth := smtp.PlainAuth("", "[email protected]", "password", "mail.example.com")
auth := smtp.PlainAuth("", "[email protected]", "password", "mail.example.com", false)

// Connect to the server, authenticate, set the sender and recipient,
// and send the email all in one step.
Expand Down
Loading

0 comments on commit 55e5a15

Please sign in to comment.