📧

認証メールを送るときには、587(starttls)じゃなくて、465(smtps)を使おう

2024/11/26に公開

はじめに

諸君らは認証系を作るときには、十中八九認証メールを送ってメールアドレスの所有者であることを確認する機能をつけることがあるだろう。

よくネット上で見るサンプルコードでは587ポートで接続して、TLS設定をしたりしなかったりするが、
中には平文で確認コードを送ってるものもあったりしたので、初めっからTLSを強制するようにしたらいいのにとおもい、この記事を書いた。

そもそも何が違うんよ

465を使った方式のほうが、プロトコルとしては古い。
587を使った方式では、通常は平文で通信をするが、STARTTLSというメソッドを渡すことによって暗号化通信に切り替えられるようにしてある。
つまり、どちらもTLS通信ができるが、STARTTLSの方が柔軟である。

なんで一般的にはSTARTTLSを推奨してるの?

メールクライアントによっては、TLS接続が不可能なクライアントも存在する。
その後方互換性を埋めるために、暗号化を強制する465ではなく、587を使うことを推奨している。
だが、認証メールを送る等のユースケースの場合、クライアントが厳選されていることから、かえってこのTLS接続の有無は邪魔になるのだ。

平文と暗号を切り替えられる機能自体のリスク

以下のサイトにて該当する脆弱性がまとめられている。是非読んでみてほしい。
https://nostarttls.secvuln.info/

上記では、さまざまな脆弱性について述べられているが、アプリケーションの設計上の問題も起きやすい。
STARTTLSでは平文での通信も許容する分、STARTTLSメソッドを渡さなければ平文で通信されてしまうため、意図せず確認メールが平文で通信されてしまうということが起こりかねない。
パスワードの変更など、ほとんどのWebアプリケーションの認証機能において、メールに依存しているので、致命的な脆弱性に発展する可能性は十分にあるのである。

Goで書いてみた465を使うときのサンプルコード

sender.go
package mail

import (
	"crypto/tls"
	"fmt"
	"net/smtp"

	"github.com/jordan-wright/email"
)

const (
	// 認証サーバー 基本的にServerAddressと同じホスト名にしておけばいい。
	smtpAuthAddress   = "mail.example.com"
	smtpServerAddress = "mail.example.com:465"
)

type EmailSender interface {
	SendEmail(
		subject string,
		content string,
		to []string,
		cc []string,
		bcc []string,
		attachFiles []string,
	) error
}

type Email struct {
	name              string
	fromEmailAddress  string
	fromEmailPassword string
}

func NewEmailSender(name string, fromEmailAddress string, fromEmailPassword string) EmailSender {
	return &Email{
		name:              name,
		fromEmailAddress:  fromEmailAddress,
		fromEmailPassword: fromEmailPassword,
	}
}

func (sender *Email) SendEmail(
	subject string,
	content string,
	to []string,
	cc []string,
	bcc []string,
	attachFiles []string,
) error {
	e := email.NewEmail()
	e.From = fmt.Sprintf("%s <%s>", sender.name, sender.fromEmailAddress)
	e.Subject = subject
	e.HTML = []byte(content)
	e.To = to
	e.Cc = cc
	e.Bcc = bcc

	for _, f := range attachFiles {
		_, err := e.AttachFile(f)
		if err != nil {
			return fmt.Errorf("添付ファイル取得に失敗 %s: %w", f, err)
		}
	}

	// TLS設定
	tlsConfig := &tls.Config{
		ServerName: smtpAuthAddress,
	}
	smtpAuth := smtp.PlainAuth("", sender.fromEmailAddress, sender.fromEmailPassword, smtpAuthAddress)

	//メール送信
	if err := e.SendWithTLS(smtpServerAddress, smtpAuth, tlsConfig); err != nil {
		return fmt.Errorf("メール送信に失敗: %w", err)
	}

	return nil
}

GoのSMTPライブラリの補遺

暗号化を強制するので、e.Sendは使えない。
余談だが、goの標準パッケージのsmtpは暗号化されない場合平文認証を行わないようになっている。
素晴らしい。

Discussion