Python - request.exceptions.SSLError - dh 键太小
- 2025-03-11 08:50:00
- admin 原创
- 72
问题描述:
我正在使用 Python 和请求抓取一些内部页面。我已关闭 SSL 验证和警告。
requests.packages.urllib3.disable_warnings()
page = requests.get(url, verify=False)
在某些服务器上,我收到了无法解决的 SSL 错误。
Traceback (most recent call last):
File "scraper.py", line 6, in <module>
page = requests.get(url, verify=False)
File "/cygdrive/c/Users/jfeocco/VirtualEnv/scraping/lib/python3.4/site-packages/requests/api.py", line 71, in get
return request('get', url, params=params, **kwargs)
File "/cygdrive/c/Users/jfeocco/VirtualEnv/scraping/lib/python3.4/site-packages/requests/api.py", line 57, in request
return session.request(method=method, url=url, **kwargs)
File "/cygdrive/c/Users/jfeocco/VirtualEnv/scraping/lib/python3.4/site-packages/requests/sessions.py", line 475, in request
resp = self.send(prep, **send_kwargs)
File "/cygdrive/c/Users/jfeocco/VirtualEnv/scraping/lib/python3.4/site-packages/requests/sessions.py", line 585, in send
r = adapter.send(request, **kwargs)
File "/cygdrive/c/Users/jfeocco/VirtualEnv/scraping/lib/python3.4/site-packages/requests/adapters.py", line 477, in send
raise SSLError(e, request=request)
requests.exceptions.SSLError: [SSL: SSL_NEGATIVE_LENGTH] dh key too small (_ssl.c:600)
在 Windows 和 OSX 中,Cygwin 内外都会发生这种情况。我的研究表明服务器上的 OpenSSL 已过时。我正在寻找理想的修复客户端。
编辑:我能够通过使用密码集来解决这个问题
import requests
requests.packages.urllib3.util.ssl_.DEFAULT_CIPHERS += 'HIGH:!DH:!aNULL'
try:
requests.packages.urllib3.contrib.pyopenssl.DEFAULT_SSL_CIPHER_LIST += 'HIGH:!DH:!aNULL'
except AttributeError:
# no pyopenssl support used / needed / available
pass
page = requests.get(url, verify=False)
解决方案 1:
这不是一个额外的答案,只是尝试将问题中的解决方案代码与额外信息结合起来,这样其他人就可以直接复制它而无需额外尝试
这不仅是服务器端的 DH Key 问题,而且 Python 模块中许多不同的库不匹配。
下面的代码段用于忽略这些安全问题,因为这些问题可能无法在服务器端解决。例如,如果它是内部旧服务器,没有人愿意更新它。
除了破解的字符串之外'HIGH:!DH:!aNULL'
,还可以导入urllib3模块来禁用警告(如果有)
import requests
import urllib3
requests.packages.urllib3.disable_warnings()
requests.packages.urllib3.util.ssl_.DEFAULT_CIPHERS += ':HIGH:!DH:!aNULL'
try:
requests.packages.urllib3.contrib.pyopenssl.util.ssl_.DEFAULT_CIPHERS += ':HIGH:!DH:!aNULL'
except AttributeError:
# no pyopenssl support used / needed / available
pass
page = requests.get(url, verify=False)
解决方案 2:
这对我也有用:
import requests
import urllib3
requests.packages.urllib3.util.ssl_.DEFAULT_CIPHERS = 'ALL:@SECLEVEL=1'
openssl SECLEVELs 文档:
https://www.openssl.org/docs/manmaster/man3/SSL_CTX_set_security_level.html
SECLEVEL=2 是目前 openssl 的默认设置(至少在我的设置中:ubuntu 20.04,openssl 1.1.1f);SECLEVEL=1 降低了标准。
安全级别旨在避免修改单个密码的复杂性。
我相信我们大多数普通人并不了解单个密码的安全强度/弱点,我当然不知道。安全级别似乎是一种很好的方法,可以控制打开安全门的程度。
注意:我收到了不同的 SSL 错误,WRONG_SIGNATURE_TYPE 而不是 SSL_NEGATIVE_LENGTH,但根本问题是相同的。
错误:
Traceback (most recent call last):
[...]
File "/usr/lib/python3/dist-packages/requests/sessions.py", line 581, in post
return self.request('POST', url, data=data, json=json, **kwargs)
File "/usr/lib/python3/dist-packages/requests/sessions.py", line 533, in request
resp = self.send(prep, **send_kwargs)
File "/usr/lib/python3/dist-packages/requests/sessions.py", line 646, in send
r = adapter.send(request, **kwargs)
File "/usr/lib/python3/dist-packages/requests/adapters.py", line 514, in send
raise SSLError(e, request=request)
requests.exceptions.SSLError: HTTPSConnectionPool(host='somehost.com', port=443): Max retries exceeded with url: myurl (Caused by SSLError(SSLError(1, '[SSL: WRONG_SIGNATURE_TYPE] wrong signature type (_ssl.c:1108)')))
解决方案 3:
禁用警告或证书验证不会有帮助。根本问题是服务器使用的 DH 密钥较弱,可能会在Logjam Attack中被滥用。
要解决这个问题,您需要选择一个不使用 Diffie Hellman 密钥交换的密码,这样就不会受到弱 DH 密钥的影响。并且服务器必须支持此密码。目前尚不清楚服务器支持什么,但您可以尝试使用以下密码AES128-SHA
或密码集HIGH:!DH:!aNULL
使用带有您自己的密码集的请求比较棘手。请参阅为什么 Python 请求会忽略验证参数?以获取示例。
解决方案 4:
我遇到了同样的问题。
并且已通过评论修复
CipherString = DEFAULT@SECLEVEL=2
插线/etc/ssl/openssl.cnf
。
解决方案 5:
urllib3
从 的版本 2或 的版本 2.30开始requests
,该DEFAULT_CIPHERS
属性被删除,并且这里提出的大多数解决方案不再有效。相反,您必须创建一个SSLContext
:
from urllib3.util import create_urllib3_context
from urllib3 import PoolManager
ctx = create_urllib3_context(ciphers=":HIGH:!DH:!aNULL")
http = PoolManager(ssl_context=ctx)
http.request("GET", ...)
如果你正在使用,requests
则必须将自己的子类化HTTPAdapter
,以初始化 a PoolManager
,并将其挂载到 a 中Session
:
from urllib3.util import create_urllib3_context
from urllib3 import PoolManager
from requests.adapters import HTTPAdapter
from requests import Session
class AddedCipherAdapter(HTTPAdapter):
def init_poolmanager(self, connections, maxsize, block=False):
ctx = create_urllib3_context(ciphers=":HIGH:!DH:!aNULL")
self.poolmanager = PoolManager(
num_pools=connections,
maxsize=maxsize,
block=block,
ssl_context=ctx
)
s = Session()
s.mount("https://example.org", AddedCipherAdapter())
s.get("https://example.org/path")
这种方法的好处是其范围仅限于受影响的服务器https://example.org
。
此外,有些人希望禁用主机名和证书检查,我不建议这样做,而且这不是修复弱 DH 密钥所必需的。这些人必须在创建的上下文对象上添加这些选项ctx
:
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
再次强调,请在执行禁用证书检查之前了解所涉及的安全风险。
解决方案 6:
来自请求 Python 库核心开发团队的某个人记录了一个方法,用于将更改限制在一台或几台服务器上:
https://lukasa.co.uk/2017/02/Configuring_TLS_With_Requests/
如果您的代码与多台服务器交互,那么不要因为一台服务器的配置有问题而降低所有连接的安全要求。
代码对我来说开箱即用。也就是说,使用我自己的值CIPHERS
。'ALL:@SECLEVEL=1'
解决方案 7:
不要覆盖默认的全局密码可能会更安全,而是在特定会话中使用所需的密码创建自定义 HTTPAdapter:
import ssl
from typing import Any
import requests
class ContextAdapter(requests.adapters.HTTPAdapter):
"""Allows to override the default context."""
def __init__(self, *args: Any, **kwargs: Any) -> None:
self.ssl_context: ssl.SSLContext|None = kwargs.pop("ssl_context", None)
super().__init__(*args, **kwargs)
def init_poolmanager(self, *args: Any, **kwargs: Any) -> Any:
# See available keys in urllib3.poolmanager.SSL_KEYWORDS
kwargs.setdefault("ssl_context", self.ssl_context)
return super().init_poolmanager(*args, **kwargs)
然后您需要创建自定义上下文,例如:
import ssl
def create_context(
ciphers: str, minimum_version: int, verify: bool
) -> ssl.SSLContext:
"""See https://peps.python.org/pep-0543/."""
ctx = ssl.create_default_context()
# Allow to use untrusted certificates.
if not verify:
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
# Just for example.
if minimum_version == ssl.TLSVersion.TLSv1:
ctx.options &= (
~getattr(ssl, "OP_NO_TLSv1_3", 0)
& ~ssl.OP_NO_TLSv1_2
& ~ssl.OP_NO_TLSv1_1
)
ctx.minimum_version = minimum_version
ctx.set_ciphers(ciphers)
return ctx
然后您需要使用自定义上下文规则配置每个网站:
session = requests.Session()
session.mount(
"https://dh.affected-website.com",
ContextAdapter(
ssl_context=create_context(
ciphers="HIGH:!DH:!aNULL"
),
),
)
session.mount(
"https://only-elliptic.modern-website.com",
ContextAdapter(
ssl_context=create_context(
ciphers="ECDHE+AESGCM"
),
),
)
session.mount(
"https://only-tls-v1.old-website.com",
ContextAdapter(
ssl_context=create_context(
ciphers="DEFAULT:@SECLEVEL=1",
minimum_version=ssl.TLSVersion.TLSv1,
),
),
)
result = session.get("https://only-tls-v1.old-website.com/object")
阅读完所有答案后,我可以说@bgoeman 的答案与我的很接近,您可以点击他们的链接了解更多信息。
解决方案 8:
根据用户 bgoeman 给出的回答,以下代码可以工作,它保留默认密码,仅添加安全级别。
import requests
requests.packages.urllib3.util.ssl_.DEFAULT_CIPHERS += '@SECLEVEL=1'
解决方案 9:
我从 18.04 升级到 Ubuntu 20.04 后遇到了这个问题,以下命令对我有效。
pip install --ignore-installed pyOpenSSL --upgrade
解决方案 10:
我将在这里打包我的解决方案。我不得不修改 python SSL 库,这是可能的,因为我在 docker 容器中运行我的代码,但这可能是你不想做的事情。
获取服务器支持的密码。我的情况是第三方电子邮件服务器,我使用了脚本描述的SSL/TLS 密码套件列表
检查支持的密码
#!/usr/bin/env bash
# OpenSSL requires the port number.
SERVER=$1
DELAY=1
ciphers=$(openssl ciphers 'ALL:eNULL' | sed -e 's/:/ /g')
echo Obtaining cipher list from $(openssl version).
for cipher in ${ciphers[@]}
do
echo -n Testing $cipher...
result=$(echo -n | openssl s_client -cipher "$cipher" -connect $SERVER 2>&1)
if [[ "$result" =~ ":error:" ]] ; then
error=$(echo -n $result | cut -d':' -f6)
echo NO ($error)
else
if [[ "$result" =~ "Cipher is ${cipher}" || "$result" =~ "Cipher :" ]] ; then
echo YES
else
echo UNKNOWN RESPONSE
echo $result
fi
fi
sleep $DELAY
done
授予其权限:
chmod +x check_supported_ciphers.sh
并执行:
./check_supported_ciphers.sh myremoteserver.example.com | grep YES
几秒钟后,您将看到类似以下内容的输出:
Testing AES128-SHA...YES (AES128-SHA_set_cipher_list)
因此将使用“ AES128-SHA ”作为 SSL 密码。
强制代码中的错误:
回溯(最近一次调用):文件“my_custom_script.py”,第 52 行,在 imap = IMAP4_SSL(imap_host)文件“/usr/lib/python2.7/imaplib.py”,第 1169 行,在init
IMAP4 中。init(self,host,port)文件“/usr/lib/python2.7/imaplib.py”,第 174 行,在init中
self.open(host,port)文件“/usr/lib/python2.7/imaplib.py”,第 1181 行,在 open 中 self.slobj = ssl.wrap_socket(self.sock,self.keyfile,self.certfile)文件“/usr/lib/python2.7/ssl.py”,第 931 行,在 wrap_socket 中 ciphers=ciphers)文件“/usr/lib/python2.7/ssl.py”,第 599 行,在init中
self.do_handshake() 文件“/usr/lib/python2.7/ssl.py”,第 828 行,在 do_handshake 中 self._sslobj.do_handshake() ssl.SSLError:[SSL:DH_KEY_TOO_SMALL] dh 密钥太小(_ssl.c:727)
获取使用的python SSL库路径,在本例中为:
/usr/lib/python2.7/ssl.py
編輯:
cp /usr/lib/python2.7/ssl.py /usr/lib/python2.7/ssl.py.bak
vim /usr/lib/python2.7/ssl.py
并替换:
_DEFAULT_CIPHERS = (
'ECDH+AESGCM:ECDH+CHACHA20:DH+AESGCM:DH+CHACHA20:ECDH+AES256:DH+AES256:'
'ECDH+AES128:DH+AES:ECDH+HIGH:DH+HIGH:RSA+AESGCM:RSA+AES:RSA+HIGH:'
'!aNULL:!eNULL:!MD5:!3DES'
)
经过:
_DEFAULT_CIPHERS = (
'AES128-SHA'
)
解决方案 11:
在CentOS 7上,在/etc/pki/tls/openssl.cnf中搜索以下内容:
[ crypto_policy ]
.include /etc/crypto-policies/back-ends/opensslcnf.config
[ new_oids ]
在 /etc/crypto-policies/back-ends/opensslcnf.config 中设置“ALL:@SECLEVEL=1”。
解决方案 12:
在 docker 镜像中,您可以添加以下命令来Dockerfile
解决此问题:
RUN sed -i '/CipherString = DEFAULT/s/^#?/#/' /etc/ssl/openssl.cnf
这会自动注释掉有问题的CipherString
行。
解决方案 13:
如果您正在使用httpx
库,则可以跳过警告:
import httpx
httpx._config.DEFAULT_CIPHERS += ":HIGH:!DH:!aNULL"
解决方案 14:
我遇到了以下错误:
SSLError:[SSL:DH_KEY_TOO_SMALL] dh 密钥太小(_ssl.c:727)
我解决了它(Fedora):
python2.7 -m pip 卸载请求
python2.7 -m pip 卸载 pyopenssl
python2.7 -m pip install pyopenssl==你的版本
python2.7 -m pip install 请求==你的版本
顺序模块安装导致:
请求.包.urllib3.contrib.pyopenssl.util.ssl_.DEFAULT_CIPHERS
当模块确实存在时,“requests.packages.urllib3.contrib”中出现 AttributeError“pyopenssl”。
解决方案 15:
我专门为这个帖子创建了一个帐户,因为这些答案对我都不起作用。使用Robin De Schepper的一段代码作为基准,我成功地让它工作了,只需要一个小小的调整
原始代码片段:
from urllib3.util import create_urllib3_context from urllib3 import PoolManager ctx = create_urllib3_context(ciphers=":HIGH:!DH:!aNULL") http = PoolManager(ssl_context=ctx) http.request("GET", ...)
修改后的一段代码:
import ssl # needed for the cert argument
from urllib3.util import create_urllib3_context
from urllib3 import PoolManager
# cert_reqs=ssl.CERT_NONE is the one adjustment that made this work
ctx = create_urllib3_context(ciphers=":HIGH:!DH:!aNULL", cert_reqs=ssl.CERT_NONE)
http = PoolManager(ssl_context=ctx)
http.request("GET", ...)
解决方案 16:
如果有人在使用 ftplib 时遇到同样的问题,我会添加此内容。
解决方案是使用旧配置创建上下文:
context = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2)
context.set_ciphers('DEFAULT@SECLEVEL=1')
with ftplib.FTP_TLS(
host=self.ftp_host,
user=self.ftp_user,
passwd=self.ftp_password,
context=context,
) as ftp:
...
扫码咨询,免费领取项目管理大礼包!