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

▲ 官方 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 官方邮件列表公告
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提交产品,这是一个对机器人友好的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 索引页
当前状态需要如实标注: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










