WordPress插件开发教程手册 — 插件安全

恭喜,你的代码通过了能力测试,但是插件代码是安全的吗?如果用户的网站被黑客盯上了,插件如何保护用户不被攻击,WordPress.org 插件目录中的插件在安全方面都做了很多工作,以确保用户的信息安全。

请记住,你的代码可能会在数百万个 WordPress 站点上运行,因此,安全性至关重要。在本章中,我们将介绍如何检查用户能力,验证输入、清理输入,清理输出,以及创建和验证随机数。

快速参考

查看 WordPress插件和主题的安全最佳实践的完整示例

外部资源

检查用户能力

如果你的插件允许用户提交数据(无论是管理员还是游客),一定要检查用户权限。

用户角色和能力

创建一个高效安全防护的重要步骤之一是建立一个用户能力系统,WordPress 以用户角色和能力的形式提供了这个能力。

每个登录到 WordPress 的用户都会根据其角色自动分配对应的能力。

用户角色其实就是用户分组的一种形象的说法,每个用户分组都有一些特定的预定义能力。

例如,网站主用户会有管理员角色,其他用户有“编辑”或“作者”角色,我们可以为一个角色分配多个用户,也就是说,一个 WordPress 站点可以有多个管理员。

用户能力是指给每个用户或用户角色指定的特定权限。

例如,管理员拥有 “manage_options” 能力,这个能力让管理员有权限查看、编辑、保存网站选项。而编辑或其他用户没有这个能力,就不能进行这些操作。

WordPress 会根据分配给角色的能力,在后台的各个位置检查用户能力,菜单,功能和 WordPres 的其他部分会根据这些检查结果被添加或删除。

开发插件时,确保当前用户的能力进行检查,只有用户具有了必需能力时,才运行代码。

用户等级制度

用户角色越高,能力就越多,每个角色都会继承等级中的低一级的角色的所有能力。

例如,WordPress 单站点中的能力最大的“管理员角色”会自动拥有“订阅者”,“投稿者”,“作者”和“编辑”的所有能力。

示例

没有限制

下面的例子在前端创建了一个链接,允许用户把文章移至回收站。因为没有检查用户能力,下面的代码允许所有访问网站的用户使用这个链接,把站点的文章移至回收站。

<?php
/**
 * 在前端创建一个删除文章的链接
 */
function wporg_generate_delete_link($content)
{
    // 只在单文章页面运行
    if (is_single() && in_the_loop() && is_main_query()) {
        // 添加查询参数: action, post
        $url = add_query_arg(
            [
                'action' => 'wporg_frontend_delete',
                'post'   => get_the_ID(),
            ],
            home_url()
        );
        return $content . ' <a href=' . esc_url($url) . '>' . esc_html__('Delete Post', 'wporg') . '</a>';
    }
    return null;
}
 
/**
 * 处理用户请求
 */
function wporg_delete_post()
{
    if (isset($_GET['action']) && $_GET['action'] === 'wporg_frontend_delete') {
 
        // 检查请求中是否有文章参数
        $post_id = (isset($_GET['post'])) ? ($_GET['post']) : (null);
 
        // 检查请求的文章是否存在
        $post = get_post((int)$post_id);
        if (empty($post)) {
            return;
        }
 
        // 删除文章
        wp_trash_post($post_id);
 
        // 跳转到文章管理界面
        $redirect = admin_url('edit.php');
        wp_safe_redirect($redirect);
 
        // 退出
        die;
    }
}
 
/**
 * 添加删除文章的链接到文章内容后面
 */
add_filter('the_content', 'wporg_generate_delete_link');
 
/**
 * WordPress 初始化时注册请求处理函数
 */
add_action('init', 'wporg_delete_post');

限制到指定的能力

上面的例子允许访问站点的任何用户点击“删除”文章的链接把文章移至回收站,我们需要的是,只有拥有编辑能力的用户可以看到这个链接,才允许删除文章,其他用户即便知道了这个链接,手动访问了,也没用。

我们把上面的代码稍微修改一下,在显示链接时检查用户是否拥有 “edit_others_posts” 能力,WordPress 默认的角色能力系统中,只有编辑以上角色的用户才拥有这个能力。

<?php

if (current_user_can('edit_others_posts')) {
    /**
     * 添加删除文章的链接到文章内容后面
     */
    add_filter('the_content', 'wporg_generate_delete_link');
 
    /**
     * WordPress 初始化时注册请求处理函数
     */
    add_action('init', 'wporg_delete_post');
}

数据验证

数据验证是指根据预定义的一个或多个规则分析数据的过程,数据验证结果只有两个,有效或无效。数据验证经常用于处理外部传输进来的数据,如用户输入和通过 API 调用的 Web 服务数据。

数据验证的简单例子:

  • 检查必须字段是否为空
  • 检查输入的电话号码是否只包含数字和符号
  • 检查邮编是否为有效的邮政编码
  • 检查数量字段是否大于 0
  • 检查 Email 是否为有效的 Email 地址

数据验证应尽早执行,也就是说,我们要在执行其他操作此前,先验证数据的有效性。

我们可以在前端使用 JavaScript 验证,在后端使用 PHP 验证,前端验证不能代替后端验证。

验证数据

在 WordPress 中,我们至少有 3 种方法可以验证数据:PHP 内置函数, WordPress 核心函数和你自己编写的函数。

内置 PHP 函数

基本数据验证可以使用许多 PHP 内置函数来执行,包括:

  • isset() 和 empty() 可以用来检查一个变量是否存在
  • mb_strlen() 或者 strlen() 可以用来检查一个字符串长度是否符合要求
  • preg_match(),strpos() 用于检查字符串中是否包含某些指定的字符串
  • count() 用于检查数组中有多少元素
  • in_array() 用于检查数组中是否存在某元素

WordPress 核心验证函数

WordPress 为我们提供了很多函数来帮助我们验证各种类型的数据,例如:

  • is_email() 验证电子邮件地址是否有效。
  • term_exists() 检查分类项目是否存在。
  • username_exists() 检查用户名是否存在
  • validate_file() 检查输入的文件路径是不是一个真实的路径(不是检查文件是否存在)

我们可以在 WordPress 代码参考中搜索类似 *_exists()*_validate(),和  is_*() 这样的名称来查找其他数据验证函数,并非所有包含这些名称的函数都是数据验证函数,但有很大一部分都可以帮助我们验证数据。

自定义 PHP 和 JavaScript 函数

我们可以编写自己的 PHP 和 JavaScript 函数,把这些函数包含在我们的主题或插件中,编写自定义验证函数时,我们可以根据语义化规则命名这些函数,如 is_phone, is_avaliable, is_zipcode,等等。

这些验证函数应该返回一个布尔值,根据验证是否通过返回 true 或者 fase,我们可以把这些函数直接作为 if 的判断条件使用。

示例 1

假设我们需要验证一个用户提交的美国邮编是否正确。

<input id=wporg_zip_code type=text maxlength=10 name=wporg_zip_code>

上面的文本字段最多允许输入 10 个字符,对于可以输入的字符类型没有任何限制,用户可以输入一些有效的 1234567890,或者其他无效或不怀好意的东西。

input 的 maxlength 验证属性由浏览器执行,如果浏览器不支持这个属性,这个验证条件就不会执行,或者在到服务器之前,用户可以对数据进行一些修改。所以,即便在前端进行了验证,我们依然需要在服务器上验证数据的长度。

通过验证,我们可以确保只接受有效的美国邮政编码。首先,我们需要编写一个函数来验证美国邮政编码。

<?php 
function is_us_zip_code($zip_code) { 
    // 验证 1: 是否为空 
    if (empty($zip_code)) { 
        return false; 
    } 

    // 验证 2: 是否多于 10 个字符
    if (strlen(trim($zip_code)) > 10) {
        return false;
    }
 
    // 验证 3: 数据格式是否正确
    if (!preg_match('/^\d{5}(\-?\d{4})?$/', $zip_code)) {
        return false;
    }
 
    // 验证如果,返回 true
    return true;
}

处理表单时,我们的代码应该首先检查 wporg_zip_code 字段的正确性,然后根据验证结果执行操作。

if (isset($_POST['wporg_zip_code']) && is_us_zip_code($_POST['wporg_zip_code'])) {
    // 需要执行的操作
}

示例2

假设我们需要查询数据库中的某些文章,并且可以让用户排序查询结果。

下面的示例代码通过使用 PHP 内置的函数 in_array 将传入的排序键(存储在 order_by 参数中)和一个允许排序的键数组进行比较来检查传入的值是否可以排序,这样可以防止用户传入恶意的数据来危害网站。

在与键数组比较之前,我们先使用 WordPress 的内置函数 sanitize_key 来处理用户输入,此函数确保键为小写字母(in_array 函数区分大小写)。

将 “true” 设置为 in_array 的第三个参数,告诉 in_array 进行严格的类型检查,不仅要比较值,也要比较值的类型,这样可以确保传入的是一个字符串而不是其他数据类型。

<?php
$allowed_keys = ['author', 'post_author', 'date', 'post_date'];
 
$orderby = sanitize_key($_POST['orderby']);
 
if (in_array($orderby, $allowed_keys, true)) {
    // 修改查询,根据 order_by 键来排序
}

安全输入

保证安全输入是对用户输入的数据进行清理(净化、过滤)的过程。如果我们不知道具体的数据类型,或者不希望进行严格的数据验证,我们可以使用数据消毒措施。

任何时候我们都可能接受不安全的数据,所以,对数据进行消毒和验证很重要。

清理数据

清理数据最简单的方法是使用 WordPress 的内置函数。

WordPress 为我们提供了一些 sanitize_*() 辅助函数来帮助我们确保最终获得安全的数据,使用这些函数,我们可以很轻松的对数据进行消毒。

示例

假设我们有一个名为title的输入字段。

<input id=title type=text name=title>

我们可以使用 sanitize_text_field() 函数清理输入数据:

$title = sanitize_text_field($_POST['title']);
update_post_meta($post->ID, 'title', $title);

在幕后,sanitize_text_field() 执行以下操作:

  • 检查无效的 UTF-8 字符
  • 将大于小于字符(><)转换为实体
  • 删除所有 HTML 标签
  • 删除换行符,制表符和额外的空白字符
  • 删除所有 8 字节字符

安全输出

安全输出是转义数据输出的过程。

转义也就是要删除不需要的数据,比如格式错误的 HTML 或脚本标记。

无论何时,渲染数据时,都要确保将其转义,转义输出可以防止 XSS(跨站脚本)攻击。

跨站点脚本(XSS)是一种 Web 应用程序中常见的计算机安全漏洞。XSS 使攻击者可以将客户端脚本注入到其他用户查看的网页中。攻击者可能会使用跨站脚本漏洞绕过访问控制,如同源策略。

转义

转义有助于在最终呈现数据给用户之前,保护我们的数据,WordPress 提供了几个辅助函数,可以帮助我们处理大多数需要转义情况。

  • esc_html() – 在显示 HTML 时,使用此函数。
  • esc_url() – 在输出 URL 时,使用此函数,包括在srchref属性中的 URL。
  • esc_js() – 对内联 JavaScript 使用此函数。
  • esc_attr() – 把数据设置为 HTML 元素属性时使用此能力。
大多数 WordPress 函数都能正确地输出数据,所以我们不需要再次转义数据。例如,我们可以安全地调用 the_title() 而不用转义。

使用本地化函数

相对于使用 echo 输出数据,我们应该更多的使用 WordPress 的本地化能力,如 _e() 或 __()

下面的本地化函数 esc_html_e 函数集成了数据转义的能力。

esc_html_e( 'Hello World', 'text_domain' );
// 等同于
echo esc_html( __( 'Hello World', 'text_domain' ) );

结合了本地化和转义能力的函数有:

自定义转义

在需要以特殊方式转义输出的情况下,我们我们可以使用函数 wp_kses() 来实现自定义转义能力,此函数确保只有指定的 HTML元素、属性和属性值会出现在输出中,并对HTML实体进行规范化。

$allowed_html = [
    'a'      => [
        'href'  => [],
        'title' => [],
    ],
    'br'     => [],
    'em'     => [],
    'strong' => [],
];
echo wp_kses( $custom_content, $allowed_html );

wp_kses_post() 是 wp_kses 函数的封装,其中$allowed_html是显示文章内容使用的一组规则。

echo wp_kses_post( $post_content );

随机数验证

出于安全的目的,随机数验证可以帮助我们验证请求的来源和意图,每个随机数只能使用一次。

如果我们的插件允许用户提交数据,无论是管理员还是游客,我们必须保证用户是执行操作的那个人,并且他们拥有执行该操作的能力,这两者一起验证,确保了只有在用户希望发生变化时,数据才会发生变化。

使用随机数

检查了用户的能力示例之后,增强用户提交数据安全的下一步是使用随机数验证。用户能力检查确保只有有删除文章能力的用户才能删除文章,但是如果有人欺骗你点击了那个链接呢?你有能力删除文章,但是你没有意图,却在不知不觉之间删除了文章。

我们可以使用随机数来检查当前用户打算执行的操作是否为用户的真实意图。在生成链接时,我们可以使用 wp_create_nonce() 函数添加一个随机数到链接,传递给该函数的参数确保创建的随机数对于该操作是唯一的。

然后,在处理删除请求时,我们可以检查该随机数是否与我们期望的一致。

关于随机数的更多信息,可以看一下 Mark Jaquith 的关于 WordPress 随机数 的文章 ,这是一个不错的资源。

完整的示例

下面是使用能力检查、数据验证、安全输入、安全输入和随机数验证的完整示例。

<?php
/**
 * 在前端创建一个删除文章的链接
 */
function wporg_generate_delete_link($content){
   // 只在单文章页面运行
   if (is_single() && in_the_loop() && is_main_query()) {
      // 添加查询参数: action, post
      $url = add_query_arg(
         [
            'action' => 'wporg_frontend_delete',
            'post'   => get_the_ID(),
            'nonce'  => wp_create_nonce('wporg_frontend_delete'),
         ],
         home_url()
      );
      return $content . ' <a href=' . esc_url($url) . '>' . esc_html__('Delete Post', 'wporg') . '</a>';
   }
   return null;
}
 
/**
 * 处理用户请求
 */
function wporg_delete_post(){
   if (
      isset($_GET['action']) &&
      isset($_GET['nonce']) &&
      $_GET['action'] === 'wporg_frontend_delete' &&
      wp_verify_nonce($_GET['nonce'], 'wporg_frontend_delete')
   ) {

      // 检查请求中是否有文章参数
      $post_id = (isset($_GET['post'])) ? ($_GET['post']) : (null);

      // 检查请求的文章是否存在
      $post = get_post((int)$post_id);
      if (empty($post)) {
         return;
      }

      // 删除文章
      wp_trash_post($post_id);

      // 跳转到文章管理界面
      $redirect = admin_url('edit.php');
      wp_safe_redirect($redirect);

      // 退出
      die;
   }
}
 
if (current_user_can('edit_others_posts')) {
   /**
    * 添加删除文章的链接到文章内容后面
    */
   add_filter('the_content', 'wporg_generate_delete_link');

   /**
    * WordPress 初始化时注册请求处理函数
    */
   add_action('init', 'wporg_delete_post');
}

我们提供 WordPress主题和插件定制开发服务

本站长期承接 WordPress主题、插件、基于 WooCommerce 的商店商城开发业务。 我们有 10 年WordPress开发经验,如果你想 用WordPress开发网站, 请联系微信: iwillhappy1314,或邮箱: amos@wpcio.com 咨询。

发表回复

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

*