ARTICLE · 1069273
WordPress插件Forminator未认证短代码执行漏洞(poc已公开)CVE-2026-92229

一、漏洞描述
Forminator Forms 是一个WordPress的表单插件,联系表单、支付收款、测验和投票等,插件声明需要 WordPress 6.4 起。近期被爆出存在未授权任意段代码执行漏洞,漏洞产生的入口挂在 WordPress 的 admin-ajax.php 上,注册钩子时同时挂了 nopriv 分支,不检查有没有登录。
受影响区间是 1.57.2 及之前所有版本,1.57.3 修掉,发布时间 9 月 17 日。
站点得开着测验模块才踩到这条分支,只摆一个联系表单的不在命中范围内。
二、漏洞原理
WordPress 的 AJAX 钩子留了两套。
WordPress 给 admin-ajax.php 的请求留了两套钩子,wp_ajax_ 走登录用户,wp_ajax_nopriv_ 对所有人开放,Forminator 两个都挂上了。
表单提交那一段卡着 nonce 校验,看着像把外人挡在外面。
插件自己留了一个取 nonce 的接口,同样挂在 nopriv 上,任何人 POST 一次带 action 与 form_id 的请求就能换到有效值。
// library/abstracts/abstract-class-front-action.php 1.57.2
139 add_action( 'wp_ajax_forminator_get_nonce', array( $this, 'get_nonce' ) );
140 add_action( 'wp_ajax_nopriv_forminator_get_nonce', array( $this, 'get_nonce' ) );
1217 public function get_nonce() {
1218 $form_id = filter_input( INPUT_POST, 'form_id', FILTER_VALIDATE_INT );
1219 wp_send_json_success( wp_create_nonce( 'forminator_submit_form' . $form_id ) );
1220 } // 返回的正是 54 行那次校验要用的值
拿到这个 nonce,再拼一个 action 为 forminator_submit_form_quizzes 的 POST 请求就够了。
测验答案能从页面 HTML 里抠出来。预览接口同走一条路,action 换成 forminator_submit_preview_form_quizzes,还不用落库。
再看 current_url 这条线。
所有 POST 字段进来只过一遍通用的递归清洗,current_url 没有专属处理。
// library/abstracts/abstract-class-front-action.php 1.57.2
973 $post_data = Forminator_Core::sanitize_array( $_POST );
// library/modules/quizzes/front/front-action.php 1.57.2
60 if ( empty( self::$prepared_data['current_url'] ) ) {
61 self::$prepared_data['current_url'] = forminator_get_current_url();
62 }
岔子出在测验结果的渲染。
社交分享那一段把 current_url 塞进了 data-url 属性,外面套的是 WordPress 自带的 esc_url。
// library/modules/quizzes/front/front-action.php 1.57.2
835 <ul class="forminator-social--icons"
836 data-message="<?php echo esc_textarea( $result_message ); ?>"
837 data-url="<?php echo esc_url( self::$prepared_data['current_url'] ); ?>">
869 $knowledge_result_html = ob_get_clean();
870 $knowledge_result_html = do_shortcode( $knowledge_result_html );
esc_url 管方括号,转的是主机名之后那一段。
本地按 WordPress 6.4 的 esc_url 实现复算了一遍,位移一个字符就翻样。
// 本地复算,未在本站点 WordPress 上执行
https://target/[caption]x[/caption] -> https://target/%5Bcaption%5Dx%5B/caption%5D
https://target[caption]x[/caption] -> https://target[caption]x[/caption%5D
https://target[shortcode] -> https://target[shortcode]
载荷往前挪,落在 parse_url 认作主机名的地方,方括号就整段留着。
真正要命的还在下面一行。
第 870 行把整段结果 HTML 丢进了 do_shortcode。
这个函数按方括号标记的写法去找站点注册过的短代码,跑一遍回调,拿返回值替换原文。
分享属性里那点内容,就此从一段装饰用的地址变成了会执行的东西。
能跑到什么程度,看站点还装了哪些插件与主题。
1.57.3 改了两处。
// library/modules/quizzes/front/front-action.php 1.57.3
<?php echo wp_kses_post( wpautop( do_shortcode( $text ), true ) ); ?>
// 870 行那次对整段缓冲的 do_shortcode 整行删除
// library/abstracts/abstract-class-front-action.php 1.57.3
$post_data = self::sanitize_post_current_url( $post_data );
protected static function sanitize_post_current_url( $post_data ) {
if ( empty( $post_data['current_url'] ) ) {
return $post_data;
}
$current_url = esc_url_raw( wp_unslash( $post_data['current_url'] ) );
$current_url = strip_shortcodes( $current_url );
$post_data['current_url'] = $current_url;
return $post_data;
}
短代码的执行范围从整段 HTML 缩到结论文字那一块。
current_url 在进业务之前先做一次地址归一,再用 strip_shortcodes 把短代码捞掉。
官方在注释里交代了为何没直接删方括号,查询串里的数组写法与 IPv6 字面地址都用得上。
三、修复建议
升到 1.57.3 或更高