WordPress源码深度解析之:`WordPress`的`CSRF`防御:`wp_nonce`字段的生成和验证机制。

各位观众老爷们,大家好!我是你们的老朋友,今天咱们来聊聊WordPress的安全问题,特别是那个神出鬼没,又不得不防的CSRF(Cross-Site Request Forgery)攻击。要说CSRF,它就像个偷偷摸摸的小贼,专门利用你已经登录的身份,在你不注意的时候干坏事。而WordPress为了保护大家的饭碗,祭出了一个法宝——wp_nonce。今天我们就来扒一扒这wp_nonce的底裤,看看它是怎么生成,又是怎么验证的,以及我们作为开发者,该怎么正确地使用它。

一、什么是CSRF,为什么要防它?

先来简单回顾一下CSRF。假设你登录了银行网站,正在浏览账户余额。这时,一个邪恶的网站给你发来了一个链接,链接里藏着这样的代码:

<img src="http://yourbank.com/transfer.php?account=hacker&amount=1000">

如果你不小心点了这个链接,浏览器会带着你的银行cookie去访问transfer.php,如果银行没有做CSRF防护,那么你的账户就可能被偷偷转走了1000块!是不是想想就后背发凉?

这就是CSRF攻击的原理:利用用户的登录态,伪造请求。

所以,防止CSRF攻击至关重要。

二、wp_nonce:WordPress的防盗门

wp_nonce,全称WordPress Nonce, Nonce是个缩写,意思是“Number used once”,也就是“一次性使用的数字”。它的作用就像一个随机生成的令牌,每次请求都会带上这个令牌,服务器验证令牌是否正确,以此来判断请求是否合法。

简单来说,wp_nonce就像一个防盗门上的密码,只有知道密码的人才能进入,否则一律拒之门外。

三、wp_nonce的生成:wp_create_nonce()函数

WordPress提供了wp_create_nonce()函数来生成wp_nonce。这个函数接受一个字符串参数,通常是你想保护的操作的标识符(action)。

$action = 'my_custom_action';
$nonce = wp_create_nonce( $action );

echo $nonce; // 输出类似: 7a3b9c2d1e

这段代码生成了一个针对my_custom_actionwp_nonce。 每次刷新页面,你会发现这个nonce的值都会变化,因为它真的是一次性的。

wp_create_nonce()函数内部做了什么?

让我们深入wp-includes/pluggable.php看看wp_create_nonce()的真面目。 为了方便理解,我简化了代码:

function wp_create_nonce( $action = -1 ) {
    $time = time();
    $i = floor( $time / ( DAY_IN_SECONDS / 2 ) ); // 12小时更新一次
    $nonce_key = wp_nonce_tick();
    return substr( hash_hmac( 'md5', $i . '|' . $action . '|' . get_current_user_id() . '|' . $nonce_key, wp_hash( get_site_url(), 'nonce' ) ), -12, 10 );
}

function wp_nonce_tick() {
    $nonce_life = apply_filters( 'nonce_life', DAY_IN_SECONDS );
    return ceil( time() / ( $nonce_life / 2 ) );
}

核心逻辑:

  1. 获取时间戳: time()获取当前时间戳。
  2. 计算时间片: floor( $time / ( DAY_IN_SECONDS / 2 ) ) 将时间戳除以12小时(DAY_IN_SECONDS / 2),并向下取整,得到一个时间片。这意味着nonce每12小时更新一次。
  3. 计算nonce_key: wp_nonce_tick() 计算一个基于时间的nonce_key,默认的lifetime是24小时,所以wp_nonce_tick()每12小时返回一个值。
  4. 生成哈希: hash_hmac() 使用HMAC-MD5算法,将时间片、action、用户ID、nonce_key 和一个站点唯一的密钥(通过wp_hash()生成)进行哈希。
  5. 截取哈希值: substr() 截取哈希值的后12位,作为最终的wp_nonce

重点:

  • 时间因素: wp_nonce与时间有关,因此具有时效性。
  • 用户ID: wp_nonce与当前用户ID有关,不同的用户生成的nonce不同。
  • Action: wp_nonce与action有关,不同的action生成的nonce不同。
  • 站点密钥: wp_nonce与站点唯一的密钥有关,不同的站点生成的nonce不同。

四、wp_nonce的使用:在表单中加入wp_nonce

生成了wp_nonce,下一步就是把它放到表单里,让它随着表单一起提交。

<form action="process_form.php" method="post">
    <input type="hidden" name="my_custom_field" value="some_value">
    <?php wp_nonce_field( 'my_custom_action', 'my_custom_nonce' ); ?>
    <input type="submit" value="Submit">
</form>

wp_nonce_field()函数的作用是生成一个隐藏的input字段,包含wp_nonce。它接受两个参数:

  • action:wp_create_nonce()中的action保持一致。
  • name: wp_nonce字段的名称,默认为_wpnonce

这段代码会生成类似这样的HTML:

<input type="hidden" id="my_custom_nonce" name="my_custom_nonce" value="7a3b9c2d1e">

五、wp_nonce的验证:wp_verify_nonce()函数

表单提交后,服务器需要验证wp_nonce是否正确。WordPress提供了wp_verify_nonce()函数来完成这个任务。

if ( isset( $_POST['my_custom_nonce'] ) && wp_verify_nonce( $_POST['my_custom_nonce'], 'my_custom_action' ) ) {
    //  验证通过,处理表单数据
    echo '验证通过!';
} else {
    // 验证失败,拒绝请求
    echo '验证失败!';
    wp_die( '安全检查失败,请重试!' );
}

wp_verify_nonce()函数接受两个参数:

  • nonce: 从POST或GET请求中获取的wp_nonce值。
  • action: 与生成wp_nonce时的action保持一致。

wp_verify_nonce()函数返回:

  • 1: 验证通过,且wp_nonce是在0-12小时内生成的。
  • 2: 验证通过,且wp_nonce是在12-24小时内生成的(说明用户可能过了很久才提交表单)。
  • false: 验证失败。

wp_verify_nonce()函数内部做了什么?

让我们再深入wp-includes/pluggable.php看看wp_verify_nonce()的真面目。 同样,我简化了代码:

function wp_verify_nonce( $nonce, $action = -1 ) {
    $nonce = (string) $nonce;
    $nonce_life = apply_filters( 'nonce_life', DAY_IN_SECONDS );

    $time = time();
    $i = floor( $time / ( $nonce_life / 2 ) );

    $nonce_key = wp_nonce_tick();

    //检查当前时间片的nonce
    $expected = substr( hash_hmac( 'md5', $i . '|' . $action . '|' . get_current_user_id() . '|' . $nonce_key, wp_hash( get_site_url(), 'nonce' ) ), -12, 10 );
    if ( hash_equals( $expected, $nonce ) ) {
        return 1;
    }

    //检查前一个时间片的nonce
    $expected = substr( hash_hmac( 'md5', ( $i - 1 ) . '|' . $action . '|' . get_current_user_id() . '|' . $nonce_key, wp_hash( get_site_url(), 'nonce' ) ), -12, 10 );
    if ( hash_equals( $expected, $nonce ) ) {
        return 2;
    }

    return false;
}

核心逻辑:

  1. 获取时间戳: time()获取当前时间戳。
  2. 计算时间片: floor( $time / ( DAY_IN_SECONDS / 2 ) ) 计算当前时间片。
  3. 生成预期的哈希值: 使用与生成wp_nonce相同的算法,生成预期的哈希值。
  4. 比较哈希值: 使用hash_equals()函数比较接收到的wp_nonce与预期的哈希值。 hash_equals()可以防止时序攻击,确保比较过程的安全性。
  5. 检查前一个时间片: 如果当前时间片的wp_nonce验证失败,则检查前一个时间片的wp_nonce,允许一定的延迟。

重点:

  • 双重验证: wp_verify_nonce()会验证当前时间片和前一个时间片的wp_nonce,允许一定的延迟,避免用户在12小时的交界处提交表单时出现问题。
  • 安全比较: 使用hash_equals()进行哈希值比较,防止时序攻击。

六、wp_nonce的最佳实践:

  1. 为每个操作使用不同的action: 不要对所有操作都使用同一个action,否则攻击者可以利用一个wp_nonce来伪造多个请求。

  2. 在表单中使用wp_nonce_field() wp_nonce_field()函数会自动生成隐藏的input字段,方便使用。

  3. 在处理表单数据前验证wp_nonce 确保在处理任何用户提交的数据之前,先验证wp_nonce

  4. 使用wp_die()或类似函数来终止无效的请求: 如果wp_nonce验证失败,不要继续处理请求,而是直接终止请求,并向用户显示错误信息。

  5. 不要在GET请求中使用wp_nonce GET请求容易被缓存和记录,wp_nonce可能会泄露。应该只在POST请求中使用wp_nonce

  6. 考虑使用nonce_life过滤器调整wp_nonce的有效期: 根据你的需求,可以调整wp_nonce的有效期。例如,对于一些需要高安全性的操作,可以将有效期缩短到几分钟。

    add_filter( 'nonce_life', function( $lifetime ) {
       return 60 * 60; // 1 小时
    });
  7. 对于AJAX请求,可以使用wp_localize_script()传递wp_nonce

    // 在你的主题或插件文件中
    wp_enqueue_script( 'my-ajax-script', get_template_directory_uri() . '/js/my-ajax-script.js', array( 'jquery' ) );
    wp_localize_script( 'my-ajax-script', 'my_ajax_object', array(
       'ajax_url' => admin_url( 'admin-ajax.php' ),
       'security' => wp_create_nonce( 'my_ajax_action' )
    ));
    
    // 在你的 JavaScript 文件中 (my-ajax-script.js)
    jQuery(document).ready(function($) {
       $('#my-button').click(function() {
           $.ajax({
               url: my_ajax_object.ajax_url,
               type: 'POST',
               data: {
                   action: 'my_ajax_action',
                   security: my_ajax_object.security,
                   // 其他数据
               },
               success: function(response) {
                   // 处理响应
               }
           });
       });
    });
    
    // 在你的 WordPress 函数文件中 (functions.php 或插件文件)
    add_action( 'wp_ajax_my_ajax_action', 'my_ajax_callback' );
    add_action( 'wp_ajax_nopriv_my_ajax_action', 'my_ajax_callback' ); // 如果未登录用户也需要访问
    
    function my_ajax_callback() {
       check_ajax_referer( 'my_ajax_action', 'security' ); // 验证 nonce
    
       // 处理 AJAX 请求
    
       wp_die(); // 结束 AJAX 请求
    }

    使用 check_ajax_referer() 函数来验证 AJAX 请求中的 nonce。

七、总结

wp_nonce是WordPress提供的一个简单而有效的CSRF防御机制。通过理解wp_nonce的生成和验证原理,并遵循最佳实践,我们可以有效地保护我们的WordPress网站免受CSRF攻击。

虽然wp_nonce可以防御CSRF攻击,但它并不是万能的。攻击者仍然可以通过其他手段绕过wp_nonce,例如XSS攻击。因此,我们还需要采取其他安全措施,例如输入验证、输出编码等,来全面保护我们的WordPress网站。

记住,安全是一个持续的过程,需要我们不断学习和更新。 希望今天的讲解能帮助大家更好地理解和使用wp_nonce,让我们的WordPress网站更加安全!

各位,今天的讲座就到这里,谢谢大家!

发表回复

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