深入理解 WordPress `wp_verify_nonce()` 函数的源码:如何验证 `Nonce` 的有效期和合法性。

各位观众老爷,大家好!我是你们的老朋友,今天咱们来聊聊 WordPress 里一个非常重要,但又经常被大家忽略的小东西:wp_verify_nonce() 函数。 别看它名字平平无奇,但它可是 WordPress 安全体系中不可或缺的一环,负责验证 Nonce 的有效性和合法性,防止各种 CSRF 攻击。 今天咱们就来扒一扒它的源码,看看它到底是怎么工作的,以及我们又该如何在实际开发中正确使用它。

啥是 Nonce? 先来个小科普

在深入 wp_verify_nonce() 之前,我们先来搞清楚 Nonce 到底是个啥玩意儿。 Nonce,全称是 "Number used once",顾名思义,就是一个只使用一次的数字。 在 WordPress 中,Nonce 主要用于防止跨站请求伪造 (CSRF) 攻击。

简单来说,CSRF 攻击就是攻击者伪装成你的身份,偷偷地执行一些你不希望执行的操作。 比如,攻击者可能在你不知情的情况下,发布一篇博客文章,或者修改你的账户信息。

Nonce 的作用就是给每个敏感的操作加上一个 "通行证",只有拥有这个通行证的人,才能执行这个操作。 这个通行证就是 Nonce,它是根据用户会话、操作名称和时间戳等信息生成的,并且每次使用都会不一样。

wp_verify_nonce() 源码大揭秘

好了,有了 Nonce 的概念,我们就可以开始研究 wp_verify_nonce() 的源码了。 这个函数位于 wp-includes/pluggable.php 文件中。

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

    $uid = (int) get_current_user_id();
    $token = wp_get_session_token();

    if ( is_numeric( $action ) ) {
        $action = (int) $action;
    } else {
        $action = wp_nonce_tick() . $action;
    }

    $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 verification fails.
     *
     * @since 4.4.0
     *
     * @param string|int $nonce  The nonce that was attempted to be verified.
     * @param string|int $action The action name.
     */
    do_action( 'wp_nonce_failed', $nonce, $action );

    return false;
}

现在,我们一行一行地来解读这段代码:

  1. 类型转换:

    $nonce = (string) $nonce;
    $uid = (int) get_current_user_id();
    $token = wp_get_session_token();
    
    if ( is_numeric( $action ) ) {
        $action = (int) $action;
    } else {
        $action = wp_nonce_tick() . $action;
    }
    • $nonce = (string) $nonce;: 将传入的 Nonce 转换为字符串类型,确保后续操作的类型一致性。
    • $uid = (int) get_current_user_id();: 获取当前用户的 ID,并转换为整数类型。 如果用户未登录,get_current_user_id() 将返回 0。
    • $token = wp_get_session_token();: 获取用户的会话 Token。 这个 Token 可以帮助进一步验证用户的身份。
    • if ( is_numeric( $action ) ) { ... } else { ... }: 检查 $action 参数是否为数字。 如果是数字,则直接转换为整数类型;如果不是数字,则使用 wp_nonce_tick() 函数生成一个时间戳,并将其与 $action 拼接起来。 这样做的目的是为了确保每个操作都有一个唯一的名字,即使操作的名称相同,只要时间戳不同,生成的 Nonce 也会不一样。
  2. 获取时间片:

    $i = wp_nonce_tick();
    • $i = wp_nonce_tick();: 调用 wp_nonce_tick() 函数获取当前的时间片。 这个时间片是一个整数,表示当前时间所属的 12 小时周期。
    function wp_nonce_tick() {
        /**
         * Filters the lifespan of nonces.
         *
         * @since 4.5.0
         *
         * @param int $lifespan The amount of time to live in seconds. Default 12 hours.
         */
        $nonce_life = apply_filters( 'nonce_life', DAY_IN_SECONDS / 2 );
    
        return ceil( time() / ( $nonce_life ) );
    }
    • DAY_IN_SECONDS 是一个常量,表示一天有多少秒 (86400)。
    • / 2 表示 Nonce 的有效期是 12 小时。
    • apply_filters( 'nonce_life', DAY_IN_SECONDS / 2 ) 允许开发者通过 nonce_life 过滤器来修改 Nonce 的有效期。
    • ceil( time() / ( $nonce_life ) ) 计算当前时间所属的时间片。 比如,如果当前时间是 2023-10-27 10:00:00,那么 time() 函数会返回一个 Unix 时间戳,然后除以 12 小时的秒数,最后使用 ceil() 函数向上取整,得到当前的时间片。
  3. 生成预期 Nonce 并进行比较:

    // 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;
    }
    • wp_hash( $i . '|' . $action . '|' . $uid . '|' . $token, 'nonce' ): 使用 wp_hash() 函数生成一个哈希值。 这个哈希值是根据时间片、操作名称、用户 ID 和会话 Token 生成的。 wp_hash 函数内部使用 WordPress 的盐 (salt) 来增加哈希值的安全性。
    • substr( ..., -12, 10 ): 截取哈希值的最后 12 位,然后取前 10 位作为预期的 Nonce。 这样做是为了缩短 Nonce 的长度,同时保留足够的随机性。
    • hash_equals( $expected, $nonce ): 使用 hash_equals() 函数比较预期的 Nonce 和传入的 Nonce 是否相等。 hash_equals() 函数是一个安全的字符串比较函数,可以防止时序攻击。
    • 代码会检查两个时间片:当前时间片 ($i) 和前一个时间片 ($i - 1)。 这样做的目的是为了允许 Nonce 在 12 小时的有效期内被使用。 如果 Nonce 是在 12-24 小时前生成的,那么它仍然可以被验证通过。
  4. Nonce 验证失败:

    /**
     * Fires when a nonce verification fails.
     *
     * @since 4.4.0
     *
     * @param string|int $nonce  The nonce that was attempted to be verified.
     * @param string|int $action The action name.
     */
    do_action( 'wp_nonce_failed', $nonce, $action );
    
    return false;
    • 如果传入的 Nonce 和预期的 Nonce 不相等,那么 wp_verify_nonce() 函数会触发 wp_nonce_failed 动作。 开发者可以通过监听这个动作来记录 Nonce 验证失败的信息,或者执行其他自定义的操作。
    • 最后,函数返回 false,表示 Nonce 验证失败。

wp_verify_nonce() 的返回值

  • 1: Nonce 验证成功,并且是在 0-12 小时内生成的。
  • 2: Nonce 验证成功,并且是在 12-24 小时内生成的。
  • false: Nonce 验证失败。

实际应用场景

了解了 wp_verify_nonce() 的源码之后,我们来看看它在实际开发中是如何使用的。

假设我们有一个表单,允许用户删除一篇博客文章。 为了防止 CSRF 攻击,我们需要在表单中添加一个 Nonce。

  1. 生成 Nonce:

    在显示表单的页面中,我们需要使用 wp_nonce_field() 函数生成一个 Nonce 字段。

    <?php wp_nonce_field( 'delete_post', 'delete_post_nonce' ); ?>
    • 'delete_post' 是一个字符串,表示操作的名称。 建议为每个操作都使用一个唯一的名称。
    • 'delete_post_nonce' 是 Nonce 字段的名称。 你可以自定义这个名称,但要确保它在整个网站中是唯一的。

    wp_nonce_field() 函数会生成一个隐藏的表单字段,其中包含 Nonce 的值。 它还会生成一个 _wp_http_referer 字段,用来记录来源页面,也起到一定的安全作用。 生成的 HTML 代码类似这样:

    <input type="hidden" id="delete_post_nonce" name="delete_post_nonce" value="a1b2c3d4e5" />
    <input type="hidden" name="_wp_http_referer" value="/wp-admin/post.php?post=123&action=edit" />
  2. 验证 Nonce:

    在处理表单提交的页面中,我们需要使用 wp_verify_nonce() 函数验证 Nonce 的有效性。

    if ( isset( $_POST['delete_post_nonce'] ) && wp_verify_nonce( $_POST['delete_post_nonce'], 'delete_post' ) ) {
        // 删除文章
        wp_delete_post( $_POST['post_id'] );
    } else {
        // Nonce 验证失败
        wp_die( 'Nonce verification failed' );
    }
    • 首先,我们检查 $_POST['delete_post_nonce'] 是否存在。 这是为了防止用户直接访问处理表单提交的页面,而没有提交表单。
    • 然后,我们调用 wp_verify_nonce() 函数验证 Nonce 的有效性。
    • 如果 Nonce 验证成功,那么我们可以安全地删除文章。
    • 如果 Nonce 验证失败,那么我们应该显示一个错误信息,或者执行其他适当的操作。

更高级的用法和注意事项

  • 自定义 Nonce 生命周期:

    可以通过 nonce_life 过滤器来修改 Nonce 的有效期。 例如,如果你希望 Nonce 的有效期是 1 小时,可以这样写:

    add_filter( 'nonce_life', function( $lifespan ) {
        return HOUR_IN_SECONDS;
    } );
  • 使用不同的 Action:

    为每个操作都使用一个唯一的 Action 名称,可以增加 Nonce 的安全性。 如果不同的操作使用相同的 Action 名称,那么攻击者可能会利用一个操作的 Nonce 来执行另一个操作。

  • 用户未登录的情况:

    如果用户未登录,get_current_user_id() 函数会返回 0。 这意味着所有未登录的用户都会使用相同的 Nonce。 为了避免这种情况,可以考虑使用 IP 地址或其他用户标识符来生成 Nonce。 但是,需要注意保护用户的隐私,避免泄露用户的敏感信息。

  • HTTPS:

    强烈建议在网站上启用 HTTPS。 HTTPS 可以加密用户和服务器之间的通信,防止中间人攻击。 如果网站没有启用 HTTPS,那么攻击者可能会截获 Nonce,并利用它来执行 CSRF 攻击。

  • 结合其他安全措施:

    Nonce 只是 WordPress 安全体系中的一部分。 为了提高网站的安全性,还需要结合其他安全措施,例如输入验证、输出转义、SQL 注入防护等。

总结

wp_verify_nonce() 函数是 WordPress 中一个非常重要的安全函数,用于验证 Nonce 的有效性和合法性,防止 CSRF 攻击。 了解它的源码和使用方法,可以帮助我们更好地保护 WordPress 网站的安全。

记住,安全无小事,一定要重视 WordPress 的安全,定期更新 WordPress 和插件,使用强密码,并采取其他必要的安全措施。

好了,今天的讲座就到这里。 希望大家能够从中受益,并在实际开发中正确使用 wp_verify_nonce() 函数。 感谢大家的观看! 下次再见!

发表回复

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