使用 STARTTLS 从 Python 发送电子邮件
Sending email from Python using STARTTLS
我想通过 Python 的 smtplib 使用 Python 脚本发送电子邮件。
如果可以建立到服务器的加密连接,脚本应该只发送电子邮件。
要加密到端口 587 的连接,我想使用 STARTTLS。
使用一些示例,我编写了以下代码:
smtp_server = smtplib.SMTP(host, port=port)
context = ssl.create_default_context()
smtp_server.starttls(context)
smtp_server.login(user, password)
smtp_server.send_message(msg)
msg、主机、端口、用户、密码是我脚本中的变量。
我有两个问题:
- 连接是否始终加密或是否容易受到 STRIPTLS 攻击 (https://en.wikipedia.org/wiki/STARTTLS)。
- 我应该使用 SMTP 对象的 ehlo() 方法吗?在某些示例中,它在调用 starttls() 之前和之后被显式调用。另一方面,在 smptlib 的文档中写道,如有必要,sendmail() 将调用它。
[编辑]
@tintin 解释说,ssl.create_default_context()
可能会导致不安全的连接。因此,我通过以下方式使用一些示例更改了代码:
_DEFAULT_CIPHERS = (
'ECDH+AESGCM:DH+AESGCM:ECDH+AES256:DH+AES256:ECDH+AES128:DH+AES:ECDH+HIGH:'
'DH+HIGH:ECDH+3DES:DH+3DES:RSA+AESGCM:RSA+AES:RSA+HIGH:RSA+3DES:!aNULL:'
'!eNULL:!MD5')
smtp_server = smtplib.SMTP(host, port=port)
# only TLSv1 or higher
context = ssl.SSLContext(ssl.PROTOCOL_SSLv23)
context.options |= ssl.OP_NO_SSLv2
context.options |= ssl.OP_NO_SSLv3
context.set_ciphers(_DEFAULT_CIPHERS)
context.set_default_verify_paths()
context.verify_mode = ssl.CERT_REQUIRED
if smtp_server.starttls(context=context)[0] != 220:
return False # cancel if connection is not encrypted
smtp_server.login(user, password)
对于密码设置,我使用了 ssl.create_default_context()
最新版本的一些代码。这些设置合适吗?
注意:我原题的代码中有一个错误。这是相关行的正确版本:
smtp_server.starttls(context=context)
[\编辑]
Is the connection always encrypted or is it vulnerable to the STRIPTLS attack (https://en.wikipedia.org/wiki/STARTTLS).
长话短说:如果您不检查 .starttls()
的响应代码,则可以从 smtplib <=py3.5.1rc1 <=py2.7.10 中删除 starttls
在支持它的 smtp 服务器上显式调用 .starttls()
,恶意 MitM 会剥离您的 STARTTLS
命令并伪造非 220
响应 不协商 ssl,也不引发异常,因此不加密您的通信 - 因此它 容易受到 striptls 的攻击,除非您手动验证对 .starttls()[0]==220
或内部 .sock
封装了 SSL。
这是一个 python 2.7.9 smtplib 通信,其中包含一个与您的示例类似的示例,该示例未能通过让服务器或 MitM 回复 999 NOSTARTTLS
而不是 200
来协商 starttls。没有明确检查客户端脚本中的 200 响应代码,由于失败的 starttls 尝试没有异常,因此邮件传输未加密:
220 xx ESMTP
250-xx
250-SIZE 20480000
250-AUTH LOGIN
250-STARTTLS
250 HELP
STARTTLS
999 NOSTARTTLS
mail FROM:<a@b.com> size=686
250 OK
rcpt TO:<a@b.com>
250 OK
data
在不支持 STARTTLS 的 smtp 服务器上显式调用 .starttls()
- 或者 MitM 从服务器响应中剥离此功能 - 将引发 SMTPNotSupportedError
。请参阅下面的代码。
一般说明:加密还取决于配置的密码规范,即您的 SSLContext,在您的情况下,它是由 ssl.create_default_context()
创建的。请注意,将您的 SSLContext 配置为允许进行身份验证但不加密的密码规范是完全有效的(如果服务器和客户端都 offered/allowed)。例如。 TLS_RSA_WITH_NULL_SHA256
。
NULL-SHA256 TLSv1.2 Kx=RSA Au=RSA Enc=None Mac=SHA256
根据 this answer python pre 2.7.9/3.4.3 NOT 尝试对默认 ssl 上下文强制执行证书验证,因此容易受到 ssl 拦截。从 Python 2.7.9/3.4.3 开始,默认上下文强制执行证书验证。这也意味着,您必须为 pre 2.7.9/3.4.3 手动启用证书验证(通过创建自定义 sslcontext),否则任何不受信任的证书都可能被接受。
Should I use the ehlo() method of the SMTP object? In some examples it is called explicitly before and after calling starttls(). On the other side in the documentation of smptlib it is written, that sendmail() will call it, if it is necessary.
.sendmail()
、.send_message
和 .starttls()
将隐式调用 .ehlo_or_helo_if_needed()
,因此无需再次显式调用它。这也是
见下文 source::smtplib::starttls (cpython, inofficial github):
def starttls(self, keyfile=None, certfile=None, context=None):
"""Puts the connection to the SMTP server into TLS mode.
If there has been no previous EHLO or HELO command this session, this
method tries ESMTP EHLO first.
If the server supports TLS, this will encrypt the rest of the SMTP
session. If you provide the keyfile and certfile parameters,
the identity of the SMTP server and client can be checked. This,
however, depends on whether the socket module really checks the
certificates.
This method may raise the following exceptions:
SMTPHeloError The server didn't reply properly to
the helo greeting.
"""
self.ehlo_or_helo_if_needed()
if not self.has_extn("starttls"):
raise SMTPNotSupportedError(
"STARTTLS extension not supported by server.")
(resp, reply) = self.docmd("STARTTLS")
if resp == 220:
if not _have_ssl:
raise RuntimeError("No SSL support included in this Python")
if context is not None and keyfile is not None:
raise ValueError("context and keyfile arguments are mutually "
"exclusive")
if context is not None and certfile is not None:
raise ValueError("context and certfile arguments are mutually "
"exclusive")
if context is None:
context = ssl._create_stdlib_context(certfile=certfile,
keyfile=keyfile)
self.sock = context.wrap_socket(self.sock,
server_hostname=self._host)
self.file = None
# RFC 3207:
# The client MUST discard any knowledge obtained from
# the server, such as the list of SMTP service extensions,
# which was not obtained from the TLS negotiation itself.
self.helo_resp = None
self.ehlo_resp = None
self.esmtp_features = {}
self.does_esmtp = 0
return (resp, reply)
我想通过 Python 的 smtplib 使用 Python 脚本发送电子邮件。
如果可以建立到服务器的加密连接,脚本应该只发送电子邮件。 要加密到端口 587 的连接,我想使用 STARTTLS。
使用一些示例,我编写了以下代码:
smtp_server = smtplib.SMTP(host, port=port)
context = ssl.create_default_context()
smtp_server.starttls(context)
smtp_server.login(user, password)
smtp_server.send_message(msg)
msg、主机、端口、用户、密码是我脚本中的变量。 我有两个问题:
- 连接是否始终加密或是否容易受到 STRIPTLS 攻击 (https://en.wikipedia.org/wiki/STARTTLS)。
- 我应该使用 SMTP 对象的 ehlo() 方法吗?在某些示例中,它在调用 starttls() 之前和之后被显式调用。另一方面,在 smptlib 的文档中写道,如有必要,sendmail() 将调用它。
[编辑]
@tintin 解释说,ssl.create_default_context()
可能会导致不安全的连接。因此,我通过以下方式使用一些示例更改了代码:
_DEFAULT_CIPHERS = (
'ECDH+AESGCM:DH+AESGCM:ECDH+AES256:DH+AES256:ECDH+AES128:DH+AES:ECDH+HIGH:'
'DH+HIGH:ECDH+3DES:DH+3DES:RSA+AESGCM:RSA+AES:RSA+HIGH:RSA+3DES:!aNULL:'
'!eNULL:!MD5')
smtp_server = smtplib.SMTP(host, port=port)
# only TLSv1 or higher
context = ssl.SSLContext(ssl.PROTOCOL_SSLv23)
context.options |= ssl.OP_NO_SSLv2
context.options |= ssl.OP_NO_SSLv3
context.set_ciphers(_DEFAULT_CIPHERS)
context.set_default_verify_paths()
context.verify_mode = ssl.CERT_REQUIRED
if smtp_server.starttls(context=context)[0] != 220:
return False # cancel if connection is not encrypted
smtp_server.login(user, password)
对于密码设置,我使用了 ssl.create_default_context()
最新版本的一些代码。这些设置合适吗?
注意:我原题的代码中有一个错误。这是相关行的正确版本:
smtp_server.starttls(context=context)
[\编辑]
Is the connection always encrypted or is it vulnerable to the STRIPTLS attack (https://en.wikipedia.org/wiki/STARTTLS).
长话短说:如果您不检查 .starttls()
在支持它的 smtp 服务器上显式调用
.starttls()
,恶意 MitM 会剥离您的STARTTLS
命令并伪造非220
响应 不协商 ssl,也不引发异常,因此不加密您的通信 - 因此它 容易受到 striptls 的攻击,除非您手动验证对.starttls()[0]==220
或内部.sock
封装了 SSL。这是一个 python 2.7.9 smtplib 通信,其中包含一个与您的示例类似的示例,该示例未能通过让服务器或 MitM 回复
999 NOSTARTTLS
而不是200
来协商 starttls。没有明确检查客户端脚本中的 200 响应代码,由于失败的 starttls 尝试没有异常,因此邮件传输未加密:220 xx ESMTP 250-xx 250-SIZE 20480000 250-AUTH LOGIN 250-STARTTLS 250 HELP STARTTLS 999 NOSTARTTLS mail FROM:<a@b.com> size=686 250 OK rcpt TO:<a@b.com> 250 OK data
在不支持 STARTTLS 的 smtp 服务器上显式调用
.starttls()
- 或者 MitM 从服务器响应中剥离此功能 - 将引发SMTPNotSupportedError
。请参阅下面的代码。一般说明:加密还取决于配置的密码规范,即您的 SSLContext,在您的情况下,它是由
ssl.create_default_context()
创建的。请注意,将您的 SSLContext 配置为允许进行身份验证但不加密的密码规范是完全有效的(如果服务器和客户端都 offered/allowed)。例如。TLS_RSA_WITH_NULL_SHA256
。NULL-SHA256 TLSv1.2 Kx=RSA Au=RSA Enc=None Mac=SHA256
根据 this answer python pre 2.7.9/3.4.3 NOT 尝试对默认 ssl 上下文强制执行证书验证,因此容易受到 ssl 拦截。从 Python 2.7.9/3.4.3 开始,默认上下文强制执行证书验证。这也意味着,您必须为 pre 2.7.9/3.4.3 手动启用证书验证(通过创建自定义 sslcontext),否则任何不受信任的证书都可能被接受。
Should I use the ehlo() method of the SMTP object? In some examples it is called explicitly before and after calling starttls(). On the other side in the documentation of smptlib it is written, that sendmail() will call it, if it is necessary.
.sendmail()
、.send_message
和.starttls()
将隐式调用.ehlo_or_helo_if_needed()
,因此无需再次显式调用它。这也是
见下文 source::smtplib::starttls (cpython, inofficial github):
def starttls(self, keyfile=None, certfile=None, context=None):
"""Puts the connection to the SMTP server into TLS mode.
If there has been no previous EHLO or HELO command this session, this
method tries ESMTP EHLO first.
If the server supports TLS, this will encrypt the rest of the SMTP
session. If you provide the keyfile and certfile parameters,
the identity of the SMTP server and client can be checked. This,
however, depends on whether the socket module really checks the
certificates.
This method may raise the following exceptions:
SMTPHeloError The server didn't reply properly to
the helo greeting.
"""
self.ehlo_or_helo_if_needed()
if not self.has_extn("starttls"):
raise SMTPNotSupportedError(
"STARTTLS extension not supported by server.")
(resp, reply) = self.docmd("STARTTLS")
if resp == 220:
if not _have_ssl:
raise RuntimeError("No SSL support included in this Python")
if context is not None and keyfile is not None:
raise ValueError("context and keyfile arguments are mutually "
"exclusive")
if context is not None and certfile is not None:
raise ValueError("context and certfile arguments are mutually "
"exclusive")
if context is None:
context = ssl._create_stdlib_context(certfile=certfile,
keyfile=keyfile)
self.sock = context.wrap_socket(self.sock,
server_hostname=self._host)
self.file = None
# RFC 3207:
# The client MUST discard any knowledge obtained from
# the server, such as the list of SMTP service extensions,
# which was not obtained from the TLS negotiation itself.
self.helo_resp = None
self.ehlo_resp = None
self.esmtp_features = {}
self.does_esmtp = 0
return (resp, reply)