深入理解 `check_ajax_referer()` 函数的源码,它如何验证 AJAX 请求中的 `Nonce`?

咳咳,各位同学,早上好啊!今天咱们来聊聊一个在 WordPress 安全领域里至关重要的小家伙—— check_ajax_referer() 函数。别看它名字挺长,其实作用很简单,就是验证 AJAX 请求中的 Nonce,确保请求的合法性。

咱们先来理清几个概念:

  • AJAX (Asynchronous JavaScript and XML): 简单来说,就是不用刷新整个页面,也能从服务器获取数据并更新部分页面内容的技术。这玩意儿让用户体验提升了N个档次。

  • Nonce (Number used Once): 这是一个安全令牌,顾名思义,只能用一次。它的主要目的是防止 CSRF (Cross-Site Request Forgery) 攻击。你可以把它想象成一把临时的钥匙,开一次门就作废。

  • CSRF (Cross-Site Request Forgery): 跨站请求伪造。攻击者诱使用户在不知情的情况下,以用户的身份执行某些操作。比如说,用户登录了银行网站,攻击者通过某种手段让用户在不知情的情况下向攻击者的账户转账。

好了,概念清楚了,咱们开始深入 check_ajax_referer() 的源码。

check_ajax_referer() 的基本用法

首先,咱们来看看 check_ajax_referer() 函数的基本用法:

check_ajax_referer( $action, $query_arg, $die );
  • $action (string, required): 一个字符串,用于生成 Nonce 的“动作”。这个动作相当于给 Nonce 贴个标签,说明这个 Nonce 是干嘛用的。

  • $query_arg (string, optional): Nonce 字段的名称。默认值是 _wpnonce。也就是说,AJAX 请求中包含的 Nonce 的参数名。

  • $die (bool, optional): 如果验证失败,是否立即终止脚本执行。默认值是 true

举个例子:

假设我们有一个 AJAX 请求,用于更新用户的个人资料。我们可以这样使用 check_ajax_referer()

<?php
add_action( 'wp_ajax_update_profile', 'my_ajax_update_profile_callback' );
add_action( 'wp_ajax_nopriv_update_profile', 'my_ajax_update_profile_callback' ); // For non-logged-in users

function my_ajax_update_profile_callback() {
    // 验证 Nonce
    check_ajax_referer( 'update_profile_action', 'security' );

    // ... 更新用户资料的代码 ...

    wp_send_json_success( array( 'message' => 'Profile updated successfully!' ) );
}

在这个例子中:

  • update_profile_action 是 Nonce 的动作。
  • security 是 Nonce 字段的名称。这意味着 AJAX 请求中应该包含一个名为 security 的参数,其值就是 Nonce。

前端 JavaScript 代码大概是这样的:

jQuery.ajax({
    url: ajaxurl,
    type: 'POST',
    data: {
        action: 'update_profile',
        security: my_nonce_value, // 替换成实际的 Nonce 值
        // ... 其他数据 ...
    },
    success: function(response) {
        console.log(response);
    }
});

check_ajax_referer() 源码解析

现在,咱们来深入 check_ajax_referer() 的源码,看看它是如何验证 Nonce 的。

首先,我们找到 check_ajax_referer() 函数的定义,它位于 wp-includes/pluggable.php 文件中。

function check_ajax_referer( $action = -1, $query_arg = false, $die = true ) {
    $result = wp_verify_nonce( $_REQUEST[ $query_arg ], $action );
    if ( false === $result ) {
        if ( $die ) {
            wp_die(
                __( 'Are you sure you want to do this?' ),
                __( 'Security check failed' ),
                array( 'response' => 403 )
            );
        }
        return false;
    }

    return true;
}

这段代码的核心是 wp_verify_nonce() 函数。check_ajax_referer() 实际上只是 wp_verify_nonce() 的一个包装器,它从 $_REQUEST 数组中获取 Nonce 值,并将其传递给 wp_verify_nonce() 进行验证。

如果 wp_verify_nonce() 返回 false,表示验证失败。此时,如果 $die 参数为 true,则 check_ajax_referer() 会调用 wp_die() 函数,终止脚本执行,并显示一个错误信息。否则,check_ajax_referer() 会返回 false

wp_verify_nonce() 源码解析

接下来,我们深入 wp_verify_nonce() 函数,看看它是如何验证 Nonce 的。 wp_verify_nonce() 函数也位于 wp-includes/pluggable.php 文件中。

function wp_verify_nonce( $nonce, $action = -1 ) {
    $nonce = (string) $nonce;
    $action = (string) $action;

    $i = wp_nonce_tick();

    // Nonce generated 0-12 hours ago
    $expected = substr( wp_hash( $i . '|' . $action . '|' . get_current_user_id() . '|' . wp_get_session_token(), 'nonce' ), -12, 10 );
    if ( hash_equals( $expected, $nonce ) ) {
        return 1;
    }

    // Nonce generated 12-24 hours ago
    $expected = substr( wp_hash( ( $i - 1 ) . '|' . $action . '|' . get_current_user_id() . '|' . wp_get_session_token(), 'nonce' ), -12, 10 );
    if ( hash_equals( $expected, $nonce ) ) {
        return 2;
    }

    // Invalid nonce
    return false;
}

这段代码有点复杂,咱们一步一步来分析:

  1. 类型转换: 首先,将 $nonce$action 转换为字符串类型,确保后续操作的类型一致性。

  2. 获取时间片段: 调用 wp_nonce_tick() 函数获取一个时间片段。这个时间片段会定期更新,用于增加 Nonce 的安全性。

  3. 生成预期的 Nonce: 使用 wp_hash() 函数生成一个预期的 Nonce。wp_hash() 函数使用 MD5 算法对以下字符串进行哈希:

    • $i: 当前的时间片段。
    • $action: Nonce 的动作。
    • get_current_user_id(): 当前用户的 ID。如果用户未登录,则返回 0。
    • wp_get_session_token(): 一个与用户会话相关联的令牌。

    然后,从哈希值的末尾截取 12 个字符,再取前10个字符作为预期的 Nonce。

  4. 比较 Nonce: 使用 hash_equals() 函数将传入的 Nonce 与预期的 Nonce 进行比较。hash_equals() 函数是一个安全的字符串比较函数,可以防止时序攻击。 如果两者相等,则表示验证成功,返回 1。

  5. 验证上一个时间片段: 如果当前时间片段生成的Nonce验证失败,则会验证上一个时间片段生成的Nonce。目的是允许 Nonce 在时间片段切换时仍然有效。

  6. 验证失败: 如果传入的 Nonce 与当前时间片段和上一个时间片段生成的预期的 Nonce 都不匹配,则表示验证失败,返回 false

wp_nonce_tick() 源码解析

咱们再来看看 wp_nonce_tick() 函数,它位于 wp-includes/pluggable.php 文件中。

function wp_nonce_tick() {
    /**
     * Filters the lifespan of nonces.
     *
     * @since 4.5.0
     *
     * @param int $lifespan Nonce lifespan in seconds. Default 12 hours.
     */
    $lifespan = apply_filters( 'nonce_life', DAY_IN_SECONDS / 2 );

    return ceil( time() / ( $lifespan / 2 ) );
}

这段代码也很简单:

  1. 获取 Nonce 的生命周期: 通过 apply_filters() 函数获取 Nonce 的生命周期。默认情况下,Nonce 的生命周期是 12 小时。

  2. 计算时间片段: 将当前时间除以 Nonce 生命周期的一半,然后向上取整,得到时间片段。

wp_hash() 源码解析

最后,我们来看看 wp_hash() 函数,它位于 wp-includes/pluggable.php 文件中。

function wp_hash( $data, $scheme = 'auth' ) {
    static $required = true;
    if ( $required ) {
        require_once ABSPATH . WPINC . '/class-phpass.php';
        $required = false;
    }

    global $wp_hasher;

    if ( empty( $wp_hasher ) ) {
        $wp_hasher = new PasswordHash( 8, true );
    }

    return $wp_hasher->HashPassword( $data );
}

这段代码使用了 PasswordHash 类,位于 wp-includes/class-phpass.php 中,用于进行哈希运算。

总结一下 check_ajax_referer() 的工作流程:

  1. 前端 JavaScript 代码使用 wp_create_nonce() 函数生成一个 Nonce,并将其包含在 AJAX 请求中。
  2. 后端 PHP 代码使用 check_ajax_referer() 函数验证 AJAX 请求中的 Nonce。
  3. check_ajax_referer() 函数实际上调用 wp_verify_nonce() 函数进行验证。
  4. wp_verify_nonce() 函数首先获取当前的时间片段。
  5. 然后,wp_verify_nonce() 函数使用 wp_hash() 函数生成一个预期的 Nonce。
  6. 最后,wp_verify_nonce() 函数使用 hash_equals() 函数将传入的 Nonce 与预期的 Nonce 进行比较。
  7. 如果两者相等,则表示验证成功。否则,表示验证失败。

表格总结

为了更好地理解,我把这些函数的核心逻辑用表格整理一下:

函数名 作用 核心逻辑
check_ajax_referer() 验证 AJAX 请求中的 Nonce。 $_REQUEST 数组中获取 Nonce 值,并将其传递给 wp_verify_nonce() 函数进行验证。如果验证失败,则终止脚本执行或返回 false
wp_verify_nonce() 验证 Nonce 的有效性。 1. 获取当前时间片段。2. 使用 wp_hash() 函数生成一个预期的 Nonce。3. 使用 hash_equals() 函数将传入的 Nonce 与预期的 Nonce 进行比较。如果两者相等,则表示验证成功。
wp_nonce_tick() 获取 Nonce 的时间片段。 1. 获取 Nonce 的生命周期。2. 将当前时间除以 Nonce 生命周期的一半,然后向上取整,得到时间片段。
wp_hash() 使用 MD5 算法对字符串进行哈希。 使用 PasswordHash 类进行哈希运算。

安全性考虑

虽然 check_ajax_referer() 函数可以有效地防止 CSRF 攻击,但仍然有一些安全性问题需要注意:

  • Nonce 的生命周期: Nonce 的生命周期越长,被破解的风险就越高。因此,应该尽量缩短 Nonce 的生命周期。WordPress 默认的 Nonce 生命周期是 12 小时,你可以使用 nonce_life 过滤器来修改它。

  • HTTPS: 使用 HTTPS 可以有效地防止中间人攻击,确保 Nonce 在传输过程中不被窃取。

  • 输入验证: 即使 Nonce 验证成功,也应该对用户输入进行验证,防止 XSS (Cross-Site Scripting) 攻击。

总结

check_ajax_referer() 函数是 WordPress 中一个非常重要的安全函数,它可以有效地防止 CSRF 攻击。通过深入理解 check_ajax_referer() 的源码,我们可以更好地保护我们的 WordPress 网站的安全。

好了,今天的讲座就到这里。希望大家有所收获! 下课!

发表回复

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