详解 WordPress `wp_nonce` 机制源码:`wp_create_nonce()` 如何生成随机字符串。

好的,各位观众老爷,咱们今天来聊聊 WordPress 里的一个神秘小玩意儿:wp_nonce。这玩意儿看着不起眼,但却肩负着保护咱们网站安全的重任。今天咱们就深入源码,扒一扒 wp_create_nonce() 到底是怎么变戏法,生成那些看似随机的字符串的。

开场白:Nonce 是个啥?

Nonce,英文全称 "number used once",顾名思义,就是“一次性使用的数字”。在 WordPress 中,Nonce 被用来防止 CSRF (Cross-Site Request Forgery,跨站请求伪造) 攻击。简单来说,CSRF 攻击就是坏人诱导你点击一个链接,然后偷偷地以你的身份发送请求,比如删除文章、修改密码等等。

wp_nonce 机制通过在表单或者 URL 中添加一个随机的、只能使用一次的字符串,来验证请求是否来自真正的用户操作,而不是恶意的第三方。

wp_create_nonce():Nonce 的制造者

wp_create_nonce() 函数就是负责生成这个随机字符串的核心人物。咱们先来看一下它的源码(简化版,重点突出):

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 );
}

看着代码是不是有点懵?没关系,咱们一步一步来拆解它。

第一步:准备材料

wp_create_nonce() 函数首先要收集一些“材料”,这些材料会影响最终生成的 Nonce 值,增加破解难度。

  • $action:动作(Action)

    这个参数是可选的,但强烈建议你使用它。$action 是一个字符串,用来描述你要保护的操作是什么,比如 ‘edit_post’ (编辑文章)、’delete_post’ (删除文章) 等等。 不同的 action 会产生不同的 Nonce 值。如果不传,默认为 -1。

  • $user:当前用户

    通过 wp_get_current_user() 获取当前用户对象,然后从中提取用户 ID ($uid)。 这样,不同的用户生成的 Nonce 值也会不同。

  • $token:会话令牌

    wp_get_session_token() 获取当前用户的会话令牌。 这个令牌存储在用户的浏览器 cookie 中,并且会随着用户登录状态的变化而变化。有了这个令牌,即使是同一个用户,在不同的会话中生成的 Nonce 值也会不同。

  • $i:时间戳(Tick)

    这个比较特殊,是通过 wp_nonce_tick() 函数生成的。咱们稍后详细分析它。

第二步:wp_nonce_tick():时间的奥秘

wp_nonce_tick() 函数返回一个基于时间戳的整数。这个整数会定期更新,也就是说,Nonce 的有效期是有限的。

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

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

简单解释一下:

  1. $nonce_life:Nonce 的有效期

    默认情况下,Nonce 的有效期是 DAY_IN_SECONDS,也就是一天(86400 秒)。但是,你可以通过 nonce_life 过滤器来修改这个值。

  2. time():当前时间戳

    获取当前的 Unix 时间戳(从 1970 年 1 月 1 日 00:00:00 UTC 到现在的秒数)。

  3. ceil( time() / ( $nonce_life / 2 ) ):计算 Tick 值

    这行代码是关键。它将当前时间戳除以 ($nonce_life / 2),然后向上取整。这意味着,每隔 ($nonce_life / 2) 秒,wp_nonce_tick() 函数的返回值就会增加 1。由于默认的 nonce_life 是一天,所以 wp_nonce_tick() 函数的返回值每 12 小时会更新一次。

    举个例子:

    时间戳 (time()) nonce_life (秒) nonce_life / 2 time() / (nonce_life / 2) ceil() 结果 (Tick)
    1678886400 (Mar 15 2023 00:00:00 UTC) 86400 43200 38863.111 38864
    1678929600 (Mar 15 2023 12:00:00 UTC) 86400 43200 38864.111 38865
    1678972800 (Mar 16 2023 00:00:00 UTC) 86400 43200 38865.111 38866

    可以看到,每隔 12 个小时,Tick 值就会增加 1。

第三步:wp_hash():哈希算法的魔力

wp_hash() 函数是生成 Nonce 字符串的核心。它使用 WordPress 的哈希算法,将前面收集到的所有材料组合在一起,生成一个唯一的哈希值。

function wp_hash( $data, $scheme = 'auth' ) {
    static $wp_hasher;

    if ( empty( $wp_hasher ) ) {
        require_once ABSPATH . 'wp-includes/class-phpass.php';
        $wp_hasher = new PasswordHash( 8, true );
    }

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

简单解释一下:

  1. $data:要哈希的数据

    这就是前面收集到的所有材料,它们被拼接成一个字符串:$i . '|' . $action . '|' . $uid . '|' . $token。 例如:38864|edit_post|1|e5b9a86b7033709672e5f880b6a39b31

  2. $scheme:哈希方案

    这里指定的是 ‘nonce’,但实际上 wp_hash() 函数并不直接使用这个参数。 它的作用主要是为了兼容旧版本的 WordPress。

  3. PasswordHash

    WordPress 使用 PasswordHash 类来进行哈希运算。 这个类来自 PHPass 库,它使用一种叫做 Portable Hash 的哈希算法。这种算法的特点是安全性高,并且可以在不同的 PHP 环境下使用。

  4. $wp_hasher->HashPassword( $data ):生成哈希值

    这一步就是真正生成哈希值的过程。 HashPassword() 方法会将 $data 经过一系列复杂的运算,最终生成一个长度为 64 位的哈希字符串。

第四步:substr():截取 Nonce 字符串

最后一步,substr() 函数从哈希字符串中截取一部分作为最终的 Nonce 值。

substr( wp_hash( $i . '|' . $action . '|' . $uid . '|' . $token, 'nonce' ), -12, 10 )

这里截取的是哈希字符串的倒数第 12 位开始,长度为 10 的子字符串。 这样做的目的是为了缩短 Nonce 的长度,方便在 URL 中传递。

总结:wp_create_nonce() 的工作流程

咱们用一张表格来总结一下 wp_create_nonce() 的工作流程:

步骤 函数/操作 说明 示例
1 收集材料 获取动作、用户 ID、会话令牌和 Tick 值 $action = 'edit_post', $uid = 1, $token = 'e5b9a86b7033709672e5f880b6a39b31', $i = 38864
2 拼接字符串 将所有材料拼接成一个字符串 $data = '38864|edit_post|1|e5b9a86b7033709672e5f880b6a39b31'
3 哈希运算 使用 wp_hash() 函数对字符串进行哈希运算 wp_hash($data) = 'f4c2f1d6a3c96f427a4383e5c9b1d3f26d89e9b84d7c4f1a6e6f7d9c1b4a2c3'
4 截取字符串 从哈希字符串中截取一部分作为 Nonce 值 substr('f4c2f1d6a3c96f427a4383e5c9b1d3f26d89e9b84d7c4f1a6e6f7d9c1b4a2c3', -12, 10) = 'a6e6f7d9c1'

Nonce 的验证:wp_verify_nonce()

光生成 Nonce 还不够,还需要验证它是否有效。 WordPress 提供了 wp_verify_nonce() 函数来完成这个任务。

function wp_verify_nonce( $nonce, $action = -1 ) {
    $nonce = (string) $nonce;
    $max_age = 2 * DAY_IN_SECONDS;

    $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
    if ( wp_hash_equals( substr( wp_hash( $i . '|' . $action . '|' . $uid . '|' . $token, 'nonce' ), -12, 10 ), $nonce ) ) {
        return 1;
    }

    // Nonce generated 12-24 hours ago
    if ( wp_hash_equals( substr( wp_hash( ( $i - 1 ) . '|' . $action . '|' . $uid . '|' . $token, 'nonce' ), -12, 10 ), $nonce ) ) {
        return 2;
    }

    // Nonce generated 24-36 hours ago
    if ( wp_hash_equals( substr( wp_hash( ( $i - 2 ) . '|' . $action . '|' . $uid . '|' . $token, 'nonce' ), -12, 10 ), $nonce ) ) {
        return 3;
    }

    return false;
}

这个函数的工作原理是:

  1. 获取参数: 传入 Nonce 值和 Action 值。
  2. 计算当前 Tick 值: 使用 wp_nonce_tick() 计算当前的 Tick 值。
  3. 尝试验证: 使用当前 Tick 值和前两个 Tick 值($i, $i - 1, $i - 2)重新生成 Nonce 值,并与传入的 Nonce 值进行比较。
  4. 返回结果: 如果验证成功,则返回 1, 2 或 3,表示 Nonce 的有效时间范围。 如果验证失败,则返回 false

为什么 wp_verify_nonce() 要验证三个 Tick 值?

这是因为 Nonce 的有效期是 24 小时(两个 Tick 周期)。 为了防止时间差导致验证失败,wp_verify_nonce() 函数会尝试使用当前 Tick 值以及前两个 Tick 值来验证 Nonce。 这样,即使 Nonce 刚好在 Tick 值更新的时候被提交,也能保证验证成功。 实际上,如果nonce_life被更改,验证会继续验证之前所有的tick直到超过最大存活时间max_age

Nonce 的使用示例

// 生成 Nonce
$nonce = wp_create_nonce( 'my_action' );

// 在 URL 中添加 Nonce
$url = add_query_arg( array(
    '_wpnonce' => $nonce,
    'action'   => 'my_action',
    'data'     => 'some_data',
), admin_url( 'admin-ajax.php' ) );

// 或者在表单中添加 Nonce
?>
<form action="<?php echo admin_url( 'admin-ajax.php' ); ?>" method="post">
    <input type="hidden" name="action" value="my_action">
    <input type="hidden" name="data" value="some_data">
    <?php wp_nonce_field( 'my_action' ); ?>
    <input type="submit" value="Submit">
</form>
<?php

// 验证 Nonce
if ( isset( $_REQUEST['action'] ) && $_REQUEST['action'] == 'my_action' ) {
    check_admin_referer( 'my_action' ); // 这是个更高级的验证函数,内部会调用 wp_verify_nonce
    // 或者
    if ( ! wp_verify_nonce( $_REQUEST['_wpnonce'], 'my_action' ) ) {
        wp_die( 'Nonce verification failed!' );
    }

    // 处理数据
    echo 'Data received: ' . $_REQUEST['data'];
}

安全建议

  • 使用 Action: 始终为你的 Nonce 指定一个具体的 Action 值。 这样可以防止 Nonce 被用于其他不相关的操作。
  • 不要泄露 Nonce: Nonce 应该只在表单或者 URL 中传递,不要在其他地方泄露它。
  • 定期更新 Nonce: Nonce 的有效期是有限的,所以要定期更新它。
  • 使用 check_admin_referer()wp_verify_nonce() 验证 Nonce: 在处理请求之前,一定要验证 Nonce 的有效性。check_admin_referer() 函数在后台管理界面使用,会自动检查 _wpnonce_wp_http_referer 字段,并验证 Nonce。 wp_verify_nonce() 函数则更加灵活,可以用于任何场景。

总结

wp_nonce 机制是 WordPress 中一种重要的安全措施,它可以有效地防止 CSRF 攻击。 通过深入了解 wp_create_nonce() 函数的源码,我们可以更好地理解 Nonce 的生成原理,从而更好地使用它来保护我们的网站安全。

好了,今天的讲座就到这里。 希望大家能够对 wp_nonce 机制有一个更深入的了解。 谢谢大家!

发表回复

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