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í)踐的完整示例。
外部資源
- Jon Cave:如何修復(fù)容易受攻擊的插件
- Mark Jaquith: 主題和插件安全性
檢查用戶(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é)果被添加或刪除。
用戶(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ù)的有效性。
驗(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ù)據(jù)
清理數(shù)據(jù)最簡(jiǎn)單的方法是使用 WordPress 的內(nèi)置函數(shù)。
WordPress 為我們提供了一些 sanitize_*() 輔助函數(shù)來(lái)幫助我們確保最終獲得安全的數(shù)據(jù),使用這些函數(shù),我們可以很輕松的對(duì)數(shù)據(jù)進(jìn)行消毒。
- sanitize_email()
- sanitize_file_name()
- sanitize_html_class()
- sanitize_key()
- sanitize_meta()
- sanitize_mime_type()
- sanitize_option()
- sanitize_sql_orderby()
- sanitize_text_field()
- sanitize_title()
- sanitize_title_for_query()
- sanitize_title_with_dashes()
- sanitize_user()
- esc_url_raw()
- wp_filter_post_kses()
- wp_filter_nohtml_kses()
示例
假設(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(跨站腳本)攻擊。
轉(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ù),包括在
src和href屬性中的 URL。 esc_js()– 對(duì)內(nèi)聯(lián) JavaScript 使用此函數(shù)。- esc_attr() – 把數(shù)據(jù)設(shè)置為 HTML 元素屬性時(shí)使用此能力。
使用本地化函數(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');
}