Pythonでメール送信機能を実装する場合、標準ライブラリであるsmtplibを利用することが最も一般的な方法のようです。以下のような関数をつくって、それを呼び出せばメールが送信できます。
import smtplib
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
def sendEmail(mail_to, mail_from, subject, content):
msg = MIMEMultipart('alternative')
msg['To'] = mail_to
msg['From'] = mail_from
msg['Subject'] = subject
msg.attach(MIMEText(content))
smtp_server = smtplib.SMTP('smtp.gmail.com', port=587)
smtp_server.starttls()
smtp_server.login('xxxxxxxx@gmail.com', 'xxxxxxxx')
smtp_server.sendmail(mail_from, mail_to, msg.as_string())
簡単ですが、これで問題はないのでしょうか。メールヘッダーインジェクションの脆弱性に配慮する必要はないのでしょうか?
ということで、調べてみました。まずは、正常なメール送信の動作確認のために、以下の実装のスクリプトをつくって(sendmail.py
というファイル名にしました)、メールを送信してみましょう。
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import smtplib
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
def sendEmail(mail_to, mail_from, subject, content):
msg = MIMEMultipart('alternative')
msg['To'] = mail_to
msg['From'] = mail_from
msg['Subject'] = subject
msg.attach(MIMEText(content))
smtp_server = smtplib.SMTP('localhost', port=2525)
smtp_server.sendmail(mail_from, mail_to, msg.as_string())
print("Sending an Email.")
try:
sendEmail('to@example.com', 'from@example.com', 'Test', 'Mail Header Injection Test')
print("Complete.")
except Exception as e:
print("ERROR:" + str(e))
と、その前に、メール送信のために、SMTPサーバーが必要です。今回は単なる検証目的なので、FakeSMTPというテスト用途のSMTPサーバーを使用します。Javaで実装されているので、WindowsでもLinuxでも同様に簡単に起動します。ここからダウンロードして、以下のコマンドで実行します。
$ java -jar fakeSMTP-2.0.jar -s -p 2525
この場合、2525番のポートでSMTPサーバーを起動します。
起動したら、先程作成したsendmail.py
を実行してメールを送信します。
問題がなければ、「Sending an Email.
」、「Complete.
」と表示されて、以下のようにFakeSMTPサーバーにメールが送信されているはずです。
メールをクリックすると、内容が確認できます。見てのとおり、特に問題はありません。
Wed, 09 May 2018 14:51:50 +0900 (JST)
Content-Type: multipart/alternative;
boundary="===============7314347251575062497=="
MIME-Version: 1.0
To: to@example.com
From: from@example.com
Subject: Test
--===============7314347251575062497==
Content-Type: text/plain; charset="us-ascii"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
Mail Header Injection Test
--===============7314347251575062497==--
では、メールヘッダーインジェクションの脆弱性を攻撃できるか検証してみましょう。
sendmail.py
の次の行を、
sendEmail('to@example.com', 'from@example.com', 'Test', 'Mail Header Injection Test')
次のように変更します。
sendEmail('to@example.com', 'from@example.com', 'Test\nbcc: all@example.com', 'Mail Header Injection Test')
件名「Test
」の後に、「\nbcc: all@example.com
」を追加することで、改行を入れてメールヘッダーとしてbccを追加できるか確認してみます。これができれば、メールヘッダーインジェクションの脆弱性を攻撃される可能性があることになります(メールヘッダーインジェクションがどういうものかについては、いろいろなサイトで解説されているので、このページでは言及しません)。ちなみに、all@example.com
はメールが受信可能な適当なメールアドレスと考えて下さい。
では、再度sendmail.py
を実行してメールを送信します。
新しいバージョンのPythonを使用している場合は、以下のように表示されるはずです(このブログではPython 3.6を使用しています)。
$ python sendmail.py
Sending an Email.
ERROR:header value appears to contain an embedded header: 'Test\nbcc: all@example.com'
メールヘッダー値に別のメールヘッダーを埋め込もうとしたことを検知した旨のエラーメッセージが出力されます。つまり、Python 3.6標準のsmtplibに対しては、メールヘッダインジェクションは攻撃できないということになります。
この検知機能は、いつから実装されているのでしょうか?
GitHubのPythonのリポジトリで、先程のエラーメッセージ「header value appears to contain an embedded header
」を検索してみると、このコミットが見つかりました。どうやら、Python 3.1.4から実装されているようです。なので、3.1.3以前のバージョンを使っていなければ、問題は無さそうです(Python 2.x系も最新の2.7には実装(バックポート)されていました)。
ただし、調査していたときに気になるバグレポートを見つけました。
Issue#32606: Email Header Injection Protection Bypass
これによると、「bcc」と「:」の間に空白があるような値を使うことで、メールヘッダーインジェクションの脆弱性を攻撃できるとのことです。つまり、以下のうち前者は検知機能でエラーになるが、後者はスルーしてしまう、ということです。
・’Test\nbcc: all@example.com’ → エラー
・’Test\nbcc :all@example.com’ → スルー
では、さっそく検証してみましょう。sendmail.py
の'Test\nbcc: all@example.com'
の箇所を'Test\nbcc :all@example.com'
に修正して、実行します。
$ python2 sendmail.py
Sending an Email.
Complete.
送信できたようです。FakeSMTPが受信したメールをクリックすると、以下のように「bcc :all@example.com
」はチェックされること無く、メールヘッダーに付加されてしまいました。
Wed, 09 May 2018 16:35:27 +0900 (JST)
Content-Type: multipart/alternative;
boundary="===============6879493970155011648=="
MIME-Version: 1.0
To: to@example.com
From: from@example.com
Subject: Test
bcc :all@example.com
--===============6879493970155011648==
Content-Type: text/plain; charset="us-ascii"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
Mail Header Injection Test
--===============6879493970155011648==--
ただし、先程のバグレポートのコメントにもあるように、RFC 5322ではメールヘッダーのキーの直後には「:」を付けるのが正しい仕様のようです。したがって、「bcc :all@example.com
」付きのメールが送信されるかどうかは、SMTPサーバーがRFC 5322を正しく実装しているか次第ということになります。
ということで、SMTPサーバーが「bcc :all@example.com」を「bcc: all@example.com」と変わらず解釈してメールが送信できるかどうかを確認してみましょう。
例えば、GoogleのSMTPサーバーの場合はどうでしょうか?次のようにsendmail.py
を修正し、実行します。
smtp_server = smtplib.SMTP('smtp.gmail.com', port=587)
smtp_server.starttls()
smtp_server.login('xxxxxxxx@gmail.com', 'xxxxxxxx')
bcc宛にはメールが送信されませんでした。Googleはメールヘッダーの「bcc :all@example.com」を無視したことになります。
次に、MSNのSMTPサーバーを検証してみましょう。次のようにsendmail.py
を修正し、実行します。
smtp_server = smtplib.SMTP('smtp-mail.outlook.com', port=587)
smtp_server.starttls()
smtp_server.login('xxxxxxxx@hotmail.com', 'xxxxxxxx')
bcc宛にメールが送信されました。MSNはメールヘッダーの「bcc :all@example.com」をもとにall@example.comにメールを送信したことになります。
このように、RFCを正確に実装していないSMTPサーバーを利用しているPythonプログラムには、メールヘッダーインジェクションの脆弱性が存在するということが言えます。
ところで、何でこんなことを調べたかというと、以前公開したバグだらけのWebアプリケーションのDjango2ベースのクローンをつくっているからです。まだ開発途中ですが、メールヘッダーインジェクションも実装したので、興味がある方はぜひ、チェックしてみて下さい。
EasyBuggy clone built on Django