深入理解 WordPress `wp_nonce` 机制的源码:如何防止 CSRF 攻击,以及其生命周期。

大家好!我是今天的主讲人,很高兴能和大家一起深入挖掘WordPress那神秘又关键的wp_nonce机制。准备好化身代码侦探,一起追踪CSRF攻击,揭秘wp_nonce的生命周期了吗?让我们开始吧!

第一幕:CSRF攻击,潜伏的危机

想象一下,你正在浏览一个看起来人畜无害的网站,突然,你的银行账户被转走了一笔钱!是不是感觉背后发凉?这很有可能就是CSRF(Cross-Site Request Forgery,跨站请求伪造)攻击在作祟。

CSRF攻击的原理很简单,坏人利用你的身份,在你不知情的情况下,冒充你向网站发起请求。他们是如何做到的呢?

  1. 信任危机: 网站信任你的浏览器,因为它已经通过Cookie或其他方式验证了你的身份。
  2. 伪造请求: 坏人通过各种手段(例如,诱导你点击一个恶意链接,或者在你浏览的页面中插入隐藏的表单),构造一个看似合法的请求。
  3. 瞒天过海: 你的浏览器会自动携带Cookie等身份验证信息,将这个伪造的请求发送给网站。
  4. 中招了! 网站误以为是你的真实操作,执行了坏人的指令。

举个例子,假设一个网站允许你通过GET请求删除一篇文章:

<a href="https://example.com/wp-admin/post.php?action=delete&post=123">删除文章</a>

坏人可以在自己的网站上构造一个隐藏的iframe:

<iframe src="https://example.com/wp-admin/post.php?action=delete&post=123" style="display:none;"></iframe>

当你访问坏人的网站时,这个iframe会自动加载,你的浏览器会带着你的身份信息发送删除文章的请求,结果就是文章被悄无声息地删除了!

第二幕:wp_nonce,安全卫士登场

为了对抗CSRF攻击,WordPress引入了wp_nonce机制。 wp_nonce,你可以理解为“一次性使用的暗号”,或者“随机验证码”。它的作用是确保请求是由用户主动发起的,而不是被坏人伪造的。

wp_nonce的核心思想是:在每个敏感的操作请求中,都包含一个独特的、难以预测的令牌(token)。网站在处理请求时,会验证这个令牌是否有效。

wp_nonce的工作流程:

  1. 生成令牌: 当你访问一个包含敏感操作的页面时,WordPress会生成一个wp_nonce
  2. 嵌入请求: 这个wp_nonce会被嵌入到表单或者URL中,作为请求的一部分发送给服务器。
  3. 验证令牌: 服务器收到请求后,会验证wp_nonce是否有效。如果有效,就执行操作;否则,拒绝请求。

wp_nonce的关键特性:

  • 唯一性: 对于每个操作,wp_nonce都是不同的。
  • 不可预测性: 坏人很难猜测出wp_nonce的值。
  • 时效性: wp_nonce在一段时间后会失效,防止被重复利用。

第三幕:源码剖析,揭秘wp_nonce的生成与验证

让我们深入WordPress的源码,看看wp_nonce是如何生成和验证的。

1. 生成wp_noncewp_create_nonce() 函数

wp_create_nonce() 函数是生成wp_nonce的核心。 它的原型如下:

function wp_create_nonce( $action = -1 ) {
    $user = wp_get_current_user();
    $uid = (int) $user->ID;
    if ( ! $uid ) {
        /** This filter is documented in wp-includes/pluggable.php */
        $uid = apply_filters( 'nonce_user_logged_out', $uid, $action );
    }

    $token = wp_get_session_token();
    $i = wp_nonce_tick();

    return substr( wp_hash( $i . '|' . $action . '|' . $uid . '|' . $token, 'nonce' ), -12, 10 );
}
  • $action:一个字符串,用于标识操作的类型。例如,’delete_post’,’update_options’等。这个action非常重要,它将wp_nonce与特定的操作关联起来。
  • wp_get_current_user(): 获取当前用户的信息。
  • $uid: 当前用户的ID。未登录用户会被赋予一个默认的ID。
  • wp_get_session_token(): 获取用户的session token.
  • wp_nonce_tick(): 返回一个基于时间的数字,用于增加wp_nonce的随机性,并且控制wp_nonce的有效期。
  • wp_hash(): 使用WordPress的哈希算法,对以上信息进行哈希运算。
  • substr(): 截取哈希结果的一部分,作为最终的wp_nonce

代码解读:

  • 首先,函数获取当前用户的ID和session token。
  • 然后,调用wp_nonce_tick()获取一个基于时间的数字。
  • 接着,将action、用户ID、session token 和时间戳组合在一起,并使用wp_hash()函数进行哈希运算。
  • 最后,截取哈希结果的一部分,作为最终的wp_nonce

wp_nonce_tick() 函数

wp_nonce_tick() 函数用于生成一个基于时间的数字,这个数字会影响wp_nonce的有效期。

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

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

代码解读:

  • DAY_IN_SECONDS 是一个常量,表示一天有多少秒(86400)。
  • nonce_life 默认值是DAY_IN_SECONDS, 即24小时. 可以通过nonce_life filter 来修改.
  • time() 函数返回当前的时间戳。
  • 函数返回 ceil( time() / ( $nonce_life / 2 ) ), 可以理解为以12小时为一个时间片, 当前时间属于第几个时间片.

2. 验证wp_noncewp_verify_nonce() 函数

wp_verify_nonce() 函数用于验证wp_nonce是否有效。它的原型如下:

function wp_verify_nonce( $nonce, $action = -1 ) {
    $nonce = (string) $nonce;
    $max_int = strlen( (string) PHP_INT_MAX );
    if ( strlen( $nonce ) > $max_int ) {
        return false;
    }

    $user = wp_get_current_user();
    $uid = (int) $user->ID;
    if ( ! $uid ) {
        /** This filter is documented in wp-includes/pluggable.php */
        $uid = apply_filters( 'nonce_user_logged_out', $uid, $action );
    }

    $token = wp_get_session_token();
    $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 3.6.0
     *
     * @param string|int $nonce  The nonce that was attempted to be verified.
     * @param string|int $action The action that was used.
     */
    do_action( 'wp_nonce_failed', $nonce, $action );

    return false;
}
  • $nonce:要验证的wp_nonce
  • $action:生成wp_nonce时使用的action

代码解读:

  • 函数首先获取当前用户ID。
  • 然后,调用wp_nonce_tick()获取当前时间片。
  • 接着,函数会计算两个可能的wp_nonce值:
    • 一个是用当前时间片生成的wp_nonce
    • 另一个是用前一个时间片生成的wp_nonce。这是为了允许wp_nonce在短时间内过期。
  • 使用hash_equals() 函数比较传入的$nonce 和计算出来的两个wp_nonce值.
  • 如果传入的$nonce 与其中一个匹配,函数返回 12;否则,返回 false

3. 使用wp_nonce:表单和URL

WordPress提供了两个辅助函数,方便我们在表单和URL中使用wp_nonce

  • wp_nonce_field() 用于在表单中生成隐藏的wp_nonce字段。
wp_nonce_field( $action, $name, $referer, $echo );
*   `$action`:生成`wp_nonce`时使用的`action`。
*   `$name`:`wp_nonce`字段的名称,默认为`_wpnonce`。
*   `$referer`:是否生成一个隐藏的`referer`字段,用于验证请求的来源,默认为`true`。
*   `$echo`:是否直接输出HTML代码,默认为`true`。
  • wp_nonce_url() 用于在URL中添加wp_nonce参数。
wp_nonce_url( $actionurl, $action, $name );
*   `$actionurl`:要添加`wp_nonce`的URL。
*   `$action`:生成`wp_nonce`时使用的`action`。
*   `$name`:`wp_nonce`参数的名称,默认为`_wpnonce`。

示例:

表单中使用wp_nonce

<form action="options.php" method="post">
    <?php settings_fields( 'my_plugin_options' ); ?>
    <?php do_settings_sections( 'my_plugin_options' ); ?>
    <?php wp_nonce_field( 'my_plugin_options_update', 'my_plugin_options_nonce' ); ?>
    <?php submit_button(); ?>
</form>

URL中使用wp_nonce

<a href="<?php echo wp_nonce_url( admin_url( 'admin-post.php?action=delete_post&post_id=' . $post_id ), 'delete_post_' . $post_id, 'delete_nonce' ); ?>">删除文章</a>

第四幕:wp_nonce的生命周期

wp_nonce的生命周期由wp_nonce_tick()函数决定。默认情况下,wp_nonce的有效期是24小时,每12小时更新一次。

这意味着,如果你在12小时内使用同一个wp_nonce,它是有效的。但是,如果超过12小时,wp_nonce就会失效。

wp_nonce失效的原因:

  • wp_nonce_tick()函数返回的时间片会随着时间的推移而改变。
  • wp_verify_nonce()函数会验证wp_nonce是否是用当前时间片或前一个时间片生成的。
  • 如果wp_nonce是用更早的时间片生成的,验证就会失败。

第五幕:wp_nonce的最佳实践

  • 使用唯一的action 为每个不同的操作使用不同的action,可以提高安全性。
  • 验证wp_nonce 在处理敏感操作时,务必验证wp_nonce,防止CSRF攻击。
  • 不要在公共缓存中使用wp_nonce wp_nonce是针对特定用户的,如果缓存在公共缓存中,可能会导致安全问题。
  • 定期更新wp_nonce 尽管wp_nonce的有效期是24小时,但建议在用户执行敏感操作时,重新生成wp_nonce
  • 考虑用户体验: wp_nonce过期可能会影响用户体验。可以考虑在wp_nonce过期时,提示用户刷新页面。

第六幕:常见问题解答

  • wp_nonce能完全防止CSRF攻击吗?

    • wp_nonce可以大大降低CSRF攻击的风险,但不能完全杜绝。如果网站存在其他安全漏洞,例如XSS攻击,坏人仍然有可能绕过wp_nonce
  • wp_nonceaction参数有什么作用?

    • action参数用于标识操作的类型,将wp_nonce与特定的操作关联起来。这可以防止坏人将一个wp_nonce用于不同的操作。
  • 我可以修改wp_nonce的有效期吗?

    • 可以通过nonce_life filter 修改wp_nonce的有效期。
  • wp_nonce和验证码有什么区别?

    • wp_nonce主要用于防止CSRF攻击,而验证码主要用于防止机器人攻击。

总结:

wp_nonce是WordPress中一个重要的安全机制,可以有效地防止CSRF攻击。理解wp_nonce的原理和使用方法,可以帮助我们构建更安全的WordPress网站。希望今天的讲解能够帮助大家更好地理解wp_nonce机制,并在实际开发中正确使用它。记住,安全无小事,保护网站安全,人人有责!

发表回复

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