PHP 如何管控 cURL 出站连接

酷明酱_3631

酷明酱_3631

2026-11-13

977人浏览

原创

在 php 里对出站连接做细粒度管控,长期缺少一个可靠的钩子:用 stream context 能禁协议、限端口,却拿不到 dns 解析之后真正要连的目标地址,ssrf 策略只能靠字符串黑名单兜底。php-src 最新的提案 curlopt_opensocketfunction 正是补这一环——在 dns 解析后、connect() 前对每个即将创建的 socket 做允许或拒绝的决策。下面先说清它的用法形态,再交代这条提案目前走到哪一步。

2026-11-12,wiki.php.net 的 RFC 变更记录出现新条目「curl_opensocketfunction - created」(dc:date 2026-11-12T10:38:27Z),页面「PHP RFC: Vetting cURL connections with CURLOPT_OPENSOCKETFUNCTION」头部标注 Version 0.1、Date 2026-11-12、Author Xavier Leune ,Status 为 Under Discussion(变更 diff 显示 Status 由 Draft 改为 Under Discussion)。同日 10:42:54 UTC,php.internals 官方邮件列表发出公告「[RFC] Vetting cURL connections with CURLOPT_OPENSOCKETFUNCTION」(消息编号 133085),Foundation Discourse 同步出现同名 [PHP-DEV] 话题。实现落在 php-src PR #25414,讨论线程即上述 internals 消息;Voting thread 一栏仍为 tbd,RFC 索引页「Under Discussion」区已列入该条目,标注 Created 2026-11-12、Discussion started 2026-11-12。也就是说:提案刚刚进入正式讨论期,尚未发起投票,也未绑定任何 PHP 版本发布计划。

官方 RFC 页面

▲ 官方 RFC 页面

要解决的问题:SSRF 防护缺一个可靠的钩子

RFC 导语指出的场景很常见:应用把用户提供的 URL 交给 cURL 请求——webhook 回调、链接预览、图片代理、feed 阅读器、OAuth/OpenID 发现——URL 可能指向 127.0.0.1、云元数据端点 169.254.169.254 或其他内网服务,这就是典型的 SSRF(服务端请求伪造)风险面。作者的观点是:唯一可靠的决策时机是 DNS 解析之后、connect() 之前,作用于实际即将连接的地址、每一次重定向跳转与 libcurl 尝试的每个地址,而 ext/curl 目前恰好没有这个钩子。

现有 workaround 都不可靠:自己解析主机做校验再配合 Symfony NoPrivateNetworkHttpClient 一类的 CURLOPT_RESOLVE 固定解析,会阻塞请求、重复 libcurl 的解析工作,还得手动跟随每次重定向;而对 URL 字符串里的 host 做解析校验,则会被 libcurl 接受的等价写法绕过(2130706433、0x7f.1、127.1、IPv4 映射 IPv6 等),还会被 DNS rebinding 攻击击穿——校验时解析到公网地址,实际连接时 DNS 已换答案。libcurl 原生就有正确的钩子 CURLOPT_OPENSOCKETFUNCTION(每个 socket 创建前以解析后的地址被调用),本提案做的事情就是把它的能力暴露给 PHP 用户侧,并把返回值简化为「允许/拒绝」。

php.internals 官方邮件列表公告

▲ php.internals 官方邮件列表公告

API 设计:bool 回调 + CurlAddressFamily 枚举

提案新增常量 CURLOPT_OPENSOCKETFUNCTION(取 libcurl 原生值 20163)与枚举 CurlAddressFamily(IpV4 / IpV6 / Unix,命名风格对齐 Uri\UriHostType),回调签名固定为:

function (CurlHandle $handle, CurlAddressFamily $family, string $address, ?int $port): bool {}

四个参数依次是发起传输的 CurlHandle、地址族、字面地址(inet_ntop() 格式;Unix 时为 socket 路径,抽象名保留前导 NUL 字节)与目标端口(Unix 时为 null)。返回 true 放行,ext/curl 按常规用 socket(family, socktype, protocol) 建立连接,非阻塞模式、CURLOPT_INTERFACE、CURLOPT_SOCKOPTFUNCTION 等行为不变;返回 false 拒绝,libcurl 转而尝试该主机的下一个地址,全部失败后传输以 CURLE_COULDNT_CONNECT 结束;返回任何非 bool 值抛 TypeError 并拒绝连接;回调里抛出的异常会从 curl_exec()/curl_multi_exec() 传播出去,与其他 ext/curl 回调一致。

官方示例展示了典型用法——只允许公网可路由地址,其余一律拒绝:

curl_setopt($ch, CURLOPT_OPENSOCKETFUNCTION,
    function (CurlHandle $handle, CurlAddressFamily $family, string $address, ?int $port): bool {
        return $family !== CurlAddressFamily::Unix
            && filter_var($address, FILTER_VALIDATE_IP, FILTER_FLAG_GLOBAL_RANGE) !== false;
    });

实现 PR #25414 的 diff 可以看到两个关键设计。其一是 curl.stub.php 里同时落地了常量与枚举:

ext/curl/curl.stub.php
@@ -257,6 +257,11 @@
 	const CURLOPT_NOSIGNAL = UNKNOWN;
+/**
+ * @var int
+ * @cvalue CURLOPT_OPENSOCKETFUNCTION
+ */
+const CURLOPT_OPENSOCKETFUNCTION = UNKNOWN;
@@ -3765,6 +3770,17 @@
+/**
+ * The address family of the endpoint libcurl is about to connect to, as reported
+ * to the CURLOPT_OPENSOCKETFUNCTION callback.
+ */
+enum CurlAddressFamily
+{
+    case IpV4;
+    case IpV6;
+    case Unix;
+}

▲ php-src 实现提交(PR #25414)

其二是 interface.c 的 C 层桥接:任何无法向回调描述的情况都直接拒绝连接(fail closed),防止策略被未展示的端点绕过——非 AF_INET/AF_INET6/AF_UNIX 的地址族、非 CURLSOCKTYPE_IPCXN 用途的 socket(目前 libcurl 只用这一种)都会被拒绝而不调用回调;回调返回 true 时由扩展代为调用 socket() 创建描述符,用户侧永远拿不到裸 fd:

ext/curl/interface.c
@@ -837,6 +855,135 @@ static int curl_ssh_hostkeyfunction(void *clientp, int keytype, const char *key,
+static curl_socket_t curl_opensocketfunction(void *clientp, curlsocktype purpose, struct curl_sockaddr *address)
+{
+	php_curl *ch = (php_curl *) clientp;
+
+	/* Unreachable in normal operation: libcurl's own socket creation is restored
+	 * whenever no callback is installed, so the trampoline is only registered
+	 * while the FCC is initialized. Refuse rather than connect, so that an
+	 * unexpected state can never silently bypass the filter. */
+	if (UNEXPECTED(!ZEND_FCC_INITIALIZED(ch->handlers.opensocket))) {
+		return CURL_SOCKET_BAD;
+	}
+
+	/* Whatever cannot be described to the callback is refused rather than
+	 * connected: a policy must never be bypassed by an endpoint it was not
+	 * shown. CURLSOCKTYPE_IPCXN is the only purpose libcurl currently uses; a
+	 * future one may well not be a destination address at all. */
+	if (UNEXPECTED(purpose != CURLSOCKTYPE_IPCXN)) {
+		return CURL_SOCKET_BAD;
+	}

▲ php-src 实现提交(PR #25414)

调用时机与讨论中的取舍

回调按 socket 调用,而非按传输调用:一个主机解析出多个地址(或双栈 Happy Eyeballs)时每个尝试的地址各调一次;每个打开新连接的重定向跳点都会被审查。需要注意连接池复用的连接不创建 socket,回调不会被调用——要审查复用连接应使用 CURLOPT_PREREQFUNCTION(每次请求前运行)。RFC 还特别提醒 IPv4 映射 IPv6 的边界情况:::ffff:127.0.0.1 从 IpV6 socket 可达 IPv4 主机,策略必须看地址本身而非只看地址族;FILTER_FLAG_GLOBAL_RANGE 遵循 IANA 特殊用途注册表但接受 NAT64 前缀(64:ff9b::/96),该前缀可经 NAT64 网关到达 169.254.169.254,而 Symfony IpUtils::PRIVATE_SUBNETS 会拒绝这些前缀——用哪种黑名单是有讲究的。

CurlShip Directory Submission
CurlShip Directory Submission

向CurlShip提交产品,这是一个对机器人友好的SaaS目录。只需一条curl命令即可发布产品,支持OG标签抓取、带徽章的dofollow链接及层级升级。

下载

设计取舍上,早期提案(GH-22159)曾让回调返回 ext/sockets 的 Socket 对象,因扩展耦合与描述符所有权问题被否;第二提案(GH-23657)用 PHP 专有名 CURLOPT_PRECONNECTFUNCTION,讨论结论是「ext/curl 应保持 libcurl 薄封装」,因此本 RFC 沿用 libcurl 的选项名与值——公告里明确写了本提案取代 GH-23657。被否掉的还有 $purpose 参数(libcurl 目前只用一种用途,其他用途直接拒绝)。尚未解决的 Open Issue 只有一个:Windows 上 Winsock 头文件缺 sockaddr_un,AF_UNIX 端点会被直接拒绝而不调用回调,若社区有需求可通过 afunix.h 补齐。生态方面,作者提供了面向 PHP 8.3–8.6 的 polyfill 包 curl_hardening(MIT),在原生支持的 PHP 上自动失效。

wiki.php.net RFC 索引页

▲ wiki.php.net RFC 索引页

当前状态需要如实标注:RFC 仍在 Under Discussion,Voting thread 为 tbd,页面上的投票栏无有效票数,实现 PR #25414 待评审;目标版本写着「Next PHP 8.x」,属于提案意图而非已确认的发布计划。对使用者的观察点是 internals 讨论线程与 PR 评审进展、以及投票线程何时开启;在此之前不应把该特性当作已采纳或已发布的 PHP 能力,生产环境的 SSRF 防护仍需依赖现有手段。

关键信息

项目 内容
类目 RFC 与需求
级别 中
日期 2026-11-12(RFC 创建 10:38:27Z,internals 公告 10:42:54Z)
关联编号 RFC: curl_opensocketfunction(Version 0.1)
作者 Xavier Leune
状态 Under Discussion;Voting thread tbd,尚未发起投票,未绑定 PHP 版本
实现 php-src PR #25414(取代 GH-23657)
新增能力 CURLOPT_OPENSOCKETFUNCTION 回调(bool 允许/拒绝)与 CurlAddressFamily 枚举(IpV4 / IpV6 / Unix)
核心用途 在 DNS 解析后、connect() 前按实际地址执行 SSRF 策略,可抵御 DNS rebinding 与等价地址写法绕过
影响版本/范围 ext/curl 连接建立阶段(提案阶段,目标 Next PHP 8.x)

来源:https://wiki.php.net/rfc/curl_opensocketfunction

来源:https://news-web.php.net/php.internals/133085

来源:https://github.com/php/php-src/pull/25414

来源:https://patch-diff.githubusercontent.com/raw/php/php-src/pull/25414.diff

来源:https://wiki.php.net/rfc

php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!

相关专题

更多
php文件怎么打开
php文件怎么打开

打开php文件步骤:1、选择文本编辑器;2、在选择的文本编辑器中,创建一个新的文件,并将其保存为.php文件;3、在创建的PHP文件中,编写PHP代码;4、要在本地计算机上运行PHP文件,需要设置一个服务器环境;5、安装服务器环境后,需要将PHP文件放入服务器目录中;6、一旦将PHP文件放入服务器目录中,就可以通过浏览器来运行它。

2023.09.01

12764

6

php怎么取出数组的前几个元素
php怎么取出数组的前几个元素

取出php数组的前几个元素的方法有使用array_slice()函数、使用array_splice()函数、使用循环遍历、使用array_slice()函数和array_values()函数等。本专题为大家提供php数组相关的文章、下载、课程内容,供大家免费下载体验。

2023.10.11

7321

5

php反序列化失败怎么办
php反序列化失败怎么办

php反序列化失败的解决办法检查序列化数据。检查类定义、检查错误日志、更新PHP版本和应用安全措施等。本专题为大家提供php反序列化相关的文章、下载、课程内容,供大家免费下载体验。

2023.10.11

2255

5

php怎么连接mssql数据库
php怎么连接mssql数据库

连接方法:1、通过mssql_系列函数;2、通过sqlsrv_系列函数;3、通过odbc方式连接;4、通过PDO方式;5、通过COM方式连接。想了解php怎么连接mssql数据库的详细内容,可以访问下面的文章。

2023.10.23

4628

4

php连接mssql数据库的方法
php连接mssql数据库的方法

php连接mssql数据库的方法有使用PHP的MSSQL扩展、使用PDO等。想了解更多php连接mssql数据库相关内容,可以阅读本专题下面的文章。

2023.10.23

5334

6

html怎么上传
html怎么上传

html通过使用HTML表单、JavaScript和PHP上传。更多关于html的问题详细请看本专题下面的文章。php中文网欢迎大家前来学习。

2023.11.03

4211

9

PHP出现乱码怎么解决
PHP出现乱码怎么解决

PHP出现乱码可以通过修改PHP文件头部的字符编码设置、检查PHP文件的编码格式、检查数据库连接设置和检查HTML页面的字符编码设置来解决。更多关于php乱码的问题详情请看本专题下面的文章。php中文网欢迎大家前来学习。

2023.11.09

6097

8

php文件怎么在手机上打开
php文件怎么在手机上打开

php文件在手机上打开需要在手机上搭建一个能够运行php的服务器环境,并将php文件上传到服务器上。再在手机上的浏览器中输入服务器的IP地址或域名,加上php文件的路径,即可打开php文件并查看其内容。更多关于php相关问题,详情请看本专题下面的文章。php中文网欢迎大家前来学习。

2023.11.13

4782

8

sprintf函数用法详解
sprintf函数用法详解

sprintf函数的用法:1、格式化字符串;2、指定输出宽度和精度;3、返回值。更多关于sprintf函数用法详解的内容,大家可以阅读下面的文章。

2023.11.27

12522

4

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
《20天入门精通PHP》视频教程
《20天入门精通PHP》视频教程

共200课时 | 49.5万人学习

墨刀帮助中心
墨刀帮助中心

共0课时 | 0人学习

MyEclipse学习中心
MyEclipse学习中心

共0课时 | 0人学习