Files
laoyang 704a9346e1 3.2.12 紧急修复:点「测试邮件」引发无限递归、内存耗尽
3.2.11 引入的事故。线上表现:
  Allowed memory size of 268435456 bytes exhausted
  in .../wp-includes/class-wp-block-parser.php on line 91
前台首页、后台登录页都正常,只有点那个按钮才炸。

根因不在区块解析器 —— 那只是递归链最后执行到的一行,看它会完全跑偏。

validate_options() 是 register_setting() 注册的 sanitize_callback,
WP 把它挂在 `sanitize_option_kratos` 过滤器上(wp-includes/option.php:3073)。
3.2.11 为了让"刚改的 SMTP 配置立刻生效",在这个回调里调了
update_option('kratos', $clean)。而 update_option() 内部**会再跑一遍**
同名过滤器 ⇒ 又调回 validate_options ⇒ 再 update_option ⇒ 无限递归。
每层都在复制 $clean 派生的大数组,直到吃满 256M。

本地实测确认递归存在:回调被调用 3 次、嵌套深度 2。

改法:不写库,改用一次性覆盖层。
- 分支里把 $clean 放进 $GLOBALS['__yang_options_override']
- kratos_option() 优先读它,取不到再读库
- 效果等价(点测试用的就是刚填的值),但不触发任何过滤器,不可能递归
- 覆盖层是本请求内的全局,不落库、不跨请求残留

assertions: run_tests 104 → 110
- 分支里**没有** update_option(这条是红线,直接对应本次事故)
- 分支里有覆盖层
- 覆盖层真生效:kratos_option 优先返回新值
- 覆盖层未命中的键仍读库(不能把别的配置一起抹掉)
- 撤掉覆盖层回到库值(不跨请求残留)
- 桩与产品代码一致:直接读 autoload.php 确认它也认覆盖层
  (桩比产品温和是这次漏检的根源之一 —— 桩里 update_option 不挂
   sanitize 过滤器,所以注入递归 bug 后 harness 根本不崩)

顺带修了两个测试基础设施问题:
1. wp_stub.php 的 kratos_option 与真实实现语义不一致。已同步,
   并加断言防止两边再次漂移(否则"测的是桩、不是产品代码")。
2. falsify 的 rebuild() 没把被 file_get_contents 读的 autoload.php
   复制进副本 ⇒ 新断言读不到文件 ⇒ 基线假红。

falsify 24 → 25:注入「分支里调 update_option」能抓住(靠静态红线,
不靠跑崩)、注入「去掉覆盖层」能抓住、注入「退回裸 wp_mail」能抓住。

全量 14 组全绿;run_tests 110 / run_tests2 154 / falsify 25。
2026-10-23 18:49:06 +08:00

130 lines
4.4 KiB
PHP

<?php
/**
* Options Framework
*
* @package Options Framework
* @author Devin Price <devin@wptheming.com>
* @license GPL-2.0+
* @link http://wptheming.com
* @copyright 2010-2014 WP Theming
*
* @wordpress-plugin
* Plugin Name: Options Framework
* Plugin URI: http://wptheming.com
* Description: A framework for building theme options.
* Version: 1.9.1
* Author: Devin Price
* Author URI: http://wptheming.com
* License: GPL-2.0+
* License URI: http://www.gnu.org/licenses/gpl-2.0.txt
* Text Domain: optionsframework
* Domain Path: /languages
*/
// If this file is called directly, abort.
if (!defined('WPINC')) {
die;
}
// Don't load if optionsframework_init is already defined
if (is_admin() && !function_exists('optionsframework_init')):
function optionsframework_init()
{
// If user can't edit theme options, exit
if (!current_user_can('edit_theme_options')) {
return;
}
require get_template_directory() . '/inc/theme-options.php';
// Loads the required Options Framework classes.
require plugin_dir_path(__FILE__) . 'includes/class-options-framework.php';
require plugin_dir_path(__FILE__) . 'includes/class-options-framework-admin.php';
require plugin_dir_path(__FILE__) . 'includes/class-options-interface.php';
require plugin_dir_path(__FILE__) . 'includes/class-options-media-uploader.php';
require plugin_dir_path(__FILE__) . 'includes/class-options-sanitization.php';
// Instantiate the options page.
$options_framework_admin = new Options_Framework_Admin;
$options_framework_admin->init();
// Instantiate the media uploader class
$options_framework_media_uploader = new Options_Framework_Media_Uploader;
$options_framework_media_uploader->init();
}
add_action('init', 'optionsframework_init', 20);
endif;
/**
* Helper function to return the theme option value.
* If no value has been saved, it returns $default.
* Needed because options are saved as serialized strings.
*
* Not in a class to support backwards compatibility in themes.
*/
if (!function_exists('kratos_option')):
function kratos_option($name, $default = false)
{
/* 一次性覆盖层:只影响当前这次请求。
*
* 用在「后台点测试邮件」这一种场景:那时用户刚在表单里改了
* SMTP 主机/账号/密码,但新值还没写进库(register_setting 的
* sanitize 回调返回值之后才落库)。直接读库会拿到上一次的配置,
* 于是"明明填对了却发不出去"。
*
* 为什么不用 update_option() 把新值先写进去:那个函数会再触发
* 一次 sanitize_option_kratos 过滤器,而写入逻辑本身就在那个
* 回调里 ⇒ 无限递归吃爆内存(3.2.11 的事故)。
*
* 覆盖层只在本请求内有效、不落库、不触发任何过滤器,
* 所以安全。其它场景这个全局变量根本不存在,行为与原来一致。
*/
if (isset($GLOBALS['__yang_options_override'])
&& is_array($GLOBALS['__yang_options_override'])
&& array_key_exists($name, $GLOBALS['__yang_options_override'])) {
return $GLOBALS['__yang_options_override'][$name];
}
$option_name = 'kratos';
// Get option settings from database
$options = get_option($option_name);
// Return specific option
if (isset($options[$name])) {
return $options[$name];
}
return $default;
}
endif;
add_action('admin_init', 'optionscheck_change_santiziation', 100);
function optionscheck_change_santiziation()
{
remove_filter('of_sanitize_textarea', 'of_sanitize_textarea');
add_filter('of_sanitize_textarea', 'custom_sanitize_textarea');
}
function custom_sanitize_textarea($input)
{
global $allowedposttags;
$custom_allowedtags["embed"] = array(
"src" => array(),
"type" => array(),
"allowfullscreen" => array(),
"allowscriptaccess" => array(),
"height" => array(),
"width" => array(),
);
$custom_allowedtags["script"] = array("type" => array(), "src" => array());
$custom_allowedtags = array_merge($custom_allowedtags, $allowedposttags);
$output = wp_kses($input, $custom_allowedtags);
return $output;
}