深入分析 `wp_nonce` 机制的源码,它是如何实现表单令牌(Token)的?

WordPress Nonce 机制深度解析:一场表单令牌的华丽冒险

大家好,我是老码,今天咱们来聊聊 WordPress 里一个默默守护着我们网站安全的英雄——wp_nonce 机制。 别看它名字拗口,实际上它的作用超级实在:防止 CSRF 攻击,保护你的表单数据不被坏人篡改。 咱们今天就扒开它的源码,看看这个小家伙是怎么工作的,以及如何在实际开发中灵活运用它。

什么是 Nonce,它为什么这么重要?

想象一下,你正在 WordPress 后台愉快地写文章,突然,一个恶意网站给你发来一个链接。 你手贱点了进去,结果你的文章被自动发布了,还附带了一段莫名其妙的广告! 这就是 CSRF (Cross-Site Request Forgery) 攻击的典型场景。

CSRF 攻击的原理是,攻击者伪造你的请求,冒充你的身份去执行一些操作。 因为浏览器会自动携带你的 Cookie,服务器就误以为是你在操作,从而执行了攻击者的指令。

为了防止这种攻击,我们需要一种机制来验证请求的真实性,确保请求确实是由用户主动发起的,而不是被攻击者伪造的。 这就是 Nonce 的作用。

Nonce,全称 Number used once,顾名思义,就是一个一次性使用的数字。 它的核心思想是:

  1. 生成 Nonce: 在表单中嵌入一个随机生成的 Nonce 值。
  2. 验证 Nonce: 当表单提交时,服务器验证 Nonce 值是否有效。
  3. 失效 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() 函数主要做了两件事:

  1. 调用 wp_create_nonce() 函数生成 Nonce 值。
  2. 生成一个隐藏的表单字段,并将 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() 函数的流程如下:

  1. 获取当前用户 ID、session token 和时间戳。
  2. 计算当前时间片对应的 Nonce 值。
  3. 将计算出的 Nonce 值与传入的 Nonce 值进行比较。 这里使用了 hash_equals() 函数进行比较,以防止 timing attack。
  4. 如果 Nonce 值匹配,则验证通过,返回 1。
  5. 如果 Nonce 值不匹配,则计算上一个时间片对应的 Nonce 值。
  6. 将计算出的 Nonce 值与传入的 Nonce 值进行比较。
  7. 如果 Nonce 值匹配,则验证通过,返回 2。
  8. 如果 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() 函数的流程如下:

  1. 检查请求中是否包含 Nonce 值。 Nonce 值通常通过 $_REQUEST 数组传递。
  2. 如果请求中不包含 Nonce 值,则调用 wp_nonce_ays() 函数显示一个确认页面,并终止脚本执行。
  3. 调用 wp_verify_nonce() 函数验证 Nonce 值的有效性。
  4. 如果 Nonce 值无效,则调用 wp_nonce_ays() 函数显示一个确认页面,并终止脚本执行。
  5. 如果 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 保护表单的步骤如下:

  1. 在表单中添加一个隐藏的 Nonce 字段。 使用 wp_nonce_field() 函数可以方便地生成这个字段。
  2. 在处理表单提交的代码中,验证 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 网站。

好了,今天的分享就到这里,大家有什么问题欢迎提问。 记住,安全无小事,保护好你的网站,才能睡个好觉!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注