Skip to content

SSL: UNSAFE_LEGACY_RENEGOTIATION_DISABLED 错误解决方法

问题描述

当使用 Python 代码通过 HTTPS 连接到特定服务器时(如 ssd.jpl.nasa.gov:443),可能会出现以下 SSL 错误:

python
requests.exceptions.SSLError: HTTPSConnectionPool(host='ssd.jpl.nasa.gov', port=443): 
Max retries exceeded with url: /api/horizons.api?format=text&EPHEM_TYPE=OBSERVER&QUANTITIES_[...]_ 
(Caused by SSLError(SSLError(1, '[SSL: UNSAFE_LEGACY_RENEGOTIATION_DISABLED] 
unsafe legacy renegotiation disabled (_ssl.c:997)')))

该错误通常发生在使用 OpenSSL 3.x 版本连接不支持安全重协商的旧服务器时。

安全警告

启用传统不安全重协商会使 SSL 连接容易受到 CVE-2009-3555 中描述的中间人前缀攻击。建议仅在必要时临时使用这些解决方案。

解决方案

方法一:使用自定义 SSL 上下文(推荐)

这是最灵活的解决方法,可以在代码级别控制 SSL 配置:

python
import ssl
import requests
import urllib3

class CustomHttpAdapter(requests.adapters.HTTPAdapter):
    """传输适配器,允许使用自定义 ssl_context"""
    
    def __init__(self, ssl_context=None, **kwargs):
        self.ssl_context = ssl_context
        super().__init__(**kwargs)
    
    def init_poolmanager(self, connections, maxsize, block=False):
        self.poolmanager = urllib3.poolmanager.PoolManager(
            num_pools=connections, 
            maxsize=maxsize,
            block=block, 
            ssl_context=self.ssl_context
        )

def get_legacy_session():
    """创建支持传统重协商的会话"""
    ctx = ssl.create_default_context(ssl.Purpose.SERVER_AUTH)
    ctx.options |= 0x4  # OP_LEGACY_SERVER_CONNECT 标志
    session = requests.session()
    session.mount('https://', CustomHttpAdapter(ctx))
    return session

# 使用示例
session = get_legacy_session()
response = session.get("https://target-website.com/api/data")

方法二:使用 urllib.request

如果不想使用 requests 库,可以使用标准库的解决方案:

python
import urllib.request
import ssl

# 创建自定义 SSL 上下文
ssl_context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH)
ssl_context.options |= 0x4  # OP_LEGACY_SERVER_CONNECT 标志

url = "https://target-website.com/api/data"
response = urllib.request.urlopen(url, context=ssl_context)
data = response.read().decode("utf-8")

方法三:aiohttp 异步请求

对于异步应用,可以使用以下方法:

python
import aiohttp
import ssl

async def secure_request(url):
    custom_ssl_context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH)
    custom_ssl_context.options |= 0x00040000  # SSL_OP_ALLOW_UNSAFE_LEGACY_RENEGOTIATION
    
    connector = aiohttp.TCPConnector(ssl=custom_ssl_context)
    
    async with aiohttp.ClientSession(connector=connector) as session:
        async with session.get(url) as response:
            return await response.text()

# 使用示例(在异步函数中)
# result = await secure_request("https://target-website.com/api/data")

开发环境警告

以下设置会降低安全性,仅推荐在开发环境中使用:

python
# 禁用主机名验证和证书验证(仅用于测试)
custom_ssl_context.check_hostname = False
custom_ssl_context.verify_mode = ssl.CERT_NONE

方法四:环境变量配置(系统级)

可以通过设置 OpenSSL 配置文件来解决此问题:

  1. 创建自定义 OpenSSL 配置文件 custom_openssl.cnf
ini
openssl_conf = openssl_init

[openssl_init]
ssl_conf = ssl_sect

[ssl_sect]
system_default = system_default_sect

[system_default_sect]
Options = UnsafeLegacyRenegotiation
  1. 设置环境变量指向该配置文件:
bash
# 临时设置
export OPENSSL_CONF=/path/to/custom_openssl.cnf
python your_script.py

# 或在运行命令时直接设置
OPENSSL_CONF=/path/to/custom_openssl.cnf python your_script.py

注意事项

这种方法会影响所有使用系统 OpenSSL 库的应用,且可能在 OpenSSL 更新时被覆盖。

其他语言的解决方案

Ruby

ruby
# 设置 OP_LEGACY_SERVER_CONNECT 选项
OpenSSL::SSL::SSLContext::DEFAULT_PARAMS[:options] |= OpenSSL::SSL::OP_LEGACY_SERVER_CONNECT

# 发起请求
uri = URI('https://example.com')
res = Net::HTTP.post(uri, {}.to_json)

# 请求完成后取消设置
OpenSSL::SSL::SSLContext::DEFAULT_PARAMS[:options] &= ~OpenSSL::SSL::OP_LEGACY_SERVER_CONNECT

Node.js

某些情况下,从 Node.js 18 降级到 16 可以解决此问题,但不推荐作为长期解决方案。

替代方案

降级 OpenSSL(不推荐)

对于使用 conda 的环境:

bash
conda install -n your-env-name openssl=1.0

降级 cryptography 包

bash
pip install cryptography==36.0.2

最佳实践

  1. 优先使用方法一或方法二,它们只影响当前应用而非整个系统
  2. 仅对确实需要连接的不支持安全重协商的服务器启用此选项
  3. 长期解决方案应该是更新服务器端以支持安全重协商
  4. 定期检查服务器是否已更新,以便及时移除这些临时解决方案

总结

UNSAFE_LEGACY_RENEGOTIATION_DISABLED 错误是由于客户端使用 OpenSSL 3.x 的安全标准与旧服务器不兼容导致的。通过创建自定义 SSL 上下文并设置适当的选项,可以临时解决此问题,但应注意相关的安全风险。

建议服务器管理员尽快更新服务器以支持安全重协商,这是最根本的解决方案。