剖析 `wp_nonce` 机制的源码,它如何生成一个基于用户 ID 和时间戳的随机字符串来防止 CSRF?

诸位观众老爷们,晚上好!我是今晚的讲师,很高兴能和大家一起聊聊WordPress里的wp_nonce机制。这玩意儿,听起来高大上,其实说白了,就是WordPress用来防CSRF(跨站请求伪造)攻击的一把瑞士军刀。今天咱们就来扒一扒它的源码,看看这把刀是怎么打造出来的。

什么是CSRF?为什么要防?

首先,咱得搞明白什么是CSRF。想象一下,你登录了银行网站,正准备转账给朋友。突然,你点开了一个看似无害的链接。这个链接背后隐藏着一个恶意网站,它在你不知情的情况下,利用你已经登录的银行账号,偷偷转走了你的钱!这就是CSRF攻击。

攻击者利用你已经登录的身份,冒充你发送请求,达到不可告人的目的。所以,防CSRF就显得尤为重要。

wp_nonce:WordPress的防盗门

wp_nonce,全称WordPress Nonce,Non-Use Once,翻译过来就是“一次性使用的令牌”。它的核心思想是:每次执行敏感操作,都生成一个随机的、只能使用一次的令牌。服务器在处理请求时,会验证这个令牌是否有效。如果令牌不对,或者已经被使用过,就拒绝执行操作。

这样一来,即使攻击者伪造了请求,也无法提供正确的令牌,从而避免了CSRF攻击。

wp_nonce的生成过程:庖丁解牛式分析

接下来,咱们就深入wp_nonce的源码,看看它是如何生成这个令牌的。

wp_nonce的生成主要涉及到两个函数:wp_create_nonce()wp_verify_nonce()wp_create_nonce()负责生成nonce,wp_verify_nonce()负责验证nonce。

  1. wp_create_nonce( $action = -1 ):打造令牌的工厂

    这个函数负责生成nonce。$action参数是一个字符串,用来标识这个nonce是用于哪个操作的。例如,你可以用delete_post作为$action来生成一个删除文章的nonce。

    function wp_create_nonce( $action = -1 ) {
       $user = wp_get_current_user();
       $uid = (int) $user->ID;
       if ( empty( $uid ) ) {
           /** This filter is documented in wp-includes/pluggable.php */
           $uid = apply_filters( 'nonce_user_logged_out', $uid, $action );
       }
    
       $token = wp_nonce_tick();
       $hash = hash_hmac( 'md5', $token . '|' . $action . '|' . $uid, wp_nonce_salt() );
    
       /**
        * Filters the nonce value.
        *
        * @since 4.7.0
        *
        * @param string $nonce  The nonce value.
        * @param int    $action The nonce action.
        * @param int    $uid    The user ID.
        */
       return apply_filters( 'nonce_value', $hash, $action, $uid );
    }

    让我们一步一步地拆解这段代码:

    • 获取用户ID:

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

      首先,它会获取当前用户的ID。如果用户未登录,$uid将为0。如果用户未登录,并且有插件使用了nonce_user_logged_out filter,那么$uid可能会被修改。

    • 获取时间戳:

      $token = wp_nonce_tick();

      wp_nonce_tick()函数返回一个基于时间戳的值。这个值会定期更新,保证nonce的有效时间是有限的。一会儿我们会详细分析这个函数。

    • 生成哈希值:

      $hash = hash_hmac( 'md5', $token . '|' . $action . '|' . $uid, wp_nonce_salt() );

      这是生成nonce的关键步骤。它使用hash_hmac()函数,基于md5算法,对$token$action$uid进行哈希运算。wp_nonce_salt()函数返回一个随机的盐值,用于增加哈希值的安全性。

    • 应用过滤器:

      return apply_filters( 'nonce_value', $hash, $action, $uid );

      最后,它会应用nonce_value过滤器,允许插件修改nonce的值。

    wp_nonce_tick():时间流逝的见证者

    function wp_nonce_tick() {
       /**
        * Filters the lifespan of nonces.
        *
        * @since 2.5.0
        *
        * @param int $lifespan Lifespan in seconds. Default 12 hours.
        */
       $nonce_life = apply_filters( 'nonce_life', DAY_IN_SECONDS / 2 );
    
       return ceil( time() / ( $nonce_life ) );
    }

    这个函数的作用是返回一个基于时间戳的值。默认情况下,nonce的有效期是12小时。time()函数返回当前的时间戳,除以$nonce_life,然后向上取整。这意味着,每隔12小时,wp_nonce_tick()返回的值就会发生变化,从而使得nonce失效。

    wp_nonce_salt():安全的基石

    function wp_nonce_salt() {
       $salt = wp_hash( 'nonce', 'auth' );
    
       /**
        * Filters the salt used in nonces.
        *
        * @since 3.5.0
        *
        * @param string $salt The salt value.
        */
       return apply_filters( 'nonce_salt', $salt );
    }

    这个函数返回一个用于生成nonce的盐值。它使用wp_hash()函数生成一个基于nonceauth的哈希值。盐值的目的是增加nonce的安全性,防止攻击者通过彩虹表等方法破解nonce。

    总结一下,wp_create_nonce()的生成过程可以概括为:

    1. 获取当前用户的ID。
    2. 获取基于时间戳的值。
    3. 使用hash_hmac()函数,基于md5算法,对时间戳、action和用户ID进行哈希运算,并使用盐值增加安全性。
    4. 应用nonce_value过滤器。
  2. wp_verify_nonce( $nonce, $action = -1 ):火眼金睛的守门人

    这个函数负责验证nonce是否有效。

    function wp_verify_nonce( $nonce, $action = -1 ) {
       $nonce = (string) $nonce;
       $user = wp_get_current_user();
       $uid = (int) $user->ID;
       if ( empty( $uid ) ) {
           /** This filter is documented in wp-includes/pluggable.php */
           $uid = apply_filters( 'nonce_user_logged_out', $uid, $action );
       }
    
       if ( empty( $nonce ) ) {
           return false;
       }
    
       $token = wp_nonce_tick();
       $expected = hash_hmac( 'md5', $token . '|' . $action . '|' . $uid, wp_nonce_salt() );
    
       if ( hash_equals( $expected, $nonce ) ) {
           /**
            * Fires when a nonce is verified.
            *
            * @since 5.8.0
            *
            * @param string $nonce  The nonce value.
            * @param int    $action The nonce action.
            * @param int    $uid    The user ID.
            */
           do_action( 'wp_verify_nonce', $nonce, $action, $uid );
    
           return 1;
       }
    
       $token = wp_nonce_tick() - 1;
       $expected = hash_hmac( 'md5', $token . '|' . $action . '|' . $uid, wp_nonce_salt() );
       if ( hash_equals( $expected, $nonce ) ) {
           /** This action is documented in wp-includes/pluggable.php */
           do_action( 'wp_verify_nonce', $nonce, $action, $uid );
           return 2;
       }
    
       /**
        * Filters whether the nonce is valid.
        *
        * @since 3.0.0
        *
        * @param bool   $valid  Whether the nonce is valid.
        * @param string $nonce  The nonce value.
        * @param int    $action The nonce action.
        * @param int    $uid    The user ID.
        */
       return apply_filters( 'nonce_check', false, $nonce, $action, $uid );
    }

    这段代码的逻辑如下:

    • 获取用户ID:wp_create_nonce()一样,先获取用户ID。
    • 检查nonce是否为空: 如果nonce为空,直接返回false。
    • 重新生成哈希值: 使用当前的时间戳,重新生成一个哈希值。
    • 比较哈希值: 使用hash_equals()函数,比较传入的nonce和重新生成的哈希值是否相等。hash_equals()函数可以防止时序攻击,保证比较的安全性。
    • 如果相等,则验证通过。
    • 考虑过期情况: 为了处理nonce过期的情况,它还会使用上一个时间戳,重新生成一个哈希值,再次进行比较。如果相等,也验证通过。
    • 应用过滤器: 如果验证失败,它会应用nonce_check过滤器,允许插件修改验证结果。

    总结一下,wp_verify_nonce()的验证过程可以概括为:

    1. 获取当前用户的ID。
    2. 使用当前的时间戳,重新生成一个哈希值。
    3. 比较传入的nonce和重新生成的哈希值是否相等。
    4. 如果相等,则验证通过。
    5. 为了处理nonce过期的情况,它还会使用上一个时间戳,重新生成一个哈希值,再次进行比较。
    6. 应用nonce_check过滤器。

wp_nonce的使用:实战演练

说了这么多理论,咱们来点实际的。看看如何在代码中使用wp_nonce

  1. 生成nonce:

    $nonce = wp_create_nonce( 'my_action' );

    这段代码会生成一个action为my_action的nonce。

  2. 将nonce添加到URL或表单中:

    $url = add_query_arg( '_wpnonce', $nonce, 'my_page.php' );

    或者:

    <input type="hidden" name="_wpnonce" value="<?php echo esc_attr( wp_create_nonce( 'my_action' ) ); ?>" />

    将nonce添加到URL或表单中,以便在提交请求时,将nonce发送到服务器。

  3. 验证nonce:

    if ( ! wp_verify_nonce( $_REQUEST['_wpnonce'], 'my_action' ) ) {
       wp_die( 'Security check failed' );
    }

    在处理请求时,使用wp_verify_nonce()函数验证nonce是否有效。如果验证失败,就拒绝执行操作。

wp_nonce的安全性:细节决定成败

wp_nonce机制虽然可以有效地防止CSRF攻击,但也不是万无一失的。以下是一些需要注意的安全问题:

  • $action参数的选择: $action参数应该尽可能地具有唯一性,避免与其他操作冲突。
  • nonce的保存位置: nonce应该保存在安全的地方,例如隐藏的表单字段或URL参数中。不要将nonce保存在cookie中,因为攻击者可以轻易地获取cookie。
  • nonce的有效时间: nonce的有效时间应该尽可能地短,以减少攻击者利用nonce的机会。
  • 使用HTTPS: 使用HTTPS可以防止中间人攻击,保证nonce的安全性。

一些常见问题及解答(Q&A)

问题 解答
wp_noncewp_kses有什么区别? wp_nonce主要用于防止CSRF攻击,它生成和验证一次性使用的令牌。wp_kses主要用于过滤用户输入,防止XSS攻击。它们的作用不同,但都是WordPress安全机制的重要组成部分。
wp_nonce的有效期是多久? 默认情况下,wp_nonce的有效期是12小时。你可以使用nonce_life过滤器来修改nonce的有效期。
如果用户未登录,wp_nonce会如何工作? 如果用户未登录,wp_create_nonce()函数会返回一个基于时间戳和action的哈希值。$uid会是0,或者被插件修改。这意味着,未登录用户的wp_nonce的安全性会降低,因为攻击者可以更容易地预测nonce的值。因此,对于未登录用户的敏感操作,应该采取其他的安全措施。
如何防止nonce被重放攻击? wp_nonce本身就是为了防止重放攻击而设计的。由于nonce是基于时间戳生成的,并且每次使用后都会失效,因此攻击者无法重放nonce。但是,如果nonce的有效期过长,攻击者就有可能在nonce失效之前重放nonce。因此,nonce的有效期应该尽可能地短。
wp_nonce是否可以完全防止CSRF攻击? wp_nonce可以有效地防止CSRF攻击,但不能完全防止。如果攻击者能够获取用户的登录信息,或者找到其他的漏洞,就有可能绕过wp_nonce机制。因此,除了使用wp_nonce之外,还应该采取其他的安全措施,例如使用HTTPS、限制用户权限等。

总结

wp_nonce机制是WordPress安全体系中不可或缺的一部分。它通过生成和验证一次性使用的令牌,有效地防止了CSRF攻击。理解wp_nonce的原理和使用方法,可以帮助我们构建更安全的WordPress应用。

好了,今天的讲座就到这里。希望大家有所收获!如果有什么问题,欢迎提问。下次再见!

发表回复

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