WordPress Nonce 机制深度解析:一场表单令牌的华丽冒险
大家好,我是老码,今天咱们来聊聊 WordPress 里一个默默守护着我们网站安全的英雄——wp_nonce
机制。 别看它名字拗口,实际上它的作用超级实在:防止 CSRF 攻击,保护你的表单数据不被坏人篡改。 咱们今天就扒开它的源码,看看这个小家伙是怎么工作的,以及如何在实际开发中灵活运用它。
什么是 Nonce,它为什么这么重要?
想象一下,你正在 WordPress 后台愉快地写文章,突然,一个恶意网站给你发来一个链接。 你手贱点了进去,结果你的文章被自动发布了,还附带了一段莫名其妙的广告! 这就是 CSRF (Cross-Site Request Forgery) 攻击的典型场景。
CSRF 攻击的原理是,攻击者伪造你的请求,冒充你的身份去执行一些操作。 因为浏览器会自动携带你的 Cookie,服务器就误以为是你在操作,从而执行了攻击者的指令。
为了防止这种攻击,我们需要一种机制来验证请求的真实性,确保请求确实是由用户主动发起的,而不是被攻击者伪造的。 这就是 Nonce 的作用。
Nonce,全称 Number used once,顾名思义,就是一个一次性使用的数字。 它的核心思想是:
- 生成 Nonce: 在表单中嵌入一个随机生成的 Nonce 值。
- 验证 Nonce: 当表单提交时,服务器验证 Nonce 值是否有效。
- 失效 Nonce: 一旦 Nonce 被使用过,或者过期,就失效。
这样,攻击者即使能够截获你的请求,也无法伪造有效的 Nonce 值,从而阻止了 CSRF 攻击。
wp_nonce
源码剖析:从生成到验证
WordPress 提供了强大的 wp_nonce
函数来简化 Nonce 的生成和验证。 让我们一起深入源码,看看它是如何实现的。
1. wp_create_nonce( $action )
:Nonce 的诞生
wp_create_nonce()
函数负责生成 Nonce 值。 它的源码位于 wp-includes/pluggable.php
文件中。 简化后的代码如下:
function wp_create_nonce( $action = -1 ) {
$user = wp_get_current_user();
$uid = (int) $user->ID;
$token = wp_get_session_token();
$i = wp_nonce_tick();
return substr( wp_hash( $i . '|' . $action . '|' . $uid . '|' . $token, 'nonce' ), -12, 10 );
}
让我们一步一步分析:
-
$action
: 一个字符串,用于标识 Nonce 的用途。 比如,你可以使用'delete_post'
作为删除文章的 Nonce 的 action。 不同的 action 会生成不同的 Nonce 值。 -
$user = wp_get_current_user();
: 获取当前用户对象。 -
$uid = (int) $user->ID;
: 获取当前用户的 ID。 -
$token = wp_get_session_token();
: 获取用户的 session token。 这个 token 用于进一步增加 Nonce 的安全性,防止 session hijacking 攻击。 -
$i = wp_nonce_tick();
: 获取一个时间戳,用于控制 Nonce 的有效期。 -
wp_hash( $i . '|' . $action . '|' . $uid . '|' . $token, 'nonce' )
: 使用wp_hash()
函数对以上信息进行哈希,生成一个唯一的字符串。wp_hash()
函数内部使用了 WordPress 的盐 (salt) 来增加哈希的安全性。 这里的 ‘nonce’ 是一个哈希算法标识符,用于区分不同的哈希用途。 -
substr( ..., -12, 10 )
: 截取哈希字符串的后 12 位,然后取前 10 位作为最终的 Nonce 值。 这主要是为了缩短 Nonce 的长度,方便在 URL 中传递。
wp_nonce_tick()
函数的奥秘
wp_nonce_tick()
函数用于获取一个时间戳,这个时间戳会随着时间推移而变化,从而控制 Nonce 的有效期。 它的源码如下:
function wp_nonce_tick() {
/**
* Filters the lifespan of nonces.
*
* @since 4.5.0
*
* @param int $lifespan Lifespan in seconds.
*/
$nonce_life = apply_filters( 'nonce_life', DAY_IN_SECONDS );
return ceil( time() / ( $nonce_life / 2 ) );
}
DAY_IN_SECONDS
: WordPress 常量,表示一天有多少秒 (86400)。$nonce_life = apply_filters( 'nonce_life', DAY_IN_SECONDS );
: 使用apply_filters()
函数允许开发者自定义 Nonce 的有效期。 默认情况下,Nonce 的有效期是一天。return ceil( time() / ( $nonce_life / 2 ) );
: 将当前时间戳除以nonce_life / 2
,然后向上取整。 这样做的目的是将一天的时间分成两个时间片,Nonce 会在每个时间片内有效。 也就是说,Nonce 的实际有效期是nonce_life / 2
,默认是 12 小时。
总结一下,wp_create_nonce()
函数的流程如下:
步骤 | 描述 |
---|---|
1 | 获取当前用户 ID、session token 和一个时间戳。 |
2 | 将这些信息与 action 一起进行哈希。 |
3 | 截取哈希字符串的一部分作为最终的 Nonce 值。 |
2. wp_nonce_field( $action, $name, $referer, $echo )
:将 Nonce 嵌入到表单中
wp_nonce_field()
函数用于生成一个隐藏的表单字段,并将 Nonce 值嵌入到该字段中。 它的源码如下:
function wp_nonce_field( $action = -1, $name = '_wpnonce', $referer = true, $echo = true ) {
$name_esc = esc_attr( $name );
$nonce_field = '<input type="hidden" id="' . $name_esc . '" name="' . $name_esc . '" value="' . wp_create_nonce( $action ) . '" />';
if ( $referer ) {
$nonce_field .= wp_referer_field( false );
}
if ( $echo ) {
echo $nonce_field;
} else {
return $nonce_field;
}
}
$action
: Nonce 的 action。$name
: 表单字段的名称。 默认是'_wpnonce'
。$referer
: 是否包含 referer 字段。 如果为 true,则会添加一个隐藏的 referer 字段,用于验证请求的来源。$echo
: 是否直接输出 HTML 代码。 如果为 true,则直接输出;否则,返回 HTML 代码。
wp_nonce_field()
函数主要做了两件事:
- 调用
wp_create_nonce()
函数生成 Nonce 值。 - 生成一个隐藏的表单字段,并将 Nonce 值赋给该字段。
wp_referer_field( $echo )
函数的作用
wp_referer_field()
函数用于生成一个隐藏的 referer 字段。 它的源码如下:
function wp_referer_field( $echo = true ) {
$referer_field = '<input type="hidden" name="_wp_http_referer" value="' . esc_attr( wp_unslash( $_SERVER['REQUEST_URI'] ) ) . '" />';
if ( $echo ) {
echo $referer_field;
} else {
return $referer_field;
}
}
wp_referer_field()
函数的作用是将当前请求的 URI (Uniform Resource Identifier) 存储在一个隐藏的表单字段中。 在验证请求时,可以检查 referer 字段的值是否与预期的值相符,从而进一步提高安全性。 但是,referer 字段可能会被浏览器篡改或禁用,因此不能完全依赖它来防止 CSRF 攻击。
3. wp_verify_nonce( $nonce, $action )
:验证 Nonce 的真伪
wp_verify_nonce()
函数负责验证 Nonce 值的有效性。 它的源码如下:
function wp_verify_nonce( $nonce, $action = -1 ) {
$nonce = (string) $nonce;
$user = wp_get_current_user();
$uid = (int) $user->ID;
$token = wp_get_session_token();
if ( empty( $nonce ) ) {
return false;
}
$i = wp_nonce_tick();
// Nonce generated 0-12 hours ago
$expected = substr( wp_hash( $i . '|' . $action . '|' . $uid . '|' . $token, 'nonce' ), -12, 10 );
if ( hash_equals( $expected, $nonce ) ) {
return 1;
}
// Nonce generated 12-24 hours ago
$expected = substr( wp_hash( ( $i - 1 ) . '|' . $action . '|' . $uid . '|' . $token, 'nonce' ), -12, 10 );
if ( hash_equals( $expected, $nonce ) ) {
return 2;
}
/**
* Fires when a nonce fails verification.
*
* @since 4.7.0
*
* @param string|int $nonce The nonce attempted to be verified.
* @param string|int $action The action that was used.
* @param WP_User $user The user object.
*/
do_action( 'wp_nonce_failed', $nonce, $action, $user );
return false;
}
wp_verify_nonce()
函数的流程如下:
- 获取当前用户 ID、session token 和时间戳。
- 计算当前时间片对应的 Nonce 值。
- 将计算出的 Nonce 值与传入的 Nonce 值进行比较。 这里使用了
hash_equals()
函数进行比较,以防止 timing attack。 - 如果 Nonce 值匹配,则验证通过,返回 1。
- 如果 Nonce 值不匹配,则计算上一个时间片对应的 Nonce 值。
- 将计算出的 Nonce 值与传入的 Nonce 值进行比较。
- 如果 Nonce 值匹配,则验证通过,返回 2。
- 如果 Nonce 值仍然不匹配,则验证失败,返回 false。
之所以要验证两个时间片的 Nonce 值,是因为 Nonce 的有效期是 12 小时,而 wp_nonce_tick()
函数会将一天分成两个时间片。 这样,即使 Nonce 已经过了当前时间片,只要还在上一个时间片内,仍然可以验证通过。
4. check_admin_referer( $action, $query_arg )
和 check_ajax_referer( $action, $query_arg, $die )
:便捷的验证函数
WordPress 还提供了两个便捷的 Nonce 验证函数:check_admin_referer()
和 check_ajax_referer()
。 这两个函数实际上是对 wp_verify_nonce()
函数的封装,专门用于验证后台和 AJAX 请求的 Nonce 值。
check_admin_referer()
函数的源码如下:
function check_admin_referer( $action = -1, $query_arg = '_wpnonce' ) {
if ( ! isset( $_REQUEST[ $query_arg ] ) ) {
wp_nonce_ays( $action );
die();
}
$result = wp_verify_nonce( $_REQUEST[ $query_arg ], $action );
if ( ! $result ) {
wp_nonce_ays( $action );
die();
}
return true;
}
check_admin_referer()
函数的流程如下:
- 检查请求中是否包含 Nonce 值。 Nonce 值通常通过
$_REQUEST
数组传递。 - 如果请求中不包含 Nonce 值,则调用
wp_nonce_ays()
函数显示一个确认页面,并终止脚本执行。 - 调用
wp_verify_nonce()
函数验证 Nonce 值的有效性。 - 如果 Nonce 值无效,则调用
wp_nonce_ays()
函数显示一个确认页面,并终止脚本执行。 - 如果 Nonce 值有效,则返回 true。
check_ajax_referer()
函数的源码如下:
function check_ajax_referer( $action = -1, $query_arg = false, $die = true ) {
if ( false === $query_arg ) {
$query_arg = '_ajax_nonce';
}
$result = wp_verify_nonce( $_REQUEST[ $query_arg ], $action );
if ( false === $result && $die ) {
wp_die( __( 'Security check failed.' ), 403 );
}
return $result;
}
check_ajax_referer()
函数与 check_admin_referer()
函数类似,但是它不会显示确认页面,而是直接输出一个错误信息并终止脚本执行。
wp_nonce
的使用示例:保护你的表单
现在,让我们通过一个简单的例子来演示如何使用 wp_nonce
来保护你的表单。
假设你正在开发一个自定义的插件,用于添加自定义的文章元数据。 你需要在插件的设置页面中添加一个表单,让用户可以设置元数据的默认值。
首先,在你的插件的设置页面中,添加以下代码:
<form method="post" action="">
<?php wp_nonce_field( 'my_plugin_settings', 'my_plugin_nonce' ); ?>
<label for="my_plugin_default_value">Default Value:</label>
<input type="text" id="my_plugin_default_value" name="my_plugin_default_value" value="<?php echo esc_attr( get_option( 'my_plugin_default_value' ) ); ?>" />
<input type="submit" value="Save Settings" />
</form>
这段代码会生成一个包含 Nonce 值的隐藏表单字段。 wp_nonce_field()
函数的第一个参数是 action,这里我们使用 'my_plugin_settings'
作为 action。 第二个参数是表单字段的名称,这里我们使用 'my_plugin_nonce'
。
接下来,在处理表单提交的代码中,添加以下代码:
if ( isset( $_POST['my_plugin_default_value'] ) ) {
if ( ! isset( $_POST['my_plugin_nonce'] ) || ! wp_verify_nonce( $_POST['my_plugin_nonce'], 'my_plugin_settings' ) ) {
wp_die( 'Security check failed.' );
}
$default_value = sanitize_text_field( $_POST['my_plugin_default_value'] );
update_option( 'my_plugin_default_value', $default_value );
echo '<div class="updated"><p>Settings saved.</p></div>';
}
这段代码会验证 Nonce 值的有效性。 如果 Nonce 值不存在,或者无效,则会显示一个错误信息并终止脚本执行。 如果 Nonce 值有效,则会更新插件的设置。
总结一下,使用 wp_nonce
保护表单的步骤如下:
- 在表单中添加一个隐藏的 Nonce 字段。 使用
wp_nonce_field()
函数可以方便地生成这个字段。 - 在处理表单提交的代码中,验证 Nonce 值的有效性。 使用
wp_verify_nonce()
函数可以验证 Nonce 值。
wp_nonce
的最佳实践
- 选择合适的 action。 Action 应该能够唯一标识 Nonce 的用途。 比如,可以使用
'delete_post_' . $post_id
作为删除特定文章的 Nonce 的 action。 - 不要在 GET 请求中使用 Nonce。 Nonce 应该只用于 POST 请求,因为 GET 请求可能会被缓存或分享。
- 定期更新 Nonce。 虽然 Nonce 的默认有效期是 12 小时,但是你可以根据你的需求,缩短 Nonce 的有效期。
- 使用
hash_equals()
函数进行 Nonce 值的比较。hash_equals()
函数可以防止 timing attack。 - 不要依赖 referer 字段来防止 CSRF 攻击。 Referer 字段可能会被浏览器篡改或禁用。
总结
wp_nonce
机制是 WordPress 中一个重要的安全特性,它可以有效地防止 CSRF 攻击。 通过深入了解 wp_nonce
的源码,我们可以更好地理解它的工作原理,并在实际开发中灵活运用它。 希望今天的讲座能够帮助大家更好地保护自己的 WordPress 网站。
好了,今天的分享就到这里,大家有什么问题欢迎提问。 记住,安全无小事,保护好你的网站,才能睡个好觉!