WordPress SQL 注入漏洞分析:从 author__not_in 参数到 REST API 全链路

**摘要:**本文基于 WordPress 7.0.1 源码,完整梳理了一条 SQL 注入漏洞的形成与利用链路:author__not_in 参数在 WP_Query 中被拼入 post_author NOT IN (...),而开发者仅用 is_array() 做了安全检查,导致字符串类型的输入可以绕过 absint() 强制整数转换,原始恶意字符串直接进入 SQL。为说明攻击者如何让内部参数接收外部输入,文章进一步分析 GET /wp/v2/posts 路由从注册、参数映射到 WP_REST_Posts_Controller::get_items() 调用 WP_Query 的完整过程,打通"HTTP 请求 → REST API → WP_Query → SQL 执行"的数据流。

由于个人水平有限,文中若有不足之处,欢迎批评指正。

sql漏洞怎么形成的

author__not_in 是什么?

从注释信息可以得到:author__not_in 是一个作者 ID 数组,表示这些作者的文章不要查询

这里把author__not_in放进了 $array_keys,紧接着在下面如果 author__not_in 没有提供,WordPress 会给它一个空数组作为默认值。

$query_vars 是WordPress 全局数组

author__not_in怎么变成 SQL语句

复制代码
if ( ! empty( $query_vars['author__not_in'] ) ) {
			if ( is_array( $query_vars['author__not_in'] ) ) {
				$query_vars['author__not_in'] = array_unique( 
                array_map( 
                    'absint', $query_vars['author__not_in'] 
                         ) 
                        );
				sort( $query_vars['author__not_in'] );
			}
$author__not_in = implode( ',', (array) $query_vars['author__not_in'] );
$where         .= " AND {$wpdb->posts}.post_author NOT IN ($author__not_in) ";
		} 

第一步:判断author__not_in是否为空

如果用户/程序提供了 author__not_in,并且它不是空的,就处理它。于是进入这个 if

第二步:检查传入的是不是数组

这里非常关键,因为前面我们已经知道:author__not_in设计上应该是:数组 。所以开发者这里进一步检查:**传进来的东西到底是不是数组?**如果是:1, 2, 3,那么is_array(...)返回true。进入下面。

第三步:真实代码中的 absint()

absint()强制类型转换,把传入变量强行变成整数

所以这里的原本作用就是:把作者 ID 按整数处理。 然后array_unique()去除重复值。最后:sort()排序。

第四步:把数组转换成 SQL 中需要的逗号分隔字符串。

复制代码
$author__not_in = implode( ',', (array) $query_vars['author__not_in'] );

(arrary)把变量强制转换为数组

比如:1, 2, 3经过:implode(',', 1,2,3),得到:1,2,3。于是:$author__not_in现在就是:"1,2,3"

第五步:真正进入 SQL查询

然后就是最关键的一行:

**wpdb:** WordPress **全局数据库对象** ,wpdb->posts → 存储文章的表名,带表前缀。

示例:sql = "SELECT \* FROM {wpdb->posts} WHERE ID=1"

执行后,字符串被解析成:SELECT * FROM wp_posts WHERE ID=1

回到本文的环境中

复制代码
$where .= " AND {$wpdb->posts}.post_author NOT IN ($author__not_in) ";

原本程序写的是:post_author NOT IN (...)。而:$author__not_in会被放进去。

正常情况下:$author__not_in = "1,2,3"于是:post_author NOT IN (1,2,3)这完全符合预期。

sql漏洞到底在哪里?

关键问题在于:

if ( is_array( $query_vars'author__not_in' ) )只有数组才执行 absint()。

如果这个变量在进入这里的时候是string ,那么is_array(...)就是false于是:array_map('absint', ...)根本不执行。

**总结:**is_array() 成为了安全处理的条件,而非数组输入可以绕过这个条件;后续 (array) 只是类型转换,不是安全校验,最终原始字符串进入了 SQL 拼接。

正常情况:1,2,3最后变成NOT IN (1,2,3)

漏洞情况:变成NOT IN (lastname,123)

漏洞是怎么被利用

首先思考的是攻击者实际上是怎么把这个"本来只能接收数组的内部参数",变成一个可控字符串,并让它进入 SQL 的?

攻击者真正需要解决的问题其实只有一个:

怎么让 author__not_in 以字符串形式到达这里?

因为如果直接传:author__not_in = 攻击者数据,它是数组,攻击者的数据就会被转换成整数。

所以:这就是为什么不能简单理解成"找到 SQL 拼接点然后传 Payload"

什么是 REST API?

1. API 是什么

API 可以简单理解成:程序和程序之间对话、拿数据、执行功能的通道

REST = 把资源的状态,以某种表述格式,在客户端与服务端之间转移

REST‑API:遵循 REST 这套设计风格写出来的 API 接口

可以把 WordPress 看成一个程序。正常情况下:浏览器访问 WordPress,WordPress 返回 HTML,这是给人/浏览器看的。但是现在很多程序并不需要 HTML。比如:手机 App、前端 JavaScript、WordPress 区块编辑器、其他程序它们更希望得到:

复制代码
{
    "id": 123,
    "title": "...",
    "author": 1
}

所以 WordPress 提供了一个专门的接口:

WordPress REST API

官方文档对它的定义就是:它提供一个接口,让应用通过 HTTP 与 WordPress 站点交互,并以 JSON 形式交换数据。


2. 为什么 WordPress 需要 REST API?

这个问题非常重要。不是因为"漏洞需要 REST API",而是:

REST API 本来就是 WordPress 正常功能的一部分。

WordPress 需要让其他程序能够:读取文章、读取页面、读取用户、创建文章、修改文章、删除文章。所以它提供了一套标准 HTTP 接口。

例如 WordPress 官方 API 中:**/wp/v2/posts对应文章。/wp/v2/users对应用户。/wp/v2/pages对应页面。**这些都是 WordPress REST API 的资源路由。

例如 WordPress 的 Posts:

复制代码
GET
/wp/v2/posts

意思是:获取文章列表。WordPress 官方文档明确把:GET /wp/v2/posts定义为获取 Posts 集合的接口


3. 这里的 posts 是什么?

这个就和漏洞直接开始产生关系了。

WordPress 里面有一个非常重要的数据类型:Post也就是文章 。REST API 把 WordPress 中的文章作为一个资源暴露出来。

所以/wp/v2/posts就是:由 WordPress REST API v2 版本接口前缀,指向 posts 资源(文章集合),映射数据库 wp_posts 文章表

URL 分段:

/wp/v2/:标识 WordPress 程序,指定接口版本 2,作为 REST API 根路径,用来区分普通网页访问

/posts:命名资源 posts,代表文章集合;REST 风格把资源名称写在路径中,不使用?action=xxx 形式,对应 wp_posts 表

HTTP 方法语义:

GET /wp/v2/posts:通过路径定位全部文章资源,使用 GET 执行获取操作,读取全部文章,返回 JSON 格式列表

因此,当程序请求:GET /wp-json/wp/v2/posts时WordPress 就需要:找到文章,然后把文章数据转换成 JSON 返回。**官方文档也明确说明,这个请求大致对应 WordPress 内部的 WP_Query。**这一点对研究 SQL 注入非常重要。


4.为什么这一点和我们的 SQL 注入有关?

在前面的解释中我们已经可以知道author__not_in 最终会影响文章查询的 SQL。但是现在问题来了:谁会调用****WP_Query ?我们不能凭空说:

只要 HTTP 传来 author__not_in,就一定自动交给 WP_Query 处理。 需要确认哪段代码会接收 HTTP 参数,并把这个参数传给 WP_Query ,只有代码做了这一步,链路才成立;如果没有代码做参数映射,即使请求带上 author__not_in,WP_Query 也接收不到,不会修改 SQL。

/wp/v2/posts****恰好就是 WordPress 对外提供的查询文章接口,该接口内部实现了这层逻辑:读取 HTTP 传入的查询参数,把author__not_in这类参数直接传递给WP_Query,再由WP_Query拼接生成 SQL 语句执行数据库查询,完整打通从 HTTP 请求到 SQL 执行的整条链路。


5.小总结

目前已经知道: WP_Query****用来查询 WordPress 文章。 而 REST API 接口 GET /wp‑json/wp/v2/posts 的作用,同样用来查询 WordPress 文章。

所以两者会产生调用联系:HTTP 请求发送到 WordPress REST API,接口执行 Posts 资源查询操作,内部调用 WP_Query,最终访问数据库完成数据读取。

官方文档说明: GET /wp‑json/wp/v2/posts 大致等价于使用 WP_Query 获取文章集合。


6.WP_Query是什么

WP_Query 是 WordPress 内置的核心****PHP ,文件位于:wp‑includes/class‑wp‑query.php。

作用:专门封装文章、页面、自定义类型数据的查询逻辑,接收一组查询参数,内部自动拼接 SQL,向 MySQL 数据库查询内容。

关键点 WP_Query 本身不会自动读取****HTTP 请求参数只有上层代码主动读取 HTTP 输入,把参数传给 WP_Query 实例,参数才会生效;REST‑API 接口内部就做了这一步参数搬运,所以外部 HTTP 参数可以影响 SQL。


也就是说WP_Query 属于 WordPress 的内部查询机制。 攻击者只能发起 HTTP 请求,无法直接在服务器 PHP 代码里直接赋值$query_vars'author__not_in'

攻击者必须找到一条可用数据流:HTTP 请求流入 WordPress 某个功能模块,再由该功能模块把参数传递给 WP_Query。

REST API 就是一类很重要的入口,完成从 HTTP 到 WordPress 内部功能的对接。

因此整体攻击数据流 为:攻击者发送****HTTP 请求,请求到达 REST API,接口处理 Posts 资源查询,内部调用 WP_Query,最后由 WP_Query 拼接生成 SQL 语句执行数据库查询。

调用WP_Query在哪里

"哪一处代码会调用 WP_Query 做文章查询?

在wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php中的

复制代码
WP_REST_Posts_Controller::get_items()

分析如下

第一层:谁负责 Posts REST API?

文件:wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php

这里已经告诉我们:Core class to access posts via the REST API。也就是:用 REST API 访问 Posts 的核心类。

WP_REST_Posts_Controller 就是 Posts 的 REST API Controller

第二层:这个 Controller 怎么知道自己负责 /posts

看这个文件中的构造函数:public function __construct():

作用:实例化这个类的时候自动执行,接收文章类型参数做初始化

**$this->namespace 是什么?**wp/v2

复制代码
$this->namespace = ! empty( $obj->rest_namespace )? $obj->rest_namespace: 'wp/v2';

默认注册文章类型时没有设置 rest_namespace,obj-\>rest_namespace为空。 这里最终会得到:this->namespace的值为wp/v2。所以wp/v2就是这个 REST API Controller 所使用的 namespace。


**$this->rest_base 是什么?**posts

复制代码
$this->rest_base = ! empty( $obj->rest_base )? $obj->rest_base: $obj->name;

它决定:这个资源在 REST API URL 中使用什么名字。

$obj->name = post(数据类型内部名称,单数)

注册 post 类型时手动设置了rest_base="posts"

obj-\>rest_base不为空,所以 this->rest_base = "posts"

所以接口路径是 /wp‑json/wp/v2/posts,而不是 /wp‑json/wp/v2/post

对于 WordPress 默认的文章 Post Type:post,对应的 REST base 是:posts

所以现在有:

$this->namespace的值为wp/v2

$this->rest_base的值为posts

这两个东西稍后会被拼起来,


第三层:真正把 /wp/v2/posts 注册出来的代码在哪里?

核心代码是:

常量 实际 HTTP 方法 对应图中回调 作用
READABLE GET get_items() 读取资源,获取文章列表 ,只读操作,不会修改数据库
CREATABLE POST create_item() 新建资源,创建一篇新文章,写入数据库,提交数据

在这里调用了register_rest_route()函数

register_rest_route()这个函数的作用:**向 WordPress REST API 注册一个路由。**也就是说:"以后有人访问这个 URL,应该交给哪个代码处理?"就是在这里建立关系。

trim --- 去除字符串首尾处的空白字符(或者其他字符)这里是/

注册**/wp/v2/posts**

这儿full_route最终拼接成/wp/v2/posts,调用register_route()完成注册

894‑914 行:判断命名空间是否存在,不存在就自动注册一条命名空间索引路由

$this‑>endpoints

作用:内存里的 REST 路由注册表,保存全部 API 接口的配置信息

this-\>endpoints\['/wp/v2/posts'\] = route_args;

$route_args:单条 REST 接口全部运行配置。包含请求方法、业务回调、权限回调、请求参数校验规则、命名空间。注册时存入 endpoints;请求到来读取它,完成鉴权、参数校验、执行控制器函数。

所以:/wp/v2/posts 这个REST API 路由 就是在这里注册出来的


这里最重要的是 callback

复制代码
'callback' => array( $this, 'get_items' ),

它的意思不是"调用 get_items"。而是:告诉 REST API:以后有人请求这个路由,并且符合这个方法时,应该交给谁处理。

这里指定:访问接口 /wp/v2/posts,程序匹配到对应的 REST 路由,读取路由配置中的 callback 回调设置。callback 指定使用 WP_REST_Posts_Controller 对象的 get_items 方法,程序调用该对象的 get_items () 方法,该方法完成数据查询,返回文章列表的 JSON 数据。

所以GET /wp-json/wp/v2/posts,最终对应:WP_REST_Posts_Controller::get_items()

所以现在整个关系已经是:

  1. 客户端发起 HTTP 请求:GET /wp‑json/wp/v2/posts
  2. WordPress 框架剥离 URL 前缀/wp‑json,得到内部路由路径:/wp/v2/posts
  3. REST 服务在$this‑>endpoints路由表匹配到/wp/v2/posts,找到对应控制器 WP_REST_Posts_Controller
  4. 根据 HTTP 方法为 GET,调度执行控制器方法 get_items()
  5. get_items() 查询文章数据,最终返回 JSON 响应。

get_itms()查询

复制代码
public function get_items( $request )

这就是刚才 callback 指向的函数。

  1. prepare_items_query:将 REST 接口请求参数,转换成WP_Query可用的查询条件数组$query_args

  2. new WP_Query():实例化 WordPress 文章查询对象

  3. posts_query-\>query(query_args):传入查询条件,执行数据库查询,获取文章数据

    query_result = posts_query->query( $query_args );

这就是:Posts REST API 最终调用 WP_Query 查询文章的真实代码


prepare_items_query():将 REST 接口的 HTTP 请求参数,转换为 WP_Query 可识别的查询参数数组,完成参数映射、适配处理,返回组装好的$query_args,供后续 WP_Query 执行数据库查询。

例如:GET /wp-json/wp/v2/posts?author_exclude=2,6

WordPress 内部 " 翻译 " :请求到达服务器后,prepare_items_query() 方法会负责将 REST API 的参数(author_exclude)"翻译"成 WP_Query 能理解的参数。相关的映射关系在 WordPress 核心代码中有明确定义,用于转换参数。

**author_exclude 这个用户友好的参数会被映射为 author__not_in。**翻译后,WP_Query 接收到的查询参数就是 'author__not_in' => 2, 6

执行查询:WP_Query 拿到 author__not_in 参数后,会构建 SQL 查询语句,从数据库中排除作者 ID 为 2 和 6 的所有文章,最终返回符合条件的结果。

复制代码
$query_args = array(
    'posts_per_page'    => 10,          // 默认值
    'paged'             => 1,           // 默认值
    'post_status'       => 'publish',   // 默认值
    'post_type'         => 'post',      // 默认值
    'orderby'           => 'date',      // 默认值
    'order'             => 'DESC',      // 默认值
    'author__not_in'    => array(2, 6), //  author_exclude 映射过来的
    'ignore_sticky_posts' => true,      // 默认值
    'suppress_filters'  => true,        // 默认值
);

真正执行WP_Query 查询文章的地方

  1. 保存参数:把传入的 $query 保存到对象的 query 和 query_vars 属性中。
  2. 执行查询:调用 return $this->get_posts(),这个方法内部会构建 SQL 语句,连接数据库,把文章数据查出来并返回。

这里也就回到了文章开头的查询部分。

相关推荐
IT_陈寒39 分钟前
Vite打包时踩了个坑,static资源去哪了?
前端·人工智能·后端
龙虾PRO40 分钟前
2026 年 AI 智能体工具调用:ReAct 模式与函数调用怎么选才不踩坑
前端·人工智能·react.js
ryan_99642 分钟前
生产环境向量检索数据库选型:Elasticsearch、Qdrant、Milvus 与 Chroma
数据库·elasticsearch·milvus·向量数据库·chroma·qdrant
何以解忧,唯有..42 分钟前
Django 框架入门指南:从零开始构建 Web 应用
前端·python·django
SCandL15242 分钟前
k8s工作原理
linux·运维·服务器
daols881 小时前
vue 甘特图 vxe-gantt 紧前紧后依赖关系连接线配置详解
前端·vue.js·甘特图
恋猫de小郭1 小时前
Flutter iOS 的深度优化 PR,搞笑的是贡献者被 Gemini 评审折磨
android·前端·flutter
weixin_444579301 小时前
Rockylinux 与 K8s 的安装
linux·云原生·容器·kubernetes
2501_937860941 小时前
从JDBC到数据访问:Java数据库编程完全指南
java·开发语言·数据库