剖析 `wp_verify_nonce()` 函数的源码,它是如何验证 `Nonce` 的有效性的?

嘿,各位码农好!今天咱们来聊聊 WordPress 的 Nonce 验证机制,特别是 wp_verify_nonce() 这个神奇的函数。

要理解 Nonce 验证,首先得明白什么是 Nonce。Nonce,全称 "Number used once",顾名思义,就是一个只能用一次的数字。在 WordPress 中,它被用来保护 URL 和表单,防止 CSRF(跨站请求伪造)攻击。

Nonce 的生命周期:生成、传递与验证

Nonce 的生命周期可以分为三个阶段:

  1. 生成(Generation): 使用 wp_create_nonce() 函数生成一个 Nonce。
  2. 传递(Transmission): 将 Nonce 嵌入到 URL 或表单的隐藏字段中。
  3. 验证(Verification): 使用 wp_verify_nonce() 函数验证 Nonce 的有效性。

今天,我们重点关注第三个阶段,深入剖析 wp_verify_nonce() 函数的源码,看看它是如何验证 Nonce 的有效性的。

wp_verify_nonce() 源码剖析:步步惊心

wp_verify_nonce() 函数位于 wp-includes/pluggable.php 文件中。为了方便理解,我们把它拆解成几个关键步骤,并配上代码注释:

function wp_verify_nonce( $nonce, $action = -1 ) {
    $nonce = (string) $nonce; // 强制转换为字符串,防止类型问题
    $action = (string) $action; // 同样,action也转字符串

    $i = wp_nonce_tick(); // 获取当前时间段的“时间片”

    // Nonce 的长度必须是 10 位,否则直接返回 false。
    if ( strlen( $nonce ) !== 10 ) {
        return false;
    }

    // 拆解 Nonce:
    // 前8位是哈希值,后两位是时间片偏移量。
    $expected = substr( wp_hash( $i . $action, 'nonce' ), -8 ); // 当前时间片计算的哈希值
    if ( hash_equals( $expected, $nonce ) ) { // 用 hash_equals 防止时序攻击
        return 1; // 验证通过,返回 1
    }

    $expected = substr( wp_hash( ( $i - 1 ) . $action, 'nonce' ), -8 ); // 上一个时间片的哈希值
    if ( hash_equals( $expected, $nonce ) ) {
        return 2; // 上一个时间片也验证通过,返回 2
    }

    // 如果当前时间和上一个时间片都验证失败,则返回 false
    return false;
}

让我们逐行解读这段代码:

  1. 类型转换: 首先,将传入的 $nonce$action 参数强制转换为字符串类型。 这样做是为了避免因为类型不一致而导致验证失败。

  2. 获取时间片: 调用 wp_nonce_tick() 函数获取当前时间段的“时间片”。这个时间片是 Nonce 验证的核心,它决定了 Nonce 的有效期。

  3. 长度校验: 检查 Nonce 的长度是否为 10 位。如果不是,直接返回 false。 这是为了确保 Nonce 的格式正确。

  4. 哈希值计算: 使用 wp_hash() 函数计算 Nonce 的哈希值。wp_hash() 函数内部默认使用 hash_hmac 函数,结合站点密钥(AUTH_KEY)和盐(NONCE_SALT)进行加密,保证 Nonce 的安全性。

    • $i . $action:将时间片和 action 拼接起来,作为哈希函数的输入。
    • 'nonce':指定哈希算法的类型为 ‘nonce’,这会影响到盐值的选择。
    • substr(..., -8):截取哈希值的最后 8 位,作为最终的 Nonce 值。
  5. 时间片验证: 将计算出的哈希值与传入的 $nonce 进行比较。如果相等,则验证通过,返回 1

  6. 过期时间片验证: 如果当前时间片的验证失败,则尝试使用上一个时间片进行验证。如果相等,则验证通过,返回 2。 这样做是为了允许 Nonce 在短时间内过期,例如,由于服务器时间不同步或网络延迟。

  7. 验证失败: 如果当前时间和上一个时间片都验证失败,则返回 false,表示 Nonce 无效。

wp_nonce_tick() 源码剖析:时间片的概念

wp_nonce_tick() 函数位于 wp-includes/pluggable.php 文件中,它负责计算当前时间段的“时间片”。

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

    return ceil( time() / ( $nonce_life ) );
}

这段代码非常简单:

  1. 获取 Nonce 生命周期: 使用 apply_filters( 'nonce_life', DAY_IN_SECONDS / 2 ) 获取 Nonce 的生命周期。 默认情况下,Nonce 的生命周期是 12 小时。你可以使用 nonce_life 过滤器来修改 Nonce 的生命周期。

  2. 计算时间片: 使用 ceil( time() / ( $nonce_life ) ) 计算当前时间片。 time() 函数返回当前时间的 Unix 时间戳,将其除以 Nonce 的生命周期,然后向上取整,得到当前时间片。

举个例子:

  • 假设 Nonce 的生命周期是 12 小时(43200 秒)。
  • 当前时间的 Unix 时间戳是 1678886400。
  • 那么,当前时间片就是 ceil( 1678886400 / 43200 ) = 38863

这意味着,在接下来的 12 小时内,所有使用相同 action 和时间片生成的 Nonce 都会被认为是有效的。

关键点总结:

  • 时间敏感性: Nonce 的有效期由 wp_nonce_tick() 函数决定,默认情况下是 12 小时。
  • 哈希算法: 使用 wp_hash() 函数生成 Nonce 的哈希值,保证 Nonce 的安全性。
  • 双重验证: 验证当前时间和上一个时间片的哈希值,允许 Nonce 在短时间内过期。
  • hash_equals函数: 使用 hash_equals() 函数进行字符串比较,防止时序攻击。这种攻击方式可以通过测量比较两个字符串所需的时间来推断字符串的内容。hash_equals() 可以确保无论字符串是否匹配,比较操作所需的时间都是相同的,从而阻止时序攻击。

为什么要使用 hash_equals()

你可能会好奇,为什么不用简单的 ===== 来比较哈希值呢? 这是因为,使用 ===== 比较字符串可能会受到时序攻击(Timing Attack)。

时序攻击是一种利用程序执行时间差异来推断敏感信息的攻击方式。 在比较字符串时,如果第一个字符不匹配,则比较会立即停止。 如果攻击者能够测量比较两个字符串所需的时间,他们就可以推断出字符串的前缀是否匹配。

hash_equals() 函数通过执行固定时间的字符串比较来防止时序攻击。 无论字符串是否匹配,hash_equals() 函数都会比较所有字符,从而避免了攻击者通过测量时间来推断敏感信息。

实用技巧和注意事项

  1. Action 的选择: action 参数应该尽可能地具有唯一性,以避免与其他 Nonce 冲突。 例如,可以使用插件或主题的名称作为 action 的一部分。
  2. Nonce 的生命周期: 默认情况下,Nonce 的生命周期是 12 小时。 但是,你可以使用 nonce_life 过滤器来修改 Nonce 的生命周期。 在选择 Nonce 的生命周期时,需要权衡安全性和用户体验。
  3. Nonce 的使用场景: Nonce 应该用于保护所有可能导致状态改变的操作,例如,提交表单、删除文章、更新设置等。
  4. 防止重放攻击: Nonce 可以防止 CSRF 攻击,但不能完全防止重放攻击。 重放攻击是指攻击者截获有效的请求并重复发送。 为了防止重放攻击,可以使用其他技术,例如,在请求中包含时间戳,并验证时间戳是否在有效期内。
  5. 错误处理: 在验证 Nonce 失败时,应该向用户显示友好的错误消息,并阻止操作的执行。

示例代码:

假设我们有一个简单的表单,用于更新用户的个人资料。

  1. 生成 Nonce: 在表单中添加一个隐藏字段,用于存储 Nonce。

    <?php
    $action = 'update_profile';
    $nonce = wp_create_nonce( $action );
    ?>
    <form method="post" action="">
        <input type="hidden" name="action" value="<?php echo esc_attr( $action ); ?>">
        <input type="hidden" name="_wpnonce" value="<?php echo esc_attr( $nonce ); ?>">
        <label for="name">Name:</label>
        <input type="text" name="name" id="name">
        <button type="submit">Update Profile</button>
    </form>
  2. 验证 Nonce: 在处理表单提交时,验证 Nonce 的有效性。

    <?php
    if ( isset( $_POST['action'] ) && $_POST['action'] == 'update_profile' ) {
        if ( ! isset( $_POST['_wpnonce'] ) || ! wp_verify_nonce( $_POST['_wpnonce'], 'update_profile' ) ) {
            wp_die( 'Invalid Nonce!' );
        }
    
        // 处理表单数据
        $name = sanitize_text_field( $_POST['name'] );
        // ...
    }
    ?>

Nonce 验证失败的常见原因:

原因 解决方法
Nonce 过期 确保 Nonce 的生命周期足够长,或者在表单提交前重新生成 Nonce。
Action 不匹配 检查生成 Nonce 和验证 Nonce 时使用的 action 是否一致。
Nonce 被篡改 确保 Nonce 在传输过程中没有被篡改。可以使用 HTTPS 来加密传输过程。
服务器时间不同步 确保服务器的时间是准确的。可以使用 NTP 服务器来同步服务器时间。
缓存问题 清除浏览器和服务器的缓存。
使用了错误的 Nonce 验证函数 确保使用了正确的 Nonce 验证函数。wp_verify_nonce() 用于验证 Nonce,而 wp_nonce_field() 用于生成包含 Nonce 的隐藏字段。
Nonce 生成和验证不在同一个请求中 确保 Nonce 在同一个请求中生成和验证。例如,如果在 AJAX 请求中生成 Nonce,然后在另一个 AJAX 请求中验证 Nonce,可能会导致验证失败。
插件冲突 禁用所有插件,然后逐个启用,以确定哪个插件导致了 Nonce 验证失败。

总结

wp_verify_nonce() 函数是 WordPress 安全体系中一个重要的组成部分。 了解它的工作原理,可以帮助我们更好地保护 WordPress 站点,防止 CSRF 攻击。 希望今天的讲座能够帮助大家更深入地理解 WordPress Nonce 验证机制。 下课!

发表回复

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