修复常见安全漏洞

在本模块的第一课中,您学习了开发自定义插件或主题时应遵循的主要安全原则。

在本课中,您将学习如何在开发 WordPress 插件和主题时应用这些原则,通过修复一个编码不安全的表单提交插件来实践。

编码糟糕的表单提交插件

首先,下载 Learn Plugin Security 插件,并将其安装到您的本地开发环境中。

然后,在代码编辑器中打开主插件文件,查看代码。

  1. 在主插件 PHP 文件的顶部,设置了一些常量,这些常量在插件其他地方使用。前两个用于定义插件将用于重定向的页面别名。为了使插件功能正常工作,这些页面必须存在,且具有正确的别名。
  2. 接下来,在插件激活钩子上注册了一个回调函数。这会在数据库中创建一个自定义的 form_submissions 表。
  3. 之后,插件的管理 JavaScript 和前端样式 CSS 文件被加载。
  4. 接着,注册了一个短代码,用于在前端显示表单。
  5. 然后,一个回调函数被挂载到 wp 动作上,插件使用它来处理表单提交。
  6. 接下来,插件注册了一个管理子菜单,用于显示表单提交列表。
  7. 有一个函数,管理子菜单使用它从数据库中获取表单提交数据。
  8. 最后,有一个回调函数被挂载到 wp_ajax 函数上,这是插件用于从管理子菜单页面删除表单提交的 Ajax 端点。

管理 JavaScript 文件处理从提交页面删除表单提交的 Ajax 请求。

前端 style.css 文件在用户为短代码输入 class 属性时使用。默认颜色为红色,但用户可以将其更改为蓝色。它只是为表单添加了一个边框。

当短代码被添加到文章或页面时,它会渲染表单,用户可以提交他们的详细信息。表单提交后,会根据提交是否成功重定向到成功或错误页面。然后在仪表盘中,管理员可以查看表单提交,并使用 Ajax 删除提交。

SQL 注入

我们要查找的第一个常见漏洞是 SQL 注入。

当输入的值没有被正确清理,允许使用输入数据的任何 SQL 命令可能在数据库上执行时,就会发生 SQL 注入。

我们需要解决潜在 SQL 注入漏洞的第一个地方是在 wp_learn_maybe_process_form 函数中。

function wp_learn_maybe_process_form() {
    if (!isset($_POST['wp_learn_form'])){
        return;
    }
    $name = $_POST['name'];
    $email = $_POST['email'];

    global $wpdb;
    $table_name = $wpdb->prefix . 'form_submissions';

    $sql = "INSERT INTO $table_name (name, email) VALUES ('$name', '$email')";
    $result = $wpdb->query($sql);
    if ( 0 < $result ) {
        wp_redirect( WPLEARN_SUCCESS_PAGE_SLUG );
        die();
    }

    wp_redirect( WPLEARN_ERROR_PAGE_SLUG );
    die();
}
  1. 我们需要确保在查询中使用任何 $_POST 数据之前,先对其进行清理。
  2. 我们需要使用 wpdb 的 prepare 或 insert 函数。
function wp_learn_maybe_process_form() {
    if (!isset($_POST['wp_learn_form'])){
        return;
    }
    $name = sanitize_text_field($_POST['name']);
    $email = sanitize_email($_POST['email']);

    global $wpdb;
    $table_name = $wpdb->prefix . 'form_submissions';

    $rows = $wpdb->insert(
        $table_name,
        array(
            'name' => $name,
            'email' => $email,
        )
    );
    if ( 0 < $rows ) {
        wp_redirect( WPLEARN_SUCCESS_PAGE_SLUG );
        die();
    }

    wp_redirect( WPLEARN_ERROR_PAGE_SLUG );
    die();
}

这将确保名称和电子邮件字段值在从表单提交请求中接收时都被清理,并且在用于将记录存储到数据库之前也被清理。虽然这看起来可能有些过度,但如果您只清理输入,而代码后来被更改为以不同方式使用这些值,您仍然可能容易受到 SQL 注入攻击。

另一个需要防止 SQL 注入的地方是在 wp_learn_delete_form_submission 函数中。

  1. 我们需要确保在查询中使用任何 $_POST 数据之前,先对其进行清理。
  2. 我们需要使用 wpdb 的 prepare 或 delete 函数。
function wp_learn_delete_form_submission() {
    $id = (int) $_POST['id'];
    global $wpdb;
    $table_name = $wpdb->prefix . 'form_submissions';

    $rows_deleted = $wpdb->delete( $table_name, array( 'id' => $id ) );
    if ( 0 < $rows_deleted ) {
        $result = 'success';
    } else {
        $result = 'error';
    }
    return wp_send_json( array( 'result' => $result ) );
}

由于这是一个整数,我们可以使用 PHP 类型转换功能来确保它始终被转换为整数。

跨站脚本攻击 (XSS) (3:47)

我们要查找的下一个常见漏洞是跨站脚本攻击 (XSS)。

当恶意方将 JavaScript 注入到网页中时,就会发生跨站脚本攻击 (XSS),这可用于从网站发起多种不同的攻击或恶意活动。

您可以通过转义输出、剥离不需要的数据来避免 XSS 漏洞。您的代码应根据要转义的内容类型,使用适当的函数转义动态内容。

让我们看看一些输出数据的地方,并确保它们被正确转义。

第一个地方是在 wp_learn_form_shortcode 短代码回调的包装 div 中。

<div id="wp_learn_form" class="<?php echo $atts['class'] ?>">

在这里,div 的 class 属性是根据传递给短代码的属性渲染的。这是一个潜在的 XSS 漏洞,因为 class 属性没有被转义。

<div id="wp_learn_form" class="<?php echo esc_attr( $atts['class'] ) ?>">

请注意,您应该专门使用 esc_attr 函数来转义 HTML 属性。

接下来,我们有 wp_learn_render_admin_page 函数,它用于渲染管理页面。

function wp_learn_render_admin_page(){
    $submissions = wp_learn_get_form_submissions();
    ?>
    <div class="wrap" id="wp_learn_admin">
        <h1>Admin</h1>
        <table>
            <thead>
                <tr>
                    <th>Name</th>
                    <th>Email</th>
                </tr>
            </thead>
            <?php foreach ($submissions as $submission){ ?>
                <tr>
                    <td><?php echo $submission->name?></td>
                    <td><?php echo $submission->email?></td>
                    <td><a class="delete-submission" data-id="<?php echo $submission->id?>" style="cursor:pointer;">Delete</a></td>
                </tr>
            <?php } ?>
        </table>
    </div>
    <?php
}

这里,$submission->name、$submission->email 和 $submission->id 应该被转义。

<?php foreach ($submissions as $submission){ ?>
    <tr>
        <td><?php echo esc_html($submission->name)?></td>
        <td><?php echo esc_html($submission->email)?></td>
        <td><a class="delete-submission" data-id="<?php echo (int) $submission->id?>" style="cursor:pointer;">Delete</a></td>
    </tr>
<?php } ?>

在这个示例中,你可以使用 esc_html,因为当 HTML 元素包含要显示的数据段时,这是正确的函数。务必密切关注每个转义函数的作用,因为有些会移除 HTML,而有些则允许保留。

你必须根据要输出的内容和上下文,选择最合适的函数。

最后,你将 ID 转换为整数,因为它被用于数据属性中。

跨站请求伪造 (CSRF)

下一个需要防范的漏洞是跨站请求伪造。CSRF 是指恶意方诱骗用户在他们已认证的 Web 应用中执行非预期的操作。

在使用 WordPress 开发时,熟悉 WordPress nonce 是帮助防范 CSRF 的必备技能。

Nonce 是“一次性使用的数字”,它提供了一种验证请求来源是否合法的方式。

  1. 当你需要验证请求是否合法时,创建一个 nonce。
  2. 你将 nonce 输出或传递给需要发起请求的地方。
  3. 当请求发起时,你验证该 nonce。

这个插件中存在两个可能的 CSRF 漏洞。

第一个漏洞出现在表单提交并处理数据时。要修复此问题,我们需要在短代码渲染的表单中添加一个 nonce,然后在表单提交时验证它。

在表单中,我们使用 wp_nonce_field 函数来添加一个包含 nonce 的隐藏字段。

<?php
wp_nonce_field( 'wp_learn_form_nonce_action', 'wp_learn_form_nonce_field' );
?>

注意你是如何传入一个 action 和一个 name 的。action 用于标识 nonce,而 name 是添加到表单中的字段名称。

如果你检查表单,可以看到 nonce 字段,它使用了你传递给函数的名称以及 nonce 值。

然后在表单提交函数中,你使用 wp_verify_nonce 函数来验证 nonce,传入 nonce 字段的值和 action。

    /**
     * 04 (b). Verify the nonce
     * https://developer.wordpress.org/apis/security/nonces/
     */
    if ( ! isset( $_POST['wp_learn_form_nonce_field'] ) || ! wp_verify_nonce( $_POST['wp_learn_form_nonce_field'], 'wp_learn_form_nonce_action' ) ) {
        wp_redirect( WPLEARN_ERROR_PAGE_SLUG );
        die();
    }

这里我们检查 nonce 字段是否已在请求中传递,然后验证 nonce 是否有效。如果 nonce 未传递或无效,我们将重定向到错误页面。

另一个需要防范 CSRF 的地方是用于删除表单提交数据的 Ajax 回调。

function wp_learn_delete_form_submission() {
    $id = (int) $_POST['id'];
    global $wpdb;
    $table_name = $wpdb->prefix . 'form_submissions';

    $rows_deleted = $wpdb->delete( $table_name, array( 'id' => $id ) );
    if ( 0 < $rows_deleted ) {
        $result = 'success';
    } else {
        $result = 'error';
    }
    return wp_send_json( array( 'result' => $result ) );
}

要修复此问题,首先我们需要使用 wp_create_nonce 手动创建一个 nonce,然后使用 wp_localize_script 将 nonce 传递给 JavaScript 层。

  /**
   * 04 (a). Add an ajax nonce to the script
   * https://developer.wordpress.org/apis/security/nonces/
   */
  $ajax_nonce = wp_create_nonce( 'wp_learn_ajax_nonce' );
  wp_localize_script(
      'wp-learn-admin',
      'wp_learn_ajax',
      array(
          'ajax_url' => admin_url( 'admin-ajax.php' ),
          'nonce'    => $ajax_nonce,
      )
  );

然后,我们需要在 jQuery POST 请求中包含该 nonce。

jQuery.post(
    wp_learn_ajax.ajax_url,
    {
        '_ajax_nonce': wp_learn_ajax.nonce,
        'action': 'delete_form_submission',
        'id': id,
    },
    function (response) {
        console.log( response );
        alert( 'Form submission deleted' );
        document.location.reload();
    }
);

注意我们如何在 POST 请求中将 nonce 指定为 _ajax_nonce。这是 WordPress 在处理 Ajax 请求时预期的 nonce 名称。

最后,在 Ajax 回调中,我们使用方便的 check_ajax_referrer 函数来验证 nonce。

    check_ajax_referer( 'wp_learn_ajax_nonce' );

你会看到传递给 check_ajax_referer 的字符串与我们创建 nonce 时传递给 wp_create_nonce 的字符串相同。

如果 check_ajax_referrer 失败,它将导致执行停止,因此我们不需要检查该函数的结果。

访问控制失效

这个插件中还有一个漏洞,即访问控制失效。BAC 是指用户能够访问他们本不应访问的资源。例如,用户可能能够访问管理员功能,即使他们不是管理员。

在我们的示例中,Ajax 函数存在一个访问控制失效漏洞。目前,任何人都可以使用正确的数据向 Ajax 请求 URL 发起请求,从而删除一个 form_submission。

要修复此问题,我们可以使用 WordPress 角色和权限 API 来检查用户是否具有删除表单提交数据的正确权限。在这种情况下,只需检查用户是否为管理员用户即可。

    if ( ! current_user_can( 'manage_options' ) ) {
        return wp_send_json( array( 'result' => 'Authentication error' ) );
    }

注意我们在这里进行了两次检查,一次针对 CSRF,一次针对访问控制。在这个示例中,执行顺序不是特别重要,但通常来说,先检查 CSRF,再检查访问控制是一个好习惯。

附加题 – 开放重定向

这个插件中还有一个额外的安全漏洞。你能找到它吗?

这是一个很难发现的漏洞,但所有使用 wp_redirect 的地方都应替换为 wp_safe_redirect。这是因为代码正在重定向到本地 URL,而 wp_safe_redirect 会检查其使用的 $location 在具有绝对路径时是否为允许的主机。这可以防止如果重定向 $location 受到攻击时可能发生的恶意重定向。

进一步阅读

要了解更多关于修复 WordPress 代码中常见漏洞的信息,请务必收藏 WordPress 开发者文档安全部分中关于常见漏洞的页面,以及包含权限检查、数据验证、安全输入、安全输出和 nonce 的示例。