iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 6
0
自我挑戰組

Wordpress 外掛開發系列 第 6

「Wordpress 外掛開發」外掛的權限偵測與安全性 nonce/Kses/wpdb

wordpress這麼完整,還需要安全?

我們在建立網站的時候,經常會遇到經典的攻擊手法,像是XSS、sql injection或提升特權的各種漏洞,而在我們的外掛也不例外,你可以能掛了幾個不安全的POST就讓人把你資料庫的資料看漏,而我們的宗旨就是將漏洞消彌,必須避免惡意的攻擊來寫腳本戳你的web serivice,前提是你的程式了解你的輸入輸出知道這個機器人的行為是不正常的。

WordpPess有提供安全的函式,可以讓人避免上述的基本攻擊,而新的core也正在與時俱進的更新本身可能有的漏洞,以及提供權限的分級,而我更愛更極端在偵測到奇怪行為就是放wp_die()來避免,不過在官網感覺上不提倡這種使用方式但沒有明言,總而言之,保護外掛的安全性不難而且是花點時間就可以達到的!

使用者的權限

而我們這裡最常使用的到的就是user permission了,你可能在哪裡都會看到is_admin這個用法,本身是非常不推薦,因為他只有在前台才有用處,在後台的話是會出現null,所以我們使用current_user_can來代替。

current_user_can

這個函式可以以capability代入近驗證是否可以行,而capability的表格在上之前有提到,有興趣可以回去瞧瞧,而他也可以執行user role,而我們最簡單而明瞭的方式,想要去驗證是否是admin,只需要輸入current_user_can('administrator')就可以精準的判斷,可以參考一下他的interface來做如何更進階的使用,在這我們使用這個就非常的方便。

在這裡的capability與User Role,我們之後會更深入的探討;而在我們呼叫的這個函式之前,如果你在init之前呼叫是會回傳Null的,所以更正確的用法是選擇更正確的Hook來做判斷,比如說wp_before_admin_bar_render是已經初始化後到渲染前的Hook之一,我們可已利用他來達到權限的正確性。

add_action('wp_before_admin_bar_render','gogo_toolbar');

function gogo_toolbar(){
  global $wp_admin_bar;
  if(current_user_can('administrator')){
    $wp_admin_bar->add_menu(...);
  }
}

wp_admin_bar 是原生提供的class之一,可以直接來操作toolbar,可以參考這裡的介紹

nonce,wordpress的token

這一個出現,基本上是來預防CSRF攻擊的,如果有人能夠透過漏洞來操控權限提升到管理著,我們選擇用nonce來保護我們的外掛,而nonce是什麼呢?在Wordpress中,他是一個串亂碼的字串,而這個亂碼包含著:一個使用者、一個執行動作、一個物件以及他能被執行的timeframe,不知道你是否有看過在core中可以能會出現那種http://example.com/wp-admin/options.php?action=create&user=110&_wpnonce=A7458c18D這種請求的網址,你只要_wpnonce是與資料庫中出現的token不符合,就沒辦法進行運作,那強迫執行就會出線下面這種連結失效的圖片(這個圖我網路上抓的,別亂連人家的網址啊可能有毒)。

https://ithelp.ithome.com.tw/upload/images/20200920/20104531vPBGA07SWw.png

來,我們設定一下

首先我們使用wp_nonce_url建立一個Nonce的網址,這個函式使用起來非常簡單,三個參數就可以設定,而建立網址我們可以使用add_query_arg來做,而他主要是我們需要的參數並成新的網址,這裏會將使用者以及動作都在這一並加入。

$url = add_query_arg(
  [
    'action' => 'delete',
    'user'   => 'Alex' 
  ],
  admin_url( 'users.php' )
);
return wp_nonce_url( $url, 'gogo_delete_ranger_Alex' );

不只是網址,我們更常需要使用到的是我們的表單,我們需要建立nonce的輸入單元,使用wp_nonce_field來製作,並且將建立好的與表格上的input做鏈接,最後再使用wp_verify_nonce來驗證這一次提交的表單是否有正確;現在使用完三個Nonce的函式後,可以建立一個完整的外掛,我們將這個外掛的功能,可以提供輸入安全的數值,增加了輸入匡與驗證輸入的資料,假如驗證成功,這個資料就會隨著update_option進到資料庫了。


add_action( 'admin_menu', 'gogo_power_ranger_nonce_example_menu'   );
add_action( 'admin_init', 'gogo_power_ranger_nonce_example_verify' );
 
function gogo_power_ranger_nonce_example_menu() {
       add_menu_page(
             'Nonce Example',
             'Nonce Example',
             'manage_options',
             'gogo_power_ranger-nonce-example',
             'gogo_power_ranger_nonce_example_template'
       );
}
 
function gogo_power_ranger_nonce_example_verify() {
 
       if ( ! isset( $_POST['gogo_power_ranger_nonce_name'] ) ) return;

       // 驗證失敗就掛點
       if ( ! wp_verify_nonce( $_POST['gogo_power_ranger_nonce_name'], 'gogo_power_ranger_nonce_action' ) ) wp_die( 'Your nonce could not be verified.' );
       
       // 驗證通過,要來將我們的資料原凈化(sanitzed)了
       if ( isset( $_POST['gogo_power_ranger_nonce_example'] ) ) {
            // 你這邊也可以用preg做正規表達式
             update_option(
                   'gogo_power_ranger_nonce_example',
                   sanitize_text_field( $_POST['gogo_power_ranger_nonce_example'] )
             );
       }

}
 
function gogo_power_ranger_nonce_example_template() { ?>
       <div class="wrap">
             <h1 >Nonce </h1>
             <h2>Verified Under Nonce</h2>
             <?php $value = get_option( 'gogo_power_ranger_nonce_example' ); ?>
             <form method="post" action="">
                 <?php wp_nonce_field( 'gogo_power_ranger_nonce_action', 'gogo_power_ranger_nonce_name' ); ?>
                 <p>
                    <label>
                        Enter your name:
                        <input type="text" name="gogo_power_ranger_nonce_example"
                        value="<?php echo esc_attr( $value ); ?>"/>
                    </label>
                 </p>
                 <?php submit_button( 'Submit', 'primary' ); ?>
             </form>
       </div>
<?php 
}

驗證輸入資料與過篩

除了CSRF,還有最討厭的就是XSS攻擊了,曾經在4版末代的時候,我當時買的新版主題就為了能快速上傳不切尺寸的圖片,所以自己做了一個upload/post.php當時候攻擊後,查到的時候真的是暈倒,不過我們現在把提供的輸入匡可能也會被灌進js的問題,很多都會有著eval然後配上一大串的字符,然後轉址到病毒網站。

而要預防的方式,就是把資料給做凈化,而不只是輸入,你的資料也可能在五鬼搬運的方式,以別的API漏洞,寫進你目前操作的table,所以輸出也是需要凈化的,而我們比較基礎使用的就是是使用wp_unslash來做,那我們也會有個簡單的預設值,讓表單如果不okay的話,他會直接轉成預設,我們選擇的方式是用做個陣列,能連接到叫相關的事下拉式選單,選擇這種可變性低的元件,是可以大大保護這些input的,畢竟他們接受的值是由你自己設定的option。

$_defaults = new stdClass();
 
$_defaults['age'] = absint( $_POST['age'] );
 
$valid_fruit = [
       'banana',
       'kiwi',
       'watermelon'
];
 
$_defaults['fruit'] = 'watermelon';
 
if ( in_array( wp_unslash( $_POST['fruit'] ), $valid_fruit, true ) ) {
       $_defaults['fruit'] = wp_unslash( $_POST['fruit'] );
}

各種類型的上傳淨化

如果你想要上傳一段程式碼與大家分享,可以使用wp_kese或者是wp_filter_nohtml_kses,從字面上就可以知道後者會把html給拿掉只留程式碼,而寫到這讓我想起幾年前Piils在Melbourne convention centre做簡報時,提出幾個很有趣的省思問題,要大家怎麼做設計又不會把使用者改滿,我一直有這個問題也還沒找到兩全其美的方式,印象中的大概問題如下:如果因為一個資料錯誤或者你的表單得全部照的驗證走,而一直無法讓使用者提交,造成他們的不方便?

給他們上傳嗎?在我的認知是請公子吃餅。

而我這裡列舉幾個常見的filter讓大家都可以開心的去淨化你相對應的類別,不過這邊說到比較特別就是integer,因為sign 與unsign所以數字有極限存在,如果超出那個ranger會變成false,最好的方式就是用regx來做,讓數字可以直接被驗證,也可以使用ctype_digit(),不過這個在有些有點問題,如果不工作可能得google一下如何將他enable,。

$value = '999999999999999999999999';

return preg_replace( '/\d/', '', $value );
  • Int : intval($value);
  • String : sanitize_text_field($string)
  • String : wp_strip_all_tags($string)
  • Email : is_eamil($email)
  • url : esc_url($url)
  • redirect: wp_redirect($url)
  • redirect: wp_safe_redirect($url)
  • HTML : esc_html($html)
  • HTML : force:balance:tags($html) 有機率會錯,使用前詳情參閱使用說明書。
  • JS : esc_js($js)

友善提醒,之前在網路上有人教學使用strip_tags,這個的使用結果可能與你想像中不依樣,使用wp_strip_all_tags最保險,安全就是要做整套,如果是想最完整的,請使用preg_match來做自己的規則,最不會出問題!

KSES 專門處理HTML的淨水器

今天假設你只願意接受divh1,的話,你可以使用上述的wp_kses來做,這一個能將"限定"的html tag被使用,而這個限定是要你在第二個參數中傳入的陣列中有提到的,而不像是sanitize_text_field把它清除掉,我們今天拿出來的範例是不是很不切實際?沒有人會為了提供人HTML的tag而加上近千個element tag到陣列之中,所以有個更好用的是wp_kses_data,他與strips_tag的行為模式很像,會將程式碼留下,但消除了script的標籤。

而我們的架構程式碼,全域變數別亂用,尤其是取用一個可能很常見的$_SERVER一定得先經過淨化才能去使用這個參數,你吃飯會洗手,這個邏輯合理吧?我們使用淨化函數來驗證我們這些資料源輸出是乾淨的,確保整個套件的運作的正確性,不管是其他收到其他請求的變數都需要去做淨化的動作,像是$_post或是$_COOKIE..e.t.c

WordPress也是有提供MySQL的ORM

最後我們使用Wordpress提供的orm來連接資料,這個好處是不用每次都連網,但為一個小缺點是你在每個class中,你得要確保你的作用域中是否有呼叫到的全域變數的$wpdb,如果沒有可以在使用之前呼叫global $wpdb;來做宣告,下面的ORM不細講,與一般的使用無異,而這邊需要你的一點SQL知識了,提供資料庫orm的interface在參考欄。

  • $wpdb->update();
  • $wpdb->insert();
  • $wpdb->get_var();
  • $wpdb->get_row();
  • $wpdb->get_col();

但在最後,大家面臨的尋找規則都是更加的複雜,所以他也有提供直接寫進SQL的功能,如果今天你的搜尋條件是有變數的話,我不建議這麼做,我們有個更可靠的大家長wpdb::prepare(),他是利用sprintf的語法作成的,所以可以支援$s這一些字元帶入,而裡頭的限制是你需要去除你的引號,而如果要加入引號的話,你需要斜槓來幫助,雖然這樣有違prepare的邏輯。

  • $wpdb->get_results($sql);
  • $wpdb->prepare( $sql , array( ‘foo’, 1337, ‘%bar’ ) );

在使用prepare我個人覺得是最重要的,使用prepare可以預防sql injection的攻擊,他會先經過乾淨化資料才會進入,不過如果你遇到要改寫表單的人是直接把field沒有整理直接丟進去的,這邊prepare需要處理的單引號還需要再加上斜槓來處理,非常的麻煩,我看見各路大神是有人使用拼湊的方式將引號給加進去,但是程式碼就是變的很醜就是了。

寫到最後已經有點累了,還剩下基礎的tune performance,快取與transients還沒有寫,明天這邊寫完將安全性與效能的部分做個段落,資料庫的部分其實都是基礎的操作,如果是有其餘基礎的人,在學這個ORM就像是洗殘廢澡一般,只需要查一下文件就可以使用去撈特定的資料了,在WordPress操作資料庫就是這麼樸實無華。

Reference

StackOverflow - If the current user is an administrator or editor
CSRF是什麼
Wordpress - nonce
Wordpress - sanitize_texxt_field
Wordpress - wpdb
Wordpress - wp_strip_all_tags
PHP - Filter
Prevent XSS with strip_tags()?
PHP如何防止XSS攻击与XSS攻击原理
SQL注入:适用于WordPress用户的入门指南
鐵人賽 - WordPress 遇上 Chatbot,像極了愛情
完整的preg參考這裡阿斯
關於wordpress的KSES Strips Evil Scripts


上一篇
「Wordpress 外掛開發」主選單設定(Administration Menus) - 下 submit_button/add_setting_error
下一篇
「Wordpress 外掛開發」效能調整 Cache Api/Transient API
系列文
Wordpress 外掛開發30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言