发布于5小时前5小时 一、漏洞概述本文记录一类常见于 WordPress 自研主题和插件的权限控制漏洞:插件注册自定义 REST API 路由时未设置有效的 permission_callback,或回调函数只判断用户是否登录,却没有校验角色、能力和对象归属,导致未授权用户读取订单、客户资料、工单或内部配置。该问题通常不属于 WordPress 核心漏洞,而是插件业务接口的访问控制缺陷。漏洞验证应在获得书面授权的测试环境或指定生产范围内进行,重点确认接口是否越过了登录边界和对象权限边界,不应批量导出真实个人信息。项目内容影响对象使用自定义 REST API 的 WordPress 插件、主题或业务扩展漏洞类型未授权访问、水平越权、垂直越权常见入口/wp-json/插件命名空间/v1/...直接影响订单、邮箱、手机号、工单、内部配置等业务数据泄露风险边界若接口还支持写入操作,可能进一步影响业务数据完整性二、典型问题代码以下代码展示了一个不安全的 REST 路由。permission_callback 返回真值后,WordPress 不会再对请求者进行能力或对象级授权判断。<?php add_action('rest_api_init', function () { register_rest_route('acme/v1', '/orders', [ 'methods' => 'GET', 'callback' => 'acme_list_orders', 'permission_callback' => '__return_true' ]); }); function acme_list_orders(WP_REST_Request $request) { global $wpdb; return $wpdb->get_results( "SELECT id, customer_email, phone, total FROM {$wpdb->prefix}acme_orders LIMIT 100" ); }另一个常见问题是仅判断登录状态:'permission_callback' => function () { return is_user_logged_in(); }这种写法仍然存在垂直越权风险。普通订阅者、低权限员工账号或被盗用的前台账号,都可能访问本应只对客服或管理员开放的数据。三、授权验证方法验证前先确认测试域名、时间窗口、允许的请求速率和数据脱敏要求。推荐使用仅包含一条测试记录的预发布环境。验证目标是证明访问控制缺失,不是获取完整数据集。从插件源码、路由文档或站点接口清单中确认命名空间和路径。先发送无认证请求,记录 HTTP 状态码、响应字段和响应长度。再使用不同权限的测试账号分别验证管理员、客服、编辑、订阅者和普通访客。如果接口包含对象编号,只使用测试对象编号,并检查是否能读取其他测试账号创建的对象。将响应中的邮箱、手机号等字段脱敏保存,仅保留足以复现问题的证据。# 仅用于已授权的测试环境,避免使用真实数据编号 curl -sk -D /tmp/headers.txt \ -H 'Accept: application/json' \ ' \ -o /tmp/response.json # 查看状态码、响应类型和响应长度 awk 'BEGIN{IGNORECASE=1} /^HTTP\/|^Content-Type:|^Content-Length:/{print}' /tmp/headers.txt wc -c /tmp/response.json head -c 500 /tmp/response.json # 使用低权限测试账号的应用会话进行对比,Cookie 不要提交到公开工单 curl -sk -b 'wordpress_logged_in_TEST=REDACTED' \ -H 'Accept: application/json' \ ' \ -o /tmp/low_privilege.json 判断依据不能只看 HTTP 状态码。以下情况通常可以确认存在访问控制缺陷:未登录请求返回业务对象;普通账号能够读取管理端数据;更换对象编号后仍能读取其他主体数据;响应中出现数据库字段而不是统一的 401 或 403 错误。四、源码排查与快速定位对授权范围内的站点,可以先定位 REST 路由注册点,再检查权限回调和数据查询是否绑定当前用户。# 在插件目录中查找 REST 路由注册和高风险权限回调 rg -n --glob '*.php' \ 'register_rest_route|permission_callback|__return_true|is_user_logged_in|current_user_can' \ wp-content/plugins wp-content/themes # 查找可能直接返回敏感字段的查询 rg -n --glob '*.php' \ 'customer_email|phone|wpdb|SELECT .*orders|SELECT .*users' \ wp-content/plugins/acme-plugin # 通过 WP-CLI 查看已安装插件及版本,结果应与资产清单交叉核对 wp plugin list --status=active --fields=name,version,update --format=table 排查时重点关注四个位置:路由注册是否明确设置了 permission_callback,而不是依赖默认行为。权限回调是否使用了符合业务要求的能力,例如 manage_woocommerce 或自定义 capability。查询是否限制到当前用户、当前租户或当前工单所属部门,不能只依赖前端传入的对象编号。返回对象是否经过字段白名单处理,避免把数据库中的内部备注、令牌和系统字段一并序列化。五、修复示例修复应同时覆盖路由级授权、对象级授权和输出字段控制。下面示例要求调用者具备指定能力,并且只返回允许暴露的字段。<?php add_action('rest_api_init', function () { register_rest_route('acme/v1', '/orders', [ 'methods' => WP_REST_Server::READABLE, 'callback' => 'acme_list_orders', 'permission_callback' => function () { return current_user_can('acme_view_orders') ? true : new WP_Error( 'rest_forbidden', 'Insufficient permissions', ['status' => 403] ); }, 'args' => [ 'limit' => [ 'default' => 20, 'sanitize_callback' => 'absint', 'validate_callback' => function ($value) { return $value > 0 && $value <= 50; } ] ] ]); }); function acme_list_orders(WP_REST_Request $request) { global $wpdb; $limit = min(50, max(1, (int) $request['limit'])); $rows = $wpdb->get_results( $wpdb->prepare( "SELECT id, status, total, created_at FROM {$wpdb->prefix}acme_orders WHERE tenant_id = %d ORDER BY id DESC LIMIT %d", get_current_user_id(), $limit ), ARRAY_A ); return array_map(function ($row) { return [ 'id' => (int) $row['id'], 'status' => sanitize_key($row['status']), 'total' => (float) $row['total'], 'created_at' => $row['created_at'] ]; }, $rows ?: []); }实际项目中,tenant_id = get_current_user_id() 只是示例,不能机械复制。多租户系统应从服务端会话或用户所属组织计算租户范围,不能信任请求参数中的租户编号。若接口需要管理员跨用户查询,应单独建立明确的管理能力,并记录操作者、查询条件和结果数量。六、日志留痕与内网攻防链路分析这类漏洞经常作为内网入侵后的数据发现环节:攻击者获得低权限 WordPress 账号后,通过 REST API 扩大数据访问范围,再利用泄露的邮箱、内部 URL 或配置字段寻找其他系统入口。因此修复时不能只关闭单个接口,还要检查同一站点及相邻系统的权限边界。Nginx 或 Apache 访问日志中可重点检索 REST 路径、异常频率、不同账号访问相同对象以及大量 200 响应。示例:# Nginx 日志格式为:$remote_addr - $remote_user [$time_local] "$request" $status $body_bytes_sent awk '$7 ~ /\/wp-json\/acme\/v1\/orders/ {print}' /var/log/nginx/access.log \ | awk '{print $1, $4, $7, $9, $10}' | head -n 100 # 统计该接口按来源地址的请求量 awk '$7 ~ /\/wp-json\/acme\/v1\/orders/ {count[$1]++} END {for (ip in count) print count[ip], ip}' \ /var/log/nginx/access.log | sort -nr | head 同时检查 WordPress、WAF、反向代理和身份系统日志。重点字段包括请求时间、源地址、User-Agent、认证主体、HTTP 状态码、响应大小和对象编号。若发现低权限账号在短时间内连续访问大量对象,应先保全原始日志和账号审计记录,再执行会话吊销、密码轮换和令牌替换。七、风险与修复建议升级存在问题的插件或主题,并确认更新后路由权限回调仍然生效,不能只根据版本号判断修复完成。对所有自定义 REST、AJAX 和 GraphQL 接口进行统一权限审计,特别是返回用户、订单、文件和配置数据的接口。为能力模型建立最小权限矩阵,明确访客、订阅者、客服、编辑和管理员可访问的资源范围。在网关或 WAF 层对异常高频读取、单账号大量对象遍历和批量分页请求设置告警,但不要把 WAF 规则当作根本修复。对已暴露的数据进行影响评估,检查是否包含密码重置令牌、云访问密钥、数据库连接信息或内网地址,并按应急流程轮换相关凭据。将接口测试纳入 CI,至少覆盖未登录、低权限、合法权限和跨对象访问四组用例,并断言返回 401、403 或脱敏结果。八、验证修复是否生效修复完成后,应使用与初次验证相同的测试请求进行回归。未登录请求应返回 401 或 403;低权限账号应被拒绝;合法账号只能获取授权范围内的对象;更换对象编号、分页参数和租户标识不能突破数据边界。还应检查应用日志中是否记录了拒绝事件,以及错误响应是否泄露数据库表名、文件路径或堆栈信息。漏洞库记录的有效证据应包含:受影响组件和版本、接口路径、请求身份、最小化响应样本、权限差异、日志时间点、修复版本和回归结果。不要在报告中保存完整客户数据或可继续使用的认证材料。九、总结WordPress 自定义 REST API 的主要风险不在路由本身,而在权限判断没有落实到业务对象。漏洞验证应围绕认证边界、角色能力和对象归属逐层进行;修复则需要同时完善路由权限、服务端数据范围、输出字段和审计告警。对于已经暴露的站点,还应沿着账号、接口、配置和内网系统的链路检查凭据复用与横向影响,确保漏洞关闭后不存在遗留访问路径。 服务器、云资产与权限边界梳理示意 代码审计、调用链与关键函数定位示意
3小时前3小时 这类问题建议先区分“未登录可读”和“低权限用户越权读取”两种情况,修复点不完全一样。可以先对自定义路由做一轮复现和留痕: curl -i ' curl -i -H 'Authorization: Basic ...' \ ' 同时检查是否存在分页、批量查询或可控 ID 参数,避免只验证单个对象时漏掉: curl -s ' curl -s ' 路由注册时不要使用空的 permission_callback,也不要仅依赖前端隐藏接口。读取对象前应同时校验能力和对象归属,例如: add_action('rest_api_init', function () { register_rest_route('acme/v1', '/orders/(?P<id>\d+)', [ 'methods' => WP_REST_Server::READABLE, 'callback' => 'acme_get_order', 'permission_callback' => function (WP_REST_Request $request) { $id = (int) $request['id']; if (!is_user_logged_in()) { return new WP_Error( 'rest_forbidden', 'Authentication required', ['status' => 401] ); } if (current_user_can('manage_woocommerce')) { return true; } $order = wc_get_order($id); if (!$order || (int) $order->get_customer_id() !== get_current_user_id()) { return new WP_Error( 'rest_forbidden', 'You cannot access this resource', ['status' => 403] ); } return true; }, 'args' => [ 'id' => [ 'validate_callback' => function ($value) { return ctype_digit((string) $value) && (int) $value > 0; }, 'sanitize_callback' => 'absint', ], ], ]); }); 回调里还应再次按当前用户查询和裁剪字段,不能直接把完整的数据库对象或用户元数据序列化返回。尤其要检查邮箱、手机号、地址、内部备注、重置令牌等字段是否被无条件暴露。 function acme_get_order(WP_REST_Request $request) { $order = wc_get_order((int) $request['id']); if (!$order) { return new WP_Error('not_found', 'Order not found', ['status' => 404]); } $is_admin = current_user_can('manage_woocommerce'); $data = [ 'id' => $order->get_id(), 'status' => $order->get_status(), 'total' => $order->get_total(), ]; if ($is_admin) { $data['billing_email'] = $order->get_billing_email(); } return rest_ensure_response($data); } 留痕方面,重点记录请求时间、用户 ID、IP、路由、对象 ID、响应状态和失败原因,但不要把 Authorization、Cookie 或完整敏感响应写入日志。修复后建议用未登录、普通用户、其他用户、管理员四组身份回归测试,并检查 Nginx/CDN/WordPress 缓存是否缓存过旧的 200 响应;涉及个人数据时应同步清理缓存和历史日志中的敏感内容。
创建帐户或登录后发表意见