WordPress插件開(kāi)發(fā)教程手冊(cè) — 插件安全

本文詳細(xì)介紹了如何開(kāi)發(fā)安全的WordPress插件,包括用戶(hù)能力檢查、輸入輸出驗(yàn)證和清理等關(guān)鍵安全措施。確保你的插件在數(shù)百萬(wàn)站點(diǎn)上運(yùn)行時(shí),能夠有效保護(hù)用戶(hù)數(shù)據(jù),防止黑客攻擊。

恭喜,你的代碼通過(guò)了能力測(cè)試,但是插件代碼是安全的嗎?如果用戶(hù)的網(wǎng)站被黑客盯上了,插件如何保護(hù)用戶(hù)不被攻擊,WordPress.org 插件目錄中的插件在安全方面都做了很多工作,以確保用戶(hù)的信息安全。

請(qǐng)記住,你的代碼可能會(huì)在數(shù)百萬(wàn)個(gè) WordPress 站點(diǎn)上運(yùn)行,因此,安全性至關(guān)重要。在本章中,我們將介紹如何檢查用戶(hù)能力,驗(yàn)證輸入、清理輸入,清理輸出,以及創(chuàng)建和驗(yàn)證隨機(jī)數(shù)。

快速參考

查看 WordPress插件和主題的安全最佳實(shí)踐的完整示例。

外部資源

檢查用戶(hù)能力

如果你的插件允許用戶(hù)提交數(shù)據(jù)(無(wú)論是管理員還是游客),一定要檢查用戶(hù)權(quán)限。

用戶(hù)角色和能力

創(chuàng)建一個(gè)高效安全防護(hù)的重要步驟之一是建立一個(gè)用戶(hù)能力系統(tǒng),WordPress 以用戶(hù)角色和能力的形式提供了這個(gè)能力。

每個(gè)登錄到 WordPress 的用戶(hù)都會(huì)根據(jù)其角色自動(dòng)分配對(duì)應(yīng)的能力。

用戶(hù)角色其實(shí)就是用戶(hù)分組的一種形象的說(shuō)法,每個(gè)用戶(hù)分組都有一些特定的預(yù)定義能力。

例如,網(wǎng)站主用戶(hù)會(huì)有管理員角色,其他用戶(hù)有“編輯”或“作者”角色,我們可以為一個(gè)角色分配多個(gè)用戶(hù),也就是說(shuō),一個(gè) WordPress 站點(diǎn)可以有多個(gè)管理員。

用戶(hù)能力是指給每個(gè)用戶(hù)或用戶(hù)角色指定的特定權(quán)限。

例如,管理員擁有 “manage_options” 能力,這個(gè)能力讓管理員有權(quán)限查看、編輯、保存網(wǎng)站選項(xiàng)。而編輯或其他用戶(hù)沒(méi)有這個(gè)能力,就不能進(jìn)行這些操作。

WordPress 會(huì)根據(jù)分配給角色的能力,在后臺(tái)的各個(gè)位置檢查用戶(hù)能力,菜單,功能和 WordPres 的其他部分會(huì)根據(jù)這些檢查結(jié)果被添加或刪除。

開(kāi)發(fā)插件時(shí),確保當(dāng)前用戶(hù)的能力進(jìn)行檢查,只有用戶(hù)具有了必需能力時(shí),才運(yùn)行代碼。

用戶(hù)等級(jí)制度

用戶(hù)角色越高,能力就越多,每個(gè)角色都會(huì)繼承等級(jí)中的低一級(jí)的角色的所有能力。

例如,WordPress 單站點(diǎn)中的能力最大的“管理員角色”會(huì)自動(dòng)擁有“訂閱者”,“投稿者”,“作者”和“編輯”的所有能力。

示例

沒(méi)有限制

下面的例子在前端創(chuàng)建了一個(gè)鏈接,允許用戶(hù)把文章移至回收站。因?yàn)闆](méi)有檢查用戶(hù)能力,下面的代碼允許所有訪問(wèn)網(wǎng)站的用戶(hù)使用這個(gè)鏈接,把站點(diǎn)的文章移至回收站。

<?php
/**
 * 在前端創(chuàng)建一個(gè)刪除文章的鏈接
 */
function wporg_generate_delete_link($content)
{
    // 只在單文章頁(yè)面運(yùn)行
    if (is_single() && in_the_loop() && is_main_query()) {
        // 添加查詢(xún)參數(shù): 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;
}
 
/**
 * 處理用戶(hù)請(qǐng)求
 */
function wporg_delete_post()
{
    if (isset($_GET['action']) && $_GET['action'] === 'wporg_frontend_delete') {
 
        // 檢查請(qǐng)求中是否有文章參數(shù)
        $post_id = (isset($_GET['post'])) ? ($_GET['post']) : (null);
 
        // 檢查請(qǐng)求的文章是否存在
        $post = get_post((int)$post_id);
        if (empty($post)) {
            return;
        }
 
        // 刪除文章
        wp_trash_post($post_id);
 
        // 跳轉(zhuǎn)到文章管理界面
        $redirect = admin_url('edit.php');
        wp_safe_redirect($redirect);
 
        // 退出
        die;
    }
}
 
/**
 * 添加刪除文章的鏈接到文章內(nèi)容后面
 */
add_filter('the_content', 'wporg_generate_delete_link');
 
/**
 * WordPress 初始化時(shí)注冊(cè)請(qǐng)求處理函數(shù)
 */
add_action('init', 'wporg_delete_post');

限制到指定的能力

上面的例子允許訪問(wèn)站點(diǎn)的任何用戶(hù)點(diǎn)擊“刪除”文章的鏈接把文章移至回收站,我們需要的是,只有擁有編輯能力的用戶(hù)可以看到這個(gè)鏈接,才允許刪除文章,其他用戶(hù)即便知道了這個(gè)鏈接,手動(dòng)訪問(wèn)了,也沒(méi)用。

我們把上面的代碼稍微修改一下,在顯示鏈接時(shí)檢查用戶(hù)是否擁有 “edit_others_posts” 能力,WordPress 默認(rèn)的角色能力系統(tǒng)中,只有編輯以上角色的用戶(hù)才擁有這個(gè)能力。

<?php

if (current_user_can('edit_others_posts')) {
    /**
     * 添加刪除文章的鏈接到文章內(nèi)容后面
     */
    add_filter('the_content', 'wporg_generate_delete_link');
 
    /**
     * WordPress 初始化時(shí)注冊(cè)請(qǐng)求處理函數(shù)
     */
    add_action('init', 'wporg_delete_post');
}

數(shù)據(jù)驗(yàn)證

數(shù)據(jù)驗(yàn)證是指根據(jù)預(yù)定義的一個(gè)或多個(gè)規(guī)則分析數(shù)據(jù)的過(guò)程,數(shù)據(jù)驗(yàn)證結(jié)果只有兩個(gè),有效或無(wú)效。數(shù)據(jù)驗(yàn)證經(jīng)常用于處理外部傳輸進(jìn)來(lái)的數(shù)據(jù),如用戶(hù)輸入和通過(guò) API 調(diào)用的 Web 服務(wù)數(shù)據(jù)。

數(shù)據(jù)驗(yàn)證的簡(jiǎn)單例子:

  • 檢查必須字段是否為空
  • 檢查輸入的電話號(hào)碼是否只包含數(shù)字和符號(hào)
  • 檢查郵編是否為有效的郵政編碼
  • 檢查數(shù)量字段是否大于 0
  • 檢查 Email 是否為有效的 Email 地址

數(shù)據(jù)驗(yàn)證應(yīng)盡早執(zhí)行,也就是說(shuō),我們要在執(zhí)行其他操作此前,先驗(yàn)證數(shù)據(jù)的有效性。

我們可以在前端使用 JavaScript 驗(yàn)證,在后端使用 PHP 驗(yàn)證,前端驗(yàn)證不能代替后端驗(yàn)證。

驗(yàn)證數(shù)據(jù)

在 WordPress 中,我們至少有 3 種方法可以驗(yàn)證數(shù)據(jù):PHP 內(nèi)置函數(shù), WordPress 核心函數(shù)和你自己編寫(xiě)的函數(shù)。

內(nèi)置 PHP 函數(shù)

基本數(shù)據(jù)驗(yàn)證可以使用許多 PHP 內(nèi)置函數(shù)來(lái)執(zhí)行,包括:

  • isset() 和 empty() 可以用來(lái)檢查一個(gè)變量是否存在
  • mb_strlen() 或者 strlen() 可以用來(lái)檢查一個(gè)字符串長(zhǎng)度是否符合要求
  • preg_match(),strpos() 用于檢查字符串中是否包含某些指定的字符串
  • count() 用于檢查數(shù)組中有多少元素
  • in_array() 用于檢查數(shù)組中是否存在某元素

WordPress 核心驗(yàn)證函數(shù)

WordPress 為我們提供了很多函數(shù)來(lái)幫助我們驗(yàn)證各種類(lèi)型的數(shù)據(jù),例如:

  • is_email() 驗(yàn)證電子郵件地址是否有效。
  • term_exists() 檢查分類(lèi)項(xiàng)目是否存在。
  • username_exists() 檢查用戶(hù)名是否存在
  • validate_file() 檢查輸入的文件路徑是不是一個(gè)真實(shí)的路徑(不是檢查文件是否存在)

我們可以在 WordPress 代碼參考中搜索類(lèi)似 *_exists(),*_validate(),和  is_*() 這樣的名稱(chēng)來(lái)查找其他數(shù)據(jù)驗(yàn)證函數(shù),并非所有包含這些名稱(chēng)的函數(shù)都是數(shù)據(jù)驗(yàn)證函數(shù),但有很大一部分都可以幫助我們驗(yàn)證數(shù)據(jù)。

自定義 PHP 和 JavaScript 函數(shù)

我們可以編寫(xiě)自己的 PHP 和 JavaScript 函數(shù),把這些函數(shù)包含在我們的主題或插件中,編寫(xiě)自定義驗(yàn)證函數(shù)時(shí),我們可以根據(jù)語(yǔ)義化規(guī)則命名這些函數(shù),如 is_phone, is_avaliable, is_zipcode,等等。

這些驗(yàn)證函數(shù)應(yīng)該返回一個(gè)布爾值,根據(jù)驗(yàn)證是否通過(guò)返回 true 或者 fase,我們可以把這些函數(shù)直接作為 if 的判斷條件使用。

示例 1

假設(shè)我們需要驗(yàn)證一個(gè)用戶(hù)提交的美國(guó)郵編是否正確。

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

上面的文本字段最多允許輸入 10 個(gè)字符,對(duì)于可以輸入的字符類(lèi)型沒(méi)有任何限制,用戶(hù)可以輸入一些有效的 1234567890,或者其他無(wú)效或不懷好意的東西。

input 的 maxlength 驗(yàn)證屬性由瀏覽器執(zhí)行,如果瀏覽器不支持這個(gè)屬性,這個(gè)驗(yàn)證條件就不會(huì)執(zhí)行,或者在到服務(wù)器之前,用戶(hù)可以對(duì)數(shù)據(jù)進(jìn)行一些修改。所以,即便在前端進(jìn)行了驗(yàn)證,我們依然需要在服務(wù)器上驗(yàn)證數(shù)據(jù)的長(zhǎng)度。

通過(guò)驗(yàn)證,我們可以確保只接受有效的美國(guó)郵政編碼。首先,我們需要編寫(xiě)一個(gè)函數(shù)來(lái)驗(yàn)證美國(guó)郵政編碼。

<?php 
function is_us_zip_code($zip_code) { 
    // 驗(yàn)證 1: 是否為空 
    if (empty($zip_code)) { 
        return false; 
    } 

    // 驗(yàn)證 2: 是否多于 10 個(gè)字符
    if (strlen(trim($zip_code)) > 10) {
        return false;
    }
 
    // 驗(yàn)證 3: 數(shù)據(jù)格式是否正確
    if (!preg_match('/^\d{5}(\-?\d{4})?$/', $zip_code)) {
        return false;
    }
 
    // 驗(yàn)證如果,返回 true
    return true;
}

處理表單時(shí),我們的代碼應(yīng)該首先檢查 wporg_zip_code 字段的正確性,然后根據(jù)驗(yàn)證結(jié)果執(zhí)行操作。

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

示例2

假設(shè)我們需要查詢(xún)數(shù)據(jù)庫(kù)中的某些文章,并且可以讓用戶(hù)排序查詢(xún)結(jié)果。

下面的示例代碼通過(guò)使用 PHP 內(nèi)置的函數(shù) in_array 將傳入的排序鍵(存儲(chǔ)在 order_by 參數(shù)中)和一個(gè)允許排序的鍵數(shù)組進(jìn)行比較來(lái)檢查傳入的值是否可以排序,這樣可以防止用戶(hù)傳入惡意的數(shù)據(jù)來(lái)危害網(wǎng)站。

在與鍵數(shù)組比較之前,我們先使用 WordPress 的內(nèi)置函數(shù) sanitize_key 來(lái)處理用戶(hù)輸入,此函數(shù)確保鍵為小寫(xiě)字母(in_array 函數(shù)區(qū)分大小寫(xiě))。

將 “true” 設(shè)置為 in_array 的第三個(gè)參數(shù),告訴 in_array 進(jìn)行嚴(yán)格的類(lèi)型檢查,不僅要比較值,也要比較值的類(lèi)型,這樣可以確保傳入的是一個(gè)字符串而不是其他數(shù)據(jù)類(lèi)型。

<?php
$allowed_keys = ['author', 'post_author', 'date', 'post_date'];
 
$orderby = sanitize_key($_POST['orderby']);
 
if (in_array($orderby, $allowed_keys, true)) {
    // 修改查詢(xún),根據(jù) order_by 鍵來(lái)排序
}

安全輸入

保證安全輸入是對(duì)用戶(hù)輸入的數(shù)據(jù)進(jìn)行清理(凈化、過(guò)濾)的過(guò)程。如果我們不知道具體的數(shù)據(jù)類(lèi)型,或者不希望進(jìn)行嚴(yán)格的數(shù)據(jù)驗(yàn)證,我們可以使用數(shù)據(jù)消毒措施。

任何時(shí)候我們都可能接受不安全的數(shù)據(jù),所以,對(duì)數(shù)據(jù)進(jìn)行消毒和驗(yàn)證很重要。

清理數(shù)據(jù)

清理數(shù)據(jù)最簡(jiǎn)單的方法是使用 WordPress 的內(nèi)置函數(shù)。

WordPress 為我們提供了一些 sanitize_*() 輔助函數(shù)來(lái)幫助我們確保最終獲得安全的數(shù)據(jù),使用這些函數(shù),我們可以很輕松的對(duì)數(shù)據(jù)進(jìn)行消毒。

示例

假設(shè)我們有一個(gè)名為title的輸入字段。

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

我們可以使用 sanitize_text_field() 函數(shù)清理輸入數(shù)據(jù):

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

在幕后,sanitize_text_field() 執(zhí)行以下操作:

  • 檢查無(wú)效的 UTF-8 字符
  • 將大于小于字符(><)轉(zhuǎn)換為實(shí)體
  • 刪除所有 HTML 標(biāo)簽
  • 刪除換行符,制表符和額外的空白字符
  • 刪除所有 8 字節(jié)字符

安全輸出

安全輸出是轉(zhuǎn)義數(shù)據(jù)輸出的過(guò)程。

轉(zhuǎn)義也就是要?jiǎng)h除不需要的數(shù)據(jù),比如格式錯(cuò)誤的 HTML 或腳本標(biāo)記。

無(wú)論何時(shí),渲染數(shù)據(jù)時(shí),都要確保將其轉(zhuǎn)義,轉(zhuǎn)義輸出可以防止 XSS(跨站腳本)攻擊。

跨站點(diǎn)腳本(XSS)是一種 Web 應(yīng)用程序中常見(jiàn)的計(jì)算機(jī)安全漏洞。XSS 使攻擊者可以將客戶(hù)端腳本注入到其他用戶(hù)查看的網(wǎng)頁(yè)中。攻擊者可能會(huì)使用跨站腳本漏洞繞過(guò)訪問(wèn)控制,如同源策略。

轉(zhuǎn)義

轉(zhuǎn)義有助于在最終呈現(xiàn)數(shù)據(jù)給用戶(hù)之前,保護(hù)我們的數(shù)據(jù),WordPress 提供了幾個(gè)輔助函數(shù),可以幫助我們處理大多數(shù)需要轉(zhuǎn)義情況。

  • esc_html() – 在顯示 HTML 時(shí),使用此函數(shù)。
  • esc_url() – 在輸出 URL 時(shí),使用此函數(shù),包括在srchref屬性中的 URL。
  • esc_js() – 對(duì)內(nèi)聯(lián) JavaScript 使用此函數(shù)。
  • esc_attr() – 把數(shù)據(jù)設(shè)置為 HTML 元素屬性時(shí)使用此能力。
大多數(shù) WordPress 函數(shù)都能正確地輸出數(shù)據(jù),所以我們不需要再次轉(zhuǎn)義數(shù)據(jù)。例如,我們可以安全地調(diào)用 the_title() 而不用轉(zhuǎn)義。

使用本地化函數(shù)

相對(duì)于使用 echo 輸出數(shù)據(jù),我們應(yīng)該更多的使用 WordPress 的本地化能力,如 _e() 或 __()

下面的本地化函數(shù) esc_html_e 函數(shù)集成了數(shù)據(jù)轉(zhuǎn)義的能力。

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

結(jié)合了本地化和轉(zhuǎn)義能力的函數(shù)有:

自定義轉(zhuǎn)義

在需要以特殊方式轉(zhuǎn)義輸出的情況下,我們我們可以使用函數(shù) wp_kses() 來(lái)實(shí)現(xiàn)自定義轉(zhuǎn)義能力,此函數(shù)確保只有指定的 HTML元素、屬性和屬性值會(huì)出現(xiàn)在輸出中,并對(duì)HTML實(shí)體進(jìn)行規(guī)范化。

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

wp_kses_post() 是 wp_kses 函數(shù)的封裝,其中$allowed_html是顯示文章內(nèi)容使用的一組規(guī)則。

echo wp_kses_post( $post_content );

隨機(jī)數(shù)驗(yàn)證

出于安全的目的,隨機(jī)數(shù)驗(yàn)證可以幫助我們驗(yàn)證請(qǐng)求的來(lái)源和意圖,每個(gè)隨機(jī)數(shù)只能使用一次。

如果我們的插件允許用戶(hù)提交數(shù)據(jù),無(wú)論是管理員還是游客,我們必須保證用戶(hù)是執(zhí)行操作的那個(gè)人,并且他們擁有執(zhí)行該操作的能力,這兩者一起驗(yàn)證,確保了只有在用戶(hù)希望發(fā)生變化時(shí),數(shù)據(jù)才會(huì)發(fā)生變化。

使用隨機(jī)數(shù)

檢查了用戶(hù)的能力示例之后,增強(qiáng)用戶(hù)提交數(shù)據(jù)安全的下一步是使用隨機(jī)數(shù)驗(yàn)證。用戶(hù)能力檢查確保只有有刪除文章能力的用戶(hù)才能刪除文章,但是如果有人欺騙你點(diǎn)擊了那個(gè)鏈接呢?你有能力刪除文章,但是你沒(méi)有意圖,卻在不知不覺(jué)之間刪除了文章。

我們可以使用隨機(jī)數(shù)來(lái)檢查當(dāng)前用戶(hù)打算執(zhí)行的操作是否為用戶(hù)的真實(shí)意圖。在生成鏈接時(shí),我們可以使用 wp_create_nonce() 函數(shù)添加一個(gè)隨機(jī)數(shù)到鏈接,傳遞給該函數(shù)的參數(shù)確保創(chuàng)建的隨機(jī)數(shù)對(duì)于該操作是唯一的。

然后,在處理刪除請(qǐng)求時(shí),我們可以檢查該隨機(jī)數(shù)是否與我們期望的一致。

關(guān)于隨機(jī)數(shù)的更多信息,可以看一下 Mark Jaquith 的關(guān)于 WordPress 隨機(jī)數(shù) 的文章 ,這是一個(gè)不錯(cuò)的資源。

完整的示例

下面是使用能力檢查、數(shù)據(jù)驗(yàn)證、安全輸入、安全輸入和隨機(jī)數(shù)驗(yàn)證的完整示例。

<?php
/**
 * 在前端創(chuàng)建一個(gè)刪除文章的鏈接
 */
function wporg_generate_delete_link($content){
   // 只在單文章頁(yè)面運(yùn)行
   if (is_single() && in_the_loop() && is_main_query()) {
      // 添加查詢(xún)參數(shù): 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;
}
 
/**
 * 處理用戶(hù)請(qǐng)求
 */
function wporg_delete_post(){
   if (
      isset($_GET['action']) &&
      isset($_GET['nonce']) &&
      $_GET['action'] === 'wporg_frontend_delete' &&
      wp_verify_nonce($_GET['nonce'], 'wporg_frontend_delete')
   ) {

      // 檢查請(qǐng)求中是否有文章參數(shù)
      $post_id = (isset($_GET['post'])) ? ($_GET['post']) : (null);

      // 檢查請(qǐng)求的文章是否存在
      $post = get_post((int)$post_id);
      if (empty($post)) {
         return;
      }

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

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

      // 退出
      die;
   }
}
 
if (current_user_can('edit_others_posts')) {
   /**
    * 添加刪除文章的鏈接到文章內(nèi)容后面
    */
   add_filter('the_content', 'wporg_generate_delete_link');

   /**
    * WordPress 初始化時(shí)注冊(cè)請(qǐng)求處理函數(shù)
    */
   add_action('init', 'wporg_delete_post');
}

我們提供 WordPress主題和插件定制開(kāi)發(fā)服務(wù)

本站長(zhǎng)期承接 WordPress主題、插件、基于 WooCommerce 的商店商城開(kāi)發(fā)業(yè)務(wù)。 我們有 10 年WordPress開(kāi)發(fā)經(jīng)驗(yàn),如果你想 用WordPress開(kāi)發(fā)網(wǎng)站, 請(qǐng)聯(lián)系微信: iwillhappy1314,或郵箱: [email protected] 咨詢(xún)。

發(fā)表回復(fù)

您的郵箱地址不會(huì)被公開(kāi)。 必填項(xiàng)已用 * 標(biāo)注

*