本篇文章更新時間:2023/03/25
如有資訊過時或語誤之處,歡迎使用 Contact 功能通知。
一介資男的 LINE 社群開站囉!歡迎入群聊聊~
如果本站內容對你有幫助,歡迎使用 BFX Pay 加密貨幣新台幣 贊助支持。


看過前兩篇 [WordPress] 外掛開發入門指南[WordPress] 外掛基礎知識篇 – 外掛開發者都需要來讀一次的文件

恭喜你,你的程式碼可運作!但是它是否安全呢?

WordPress 開發團隊嚴謹看待安全性。在網站設計中,安全性有極大的重要性,因此安全性也是必須要關注的核心。儘管核心開發人員已經有一個專門團隊專注保護平台安全,但是像你這樣的主題或外掛開發人員也很清楚,核心外部可能存在許多潛在的漏洞。由於 WordPress 提供了很多功能和靈活性,外掛和主題成了最容易被攻擊的點。

當程式碼在數百,甚至數千個網站上執行時,你應該特別小心處理輸入 WordPress 的資料以及它如何呈現給最終使用者。這個安全性問題通常在你建立的主題設定頁面、建立使用短碼或儲存和顯示與文章相關的額外資料時出現。

內容目錄

發展安全意識

開發時,考量新功能的安全性是非常重要的。開發過程中請使用以下原則:

  • 不要相信任何資料。 不要相信用戶輸入、第三方 API 或資料庫中的資料而沒有驗證。保護你的 WordPress 主題,確保進入和離開你的主題的資料是正確的。在使用者輸入資料之前,必須進行 驗證清理(sanitize),輸出時進行編碼逸出轉譯(escape)。
  • 使用 WordPress API。 許多 WordPress 核心功能提供了內建驗證和清理資料的功能。當可能發生安全性問題時,應使用 WordPress 提供的功能。
  • 保持更新。 隨著技術的進步,外掛或主題中新的安全漏洞也可能隨之而來。保持警惕,透過維護你的程式碼並在必要時更新來避免安全風險。

引導原則

  1. 永遠不要相信使用者輸入的資料。
  2. 盡可能地進行逸出轉譯(Escape)
  3. 逸出轉譯(Escape) 所有來自不可信的來源(例如資料庫和使用者)、第三方(例如 Twitter)等。
  4. 絕不要假設任何事情,驗證它。
  5. 進行清理(Sanitation)輸入資料,但是驗證或拒絕是更好的做法。

檢查使用者權限(Capabilities)

如果你的外掛允許使用者提交資料,無論是在後台管理還是前端操作,都應該檢查用戶權限。

使用者角色與權限

架構高效安全層級最重要的一步是設置用戶權限系統。WordPress 透過『使用者角色與權限』(User Roles and Capabilities)功能實現了這一點。

每個登入到 WordPress 的使用者根據其使用者角色會自動被分配指定的使用者權限。

「使用者角色(User roles)」只是一種描述使用者所屬群組的方式。每個群組都有一組預先定義的權限。

例如,你網站的主要使用者將具有「管理員(Administrator)」的使用者角色,而其他使用者可能會擁有像「編輯(Editor)」或「作者(Author)」的角色。你可以指派多個使用者到同一個角色中,例如網站可能會有兩個管理員。

「用戶權限」是指你專為每個用戶或用戶角色分配的具體權限。

例如,管理員具有「manage_options」權限,這使他們可以查看、編輯和保存網站的設定。相反的,編輯人員無此權限,可以防止他們越權操作。

這些權限會在管理後台中的各個操作點進行檢查。依據分配給角色的權限,選單、功能與其他期望的體驗也許是新增或是刪除。

建立一個外掛時,請務必確保當前的使用者只擁有必要的權限,才可以執行你的程式。

權限階層

使用者角色層級越高,使用者就有越多的功能。每個使用者角色都繼承了層次結構中之前的角色。

例如,對於單站點的安裝,最高的使用者角色為「管理員」,該角色會繼承以下角色及其權限:「訂閱者」、「投稿者」、「作者」和「編輯」。

範例

不限制版本

下面的範例會在前端建立一個連結,允許將文章放入垃圾桶。因為這段程式碼沒有檢查使用者的權限,所以它允許任何訪問該網站的訪客將文章放入垃圾桶!

/**
 * Generate a Delete link based on the homepage url.
 *
 * @param string $content   Existing content.
 *
 * @return string|null
 */
function wporg_generate_delete_link( $content ) {
    // Run only for single post page.
    if ( is_single() && in_the_loop() && is_main_query() ) {
        // Add query arguments: action, post.
        $url = add_query_arg(
            [
                'action' => 'wporg_frontend_delete',
                'post'   => get_the_ID(),
            ], home_url()
        );

        return $content . ' ' . esc_html__( 'Delete Post', 'wporg' ) . '';
    }

    return null;
}

/**
 * Request handler
 */
function wporg_delete_post() {
    if ( isset( $_GET['action'] ) && 'wporg_frontend_delete' === $_GET['action'] ) {

        // Verify we have a post id.
        $post_id = ( isset( $_GET['post'] ) ) ? ( $_GET['post'] ) : ( null );

        // Verify there is a post with such a number.
        $post = get_post( (int) $post_id );
        if ( empty( $post ) ) {
            return;
        }

        // Delete the post.
        wp_trash_post( $post_id );

        // Redirect to admin page.
        $redirect = admin_url( 'edit.php' );
        wp_safe_redirect( $redirect );

        // We are done.
        die;
    }
}

/**
 * Add the delete link to the end of the post content.
 */
add_filter( 'the_content', 'wporg_generate_delete_link' );

/**
 * Register our request handler with the init hook.
 */
add_action( 'init', 'wporg_delete_post' );

限制特殊權限的版本

上述的範例允許任何訪客點擊「刪除」連結並將文章刪除。然而,我們只希望「編輯」權限及以上的使用者能夠點擊「刪除」連結。

為了實現這個目標,我們將檢查當前使用者是否具備 edit_others_posts 權限,而這個權限只有「編輯」權限及以上的使用者才具備。

/**
 * Generate a Delete link based on the homepage url.
 *
 * @param string $content   Existing content.
 *
 * @return string|null
 */
function wporg_generate_delete_link( $content ) {
    // Run only for single post page.
    if ( is_single() && in_the_loop() && is_main_query() ) {
        // Add query arguments: action, post.
        $url = add_query_arg(
            [
                'action' => 'wporg_frontend_delete',
                'post'   => get_the_ID(),
            ], home_url()
        );

        return $content . ' ' . esc_html__( 'Delete Post', 'wporg' ) . '';
    }

    return null;
}

/**
 * Request handler
 */
function wporg_delete_post() {
    if ( isset( $_GET['action'] ) && 'wporg_frontend_delete' === $_GET['action'] ) {

        // Verify we have a post id.
        $post_id = ( isset( $_GET['post'] ) ) ? ( $_GET['post'] ) : ( null );

        // Verify there is a post with such a number.
        $post = get_post( (int) $post_id );
        if ( empty( $post ) ) {
            return;
        }

        // Delete the post.
        wp_trash_post( $post_id );

        // Redirect to admin page.
        $redirect = admin_url( 'edit.php' );
        wp_safe_redirect( $redirect );

        // We are done.
        die;
    }
}

/**
 * Add delete post ability
 */
add_action('plugins_loaded', 'wporg_add_delete_post_ability');

function wporg_add_delete_post_ability() {    
    if ( current_user_can( 'edit_others_posts' ) ) {
        /**
         * Add the delete link to the end of the post content.
         */
        add_filter( 'the_content', 'wporg_generate_delete_link' );

        /**
         * Register our request handler with the init hook.
         */
        add_action( 'init', 'wporg_delete_post' );
    }
}

資料驗證

不可信的資料來自多個來源(使用者、第三方網站,甚至你自己的資料庫!)所有資料在使用前都需要檢查。

請記住:即使是管理員也是用戶,用戶會輸入錯誤的資料,無論是故意還是偶然。你的工作是保護使用者不受自己(或別人)的傷害。

驗證 輸入是將資料根據預定格式(或模式)進行測試並得出明確結果(有效或無效)的過程。相對於逸出轉譯化,驗證是一種更具體的方法,但兩者都有其作用。

簡單的驗證範例:

  • 確認必填欄位不留空
  • 檢查輸入的電話號碼是否只包含數字和連結符號
  • 確認輸入的字串是否為有效的選項之一
  • 確認數量欄位是否大於 0

資料驗證應盡可能在最早期進行。 這意味著在執行任何操作之前對資料進行驗證。

驗證的哲學

關於驗證,有幾種不同的哲學觀點在聊「如何驗證」,每種觀點都適用於不同的情境。

安全名單、白名單

只接受資料從已知且可信的列表輸入。

當不信任的資料與安全列表進行比較時,重要的是確保使用嚴格的類型檢查。否則,攻擊者可能會以某種方式包裝輸入,以使其通過安全列表,但仍具有惡意效果。

比較運算元

$untrusted_input = '1 malicious string';  // will evaluate to integer 1 during loose comparisons

if ( 1 === $untrusted_input ) {  // 使用「==」 比對的話會回傳 true, 但 「===」會回傳 false
    echo '

Valid data'; } else { wp_die( 'Invalid data' ); }

in_array()

$untrusted_input = '1 malicious string';  // will evaluate to integer 1 during loose comparisons
$safe_values     = array( 1, 5, 7 );

if ( in_array( $untrusted_input, $safe_values, true ) ) {  // 第三個參數 `true` 會打開強型別檢查
    echo '

Valid data'; } else { wp_die( 'Invalid data' ); }

switch()

$untrusted_input = '1 malicious string';  // will evaluate to integer 1 during loose comparisons

switch ( true ) {
    case 1 === $untrusted_input:  // 使用自己的強型別檢查比使用 switch 的方法好
        echo '

Valid data'; break; default: wp_die( 'Invalid data' ); }

黑名單

拒絕來自已知不受信任的資料列表。通常不是一個好主意。

格式檢查

測試資料是否符合正確的格式,只有當符合格式時才予以接受。

if ( ! ctype_alnum( $data ) ) {
  wp_die( "Invalid format" );
}

if ( preg_match( "/[^0-9.-]/", $data ) ) {
  wp_die( "Invalid format" );
}

格式校正

接受大部分資料,但移除或是逸出轉譯可能的危險部分。

$trusted_integer = (int) $untrusted_integer;
$trusted_alpha = preg_replace( '/[^a-z]/i', "", $untrusted_alpha );
$trusted_slug = sanitize_title( $untrusted_slug );

範例一

讓我們來設計一個輸入欄位,用來輸入 US 美國的郵遞區號:

在這裡,我們告訴瀏覽器只允許輸入最多十個字符,但是在輸入哪些字符上沒有限制。他們可以輸入 11221eval()

這就是驗證的作用。在處理表單時,我們寫程式來檢查每個字串的正確資料類型,如果不正確,就將其丟棄。

例如:為了檢查my-zipcode字段,我們可以進行以下操作:

/**
 * Validate a US zip code.
 *
 * @param string $zip_code   RAW zip code to check.
 *
 * @return bool              true if valid, false otherwise.
 */
function wporg_is_valid_us_zip_code( string $zip_code ):bool {
    // Scenario 1: empty.
    if ( empty( $zip_code ) ) {
        return false;
    }

    // Scenario 2: more than 10 characters.
    // The `maxlength` attribute is only enforced by 
    // the browser, so we still need to validate the
    // length of the input on the server to protect
    // against a manual submission.
    if ( 10 < strlen( trim( $zip_code ) ) ) {
        return false;
    }

    // Scenario 3: incorrect format.
    if ( ! preg_match( '/^d{5}(-?d{4})?$/', $zip_code ) ) {
        return false;
    }

    // Passed successfully.
    return true;
}

然後,在處理表單時,你的程式碼應該檢查 wporg_zip_code 欄位,並根據結果執行相應的動作:

if ( isset( $_POST['wporg_zip_code'] ) && wporg_is_valid_us_zip_code( $_POST['wporg_zip_code'] ) ) {
    // $_POST['wporg_zip_code'] is valid; carry on
}

注意,這個具體的例子是在檢查提供的資料是否為正確的格式;它並不是在檢查資料是否為有效的郵遞區號。為了達到這個目的,你需要再寫一個函式來將其與一個有效郵遞區號列表進行比較。

範例二

假設你的程式碼會查詢資料庫中的文章,而你希望允許使用者對查詢結果進行排序。

$allowed_keys = array( 'author', 'post_author', 'date', 'post_date' );
$orderby      = sanitize_key( $_POST['orderby'] );
if ( in_array( $orderby, $allowed_keys, true ) ) {
    // $orderby is valid; carry on
}

此範例程式碼會透過比對鍵值排序的陣列,確認傳入鍵值排序(紀錄在 orderby 輸入參數中)的有效性,以防止使用者傳入任意且可能有害的資料。

在將傳入的鍵值排序與陣列比對前,會先使用 WordPress 內建的 sanitize_key() 函式處理鍵值。此函式確保(除其他處理之外)鍵值全為小寫,因為 in_array() 進行區分大小寫的搜尋。

true 傳入 in_array() 第三個參數即啟用嚴格型別檢查,讓該函式不僅比較值,也比較值的型別。透過此方式,程式碼可以確信傳入的鍵值排序是字串類型,而非其他類型的資料。

驗證資料會使用的函式

大部分的驗證都是在自定義的程式碼中進行,但也有一些輔助功能。這些輔助功能是此清理(Sanitation)頁面上列出的功能之外的。

  • balanceTags( $html )force_balance_tags( $html ) – 用於嘗試確保 HTML 標籤平衡,有開頭有結尾,用以產生有效的 XML 輸出。
  • count() 用於檢查陣列中有多少項目
  • in_array() 用於檢查某個項目是否存在於陣列中
  • is_email() 用於驗證電子郵件地址是否有效。
  • in_array() 用於檢查某個項目是否存在於陣列中
  • mb_strlen()strlen() 用於檢查字串的預期字元數
  • preg_match()strpos() 用於在其他字串中檢查某些字串是否存在
  • sanitize_html_class( $class, $fallback ) – 對 HTML 中的 Class 屬性值進行逸出轉譯,確保只包含有效字元。將字串逸出轉譯成 A-Z、a-z、0-9、'-' 等字元,如此逸出轉譯產生空字串,則會返回提供的替代值。
  • tag_escape( $html_tag_name ) – 對 HTML 標籤名稱進行逸出轉譯。
  • term_exists() 用於檢查標籤、類別或其他分類法詞彙是否存在。
  • username_exists() 用於檢查使用者名稱是否存在。
  • validate_file() 用於驗證輸入的檔案路徑是否為真實路徑(但並不驗證檔案是否存在)。

請查看 WordPress 開發參考文件 以了解更多像這樣的功能。搜尋名稱類似 *_exists()*_validate()is_*()的函式。並非所有這些都是驗證函式,但其中許多都很有用。

清理 (Sanitizing) 資料

「Sanitizing input」指的是保護、清理、過濾輸入資料的過程。「Validation 驗證」比「Sanitization 清理」更具體,因此通常優先考慮使用 Validation 驗證。但是,當「更具體細節」的驗證方式不可行時,「Sanitization 清理」便是次佳的選擇。

範例

假設我們有一個輸入的欄位名稱是 title

我們在這裡不能使用驗證(Validation),因為文字欄位太一般了:它可以是任何內容。因此,我們使用 sanitize_text_field() 函式對輸入資料進行過濾。

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

前述提到的 sanitize_text_field() 做以下幾件事情:

  1. 檢查無效的 UTF-8
  2. 將單一的 less-than 字元(<) 轉成實體格式(HTML entity)
  3. 去除所有的 HTML 標籤
  4. 移除換行、tab 和多餘的空白
  5. 去除八進制字元。

Sanitization 清理可以使用的函式

這裡列出一些方法可以用來幫你「清理」資料:

逸出轉譯資料

「逸出轉譯」是一種保護輸出資料的過程,通過排除不需要的資料,例如格式不正確的 HTML 標籤,從而保護你的資料,在呈現給終端用戶之前確保其安全性。

大部分 WordPress 的函式都會妥善處理好輸出的資料,不需要額外的逸出轉譯處理。

逸出轉譯的函式

WordPress 有許多輔助功能可用於大多數常見的情境。

仔細注意每個功能的行為,因為有些會刪除 HTML 標籤,有些則允許 HTML 標籤存在。你必須根據輸出的內容與上下文,使用最適合的方法,且總是輸出時才進行轉譯,而不是之前。

  • esc_html() – 將HTML元素排除於內容中,可在任何時候使用。這方法會去除 HTML。

  • esc_js() – 使用在內嵌形式呈現的 JavaScript

  • esc_url() – 使用在任何形式下的 URL 連結資源,包含 srchref 屬性

  • esc_url_raw() – 使用在 URL 儲存資料庫或其他需要非編碼 URL 的情況下。

  • esc_xml() – 使用在 XML 格式區塊中。

  • esc_attr() – 將其用於所有印出 HTML 元素屬性中的內容。

  • esc_textarea() – 用於 Textarea 多行輸入的元素中,轉譯其文字內容。

  • wp_kses() – 使用此方法來安全地轉譯所有不信任的 HTML(如文章、留言等),並且保留 HTML 的格式。

  • wp_kses_post() – 另一個版本的 wp_kses(),可允許所有在文章內容中允許的 HTML 元素。

  • wp_kses_data() – 另一個版本的 wp_kses(),可允許所有在留言內容中允許的 HTML 元素。

客製化逸出轉譯的範例

如果你需要以特定的方式逸出轉譯,那麼函式 wp_kses()(發音為“kisses”)就很有用。

該函式確保你的輸出中只包含指定的 HTML 元素、屬性和屬性值,並規範化 HTML 實體。

 array(
            'href'  => array(),
            'title' => array(),
        ),
        'br'     => array(),
        'em'     => array(),
        'strong' => array(),
    )
); ?>

在這個例子中,除了
以外的所有標籤都會被去除。此外,如果傳遞了一個
標籤,則轉譯會確保只返回 hreftitle 的屬性。

總是最後才執行轉譯

最好在輸出資料時盡可能晚地進行輸出轉譯。

遲點轉譯有幾點好處:

  • 可以更快速地進行程式碼審查和部署,因為可以一眼判斷輸出是否安全,而不是在許多行程式碼中找尋以查看是否已轉譯。
  • 在變數首次轉換和輸出之間,某些問題可能會不小心更改變數,引入潛在漏洞。
  • 晚點轉譯可以更輕鬆地進行自動程式碼掃描,節省時間,並減少審查與部署時間。
  • 在任何可能的情況下,晚點轉譯使程式碼更加彈性與未來的相容性。
  • 在輸出時轉換/轉譯可以消除任何歧義並增加了清晰度(對於開發維護人員)。
// 可以,但不是很好。
$url = esc_url( $url );
$text = esc_html( $text );
echo '' . $text . '';

// 好多了!
echo '' . esc_html( $text ) . '';

除非... 你無法這麼做

有時候逃脫字元的運用並不實用。在少數特殊情況下,無法將輸出傳遞給 wp_kses(),因為就其定義而言,它會剔除正在運作的程式碼,導致錯誤。

在這種情況下,將在建立字串時進行轉譯,並將值儲存在帶有後綴 _escaped_safe_clean 的變數中(例如, $variable 變成 $variable_escaped$variable_safe)。

如果一個函式不能在內部最後才輸出與轉譯,那麼它要確保總是能返回「安全」的 HTML。這允許 echo my_custom_script_code(); 這樣的客製化操作,而不使用 wp_kses(),並使用這些標籤。

帶有本地化函式的轉譯

相較於使用 echo 輸出資料,通常會使用 WordPress 的本地化函式,例如 _e()__()

這些函式僅僅是將一個本地化函式包在一個轉譯函式中:

esc_html_e( 'Hello World', 'text_domain' );
// 相同於下面
echo esc_html( __( 'Hello World', 'text_domain' ) );

下面是一些有用的本地化與轉譯方法:

範例

任何數值變數在任何地方使用都需要進行轉譯處理

echo $int;

根據是整數還是浮點數,(int)absint()(float) 都是正確且可接受的方法。不過有時,number_format()number_format_i18n() 可能更適合使用。
intval()floatval() 是可接受,但過時的(PHP4)函式。

將 HTML 屬性內的任意變數進行轉譯

echo '
';

可以呼叫 esc_attr() 方法進行轉譯。

當一個變數被用作屬性或 URL 的一部分時,最好將整個字串進行轉譯,這樣,如果變數前面有一個潛在的轉譯字元,它也會被正確地轉譯。

正確:

echo '
';

不正確:

echo '
';

注意:如果在 HTML 屬性中使用 wp_create_nonce() 建立的 nonce,也應像這樣進行轉譯處理。

在 HTML 屬性中跳脫任意 URL,在其他情境下也同樣適用

echo '';

這應該使用 esc_url() 方法進行轉譯。

正確:

echo '';

不正確:

echo '';
echo '';

透過 wp_localize_script() 方法傳遞任意 JavaScript 變數

wp_localize_script( 'handle', 'name',
    array(
        'prefix_nonce' => wp_create_nonce( 'plugin-name' ),
        'ajaxurl'      => admin_url( 'admin-ajax.php' ),
        'errorMsg'     => __( 'An error occurred', 'plugin-name' ),
    )
);

不需要特別轉譯,WordPress 方法會替你處理。

轉譯 JavaScript 程式碼區塊中的任意變數

$my_var 變數應使用 esc_js() 方法轉譯。

正確:

在內嵌的 JavaScript 程式碼中轉譯任意變數

$var 變數應使用 esc_js() 方法轉譯。

正確:

在 HTML 屬性中轉譯任意變數以供 JavaScript 使用

$var 變數應使用 esc_js()json_encode()wp_json_encode() 方法轉譯。

正確:

轉譯 HTML textarea 多行輸入元素中的任意字串

echo '';

$data 變數應使用 esc_textarea() 方法轉譯。

正確:

echo '';

轉譯帶有任意標籤的 HTML 字串

echo '
', $phrase, '
';

這取決於 $phrase 變數預期是否包含 HTML 標籤。

  • 如果不包含,則使用 esc_html() 或其變體方法。
  • 如果預期有 HTML,請使用 wp_kses_post()wp_kses_allowed_html()wp_kses(),其中包含你希望保留的 HTML 標籤。

在 XML 或 XSL 格式中的上下文中轉譯任意字串

echo '', $var, '';

使用 esc_xml()ent2ncr() 方法轉譯。

正確:

echo '', ent2ncr( $var ), '';

Nonces

「Nonce」是指「一次性使用的數字」,用來保護URL和表單免受某些類型的濫用、惡意或其他影響。

在技術上,WordPress 的 nonce 並不都絕對是數字;它們是由數字和字母組成的雜湊。它們也不僅僅使用一次:它們有一個有限的「壽命(週期)」,在這個期限過後它們就會過期。在這段時間內,在特定的用戶與使用情境中,相同的 nonce 將會產生。對於該操作的 nonce ,該用戶的 nonce 將保持不變,直到該 nonce 的生命週期結束為止。

WordPress 的安全權杖被稱為「Nonces」(儘管與前述真正的 Nonces 定義存在差異),因為它們與 Nonces 具有類似的作用。有助於防範多種類型的攻擊,包括 CSRF,但不會防止重放攻擊 (Replay attack),因為不會檢查 Nonces 是否為一次性使用。永遠不應該在身份驗證、授權或控制訪問時僅仰賴 Nonces 的功能,請使用 current_user_can() 來保護你的功能,並始終假設非安全權杖可能會被破解。

為何要使用 nonce?

以一個使用 nonce 的例子作為說明,考慮管理員後台可能產生的 URL 範例如下,會將文章編號為 123 的文章刪除。

http://example.com/wp-admin/post.php?post=123&action=trash

當你前往該 URL 時,WordPress 將驗證你的授權 cookie 資訊,如果你有權刪除該文章,WordPress 將繼續進行刪除作業。攻擊者可以做的是讓你的瀏覽器在不知情的情況下轉到該網址。例如,攻擊者可以在第三方網頁上製作一個偽裝的連結,如下所示:

這會觸發你的瀏覽器向 WordPress 發出請求,並且瀏覽器會自動附加你的驗證 cookie,而 WordPress 將認為這是一個有效的請求。

加上 nonce 可以防止這種情況發生。例如,當使用 nonce 時,WordPress 為使用者產生的 URL 如下所示:

http://example.com/wp-admin/post.php?post=123&action=trash&_wpnonce=b192fc4204

如果有人試圖在沒有由 WordPress 生成並提供給用戶的正確 nonce 的情況下刪除編號為 123 的文章,WordPress 將向瀏覽器發送 “403 Forbidden” 回應。

建立一個 nonce

你可以建立一個 Nonce 並將其新增到 URL 的查詢字串中,也可以將其新增到表單中的隱藏欄位中,或者以其他方式使用它。

對於要在 AJAX 請求中使用的 Nonces,通常會將 Nonce 新增到隱藏的欄位中,JavaScript 程式碼可以從中提取它。

請注意,Nonces 對當前使用者的操作都是唯一的,因此,如果使用者在非同步的登入或登出操作下,頁面上的 Nonces 將不再有效。

在 URL 中加入 nonce

要在 URL 中加入一個 nonce,請使用 wp_nonce_url() 方法,代入指定 URL 和查詢操作的字串。例如:

$complete_url = wp_nonce_url( $bare_url, 'trash-post_'.$post->ID );

為了最大限度地保護安全,請確保查詢動作的字串越具體越好。

預設情況下,wp_nonce_url() 會加入一個名為 _wpnonce 的欄位。你可以在函式呼叫中並指定不同的名稱。例如:

$complete_url = wp_nonce_url( $bare_url, 'trash-post_'.$post->ID, 'my_nonce' );

在表單中加入 nonce

若要在表單中加入 nonce,可以呼叫 wp_nonce_field() 方法,並指定代表該項動作的字串。預設情況下,wp_nonce_field() 會產生兩個隱藏欄位,一個其值為 nonce,另一個其值為當前網址(來源),並將結果輸出。例如,這個呼叫:

wp_nonce_field( 'delete-comment_'.$comment_id );

可能會輸出類似以下的結果:


為達到最大的保護效果,請確認代表該項動作的字串越具體越好。

你可以指定 nonce 欄位的不同名稱,也可以指定不想要引薦來源欄位,還可以指定將結果回傳而非直接輸出。有關語法的詳細說明,請參考: wp_nonce_field() 方法文件說明。

在其他地方使用 nonce

如果要建立一個 nonce 以供其他用途使用,請呼叫 wp_create_nonce() 方法,並指定一個表示動作的字串。例如:

$nonce = wp_create_nonce( 'my-action_'.$post->ID );

這僅僅返回 nonce 本身,例如:295a686963

為了獲得最大限度的保護,表示動作的字串應盡可能的具體描述。

驗證 nonce

你可以驗證傳遞在 URL、後台管理畫面的表單、AJAX 要求,或其他一些情境中的 nonce。

要驗證傳遞在 URL 或管理員畫面的表單中的 nonce,請呼叫 check_admin_referer(),並指定表示動作的字串。

例如:

check_admin_referer( 'delete-comment_'.$comment_id );

此呼叫會檢查 nonce 和 referrer,如果驗證失敗,則採取正常操作(以 “403 Forbidden” 回應錯誤訊息終止此動作執行)。

如果你在建立 nonce 時未使用預設的欄位名稱(_wpnonce),請指定欄位名稱。

例如:

check_admin_referer( 'delete-comment_'.$comment_id, 'my_nonce' );

驗證透過 AJAX 請求的 nonce

若要驗證傳遞在 AJAX 請求中的 nonce,請呼叫 check_ajax_referer() 方法,並指定代表該動作的字串。例如:

check_ajax_referer( 'process-comment' );

這個呼叫會檢查 nonce(但不會檢查 referrer),如果檢查失敗,則預設會終止程式執行。

如果你在建立 nonce 時沒有使用預設的欄位名稱(_wpnonce_ajax_nonce),或者想採取其他行動而不是終止程式執行,可以指定其他參數。詳細內容請參閱:check_ajax_referer()

驗證在其他情況下傳遞的 nonce

為了驗證在其他情況下傳遞的 nonce,使用 wp_verify_nonce(),並指定 nonce 和表示動作的字串。

例如:

wp_verify_nonce( $_REQUEST['my_nonce'], 'process-comment'.$comment_id );

如果方法驗證結果是 false,則不繼續處理請求。取而代之,採取一些適當的動作。通常是呼叫 wp_nonce_ays(),該函式會向瀏覽器發送 "403 Forbidden" 的回應。

修改 nonce 的系統

你可以透過勾點來修改 nonce 系統。

修改 nonce 的生命週期(過期時間)

預設情況下,nonce 會在一天後失效,即使其仍符合相符的動作字串。若要更改生命週期,可以新增一個 nonce_life 事件過濾器,指定生命週期的秒數。

例如,將生命週期更改為四小時:

add_filter( 'nonce_life', function () { return 4 * HOUR_IN_SECONDS; } );

執行額外驗證

check_admin_referrer() 發現 nonce 和 referrer 都有效時,可以加入 check_admin_referer 動作以進行額外的驗證。

例如:

function wporg_additional_check ( $action, $result ) {
    ...
}
add_action( 'check_admin_referer', 'wporg_additional_check', 10, 2 );

修改發生錯誤時的訊息

你可以透過本地化的翻譯機制更改當一個 nonce 無效時顯示的錯誤訊息。例如:

function my_nonce_message ($translation) {
    if ($translation === 'Are you sure you want to do this?') {
       return 'No! No! No!';
    } 

    return $translation;
}

add_filter('gettext', 'my_nonce_message');

額外的資訊

本章節包含有關 WordPress 中 nonce 系統的其他資訊,可能在某些情況下很有用。

Nonce 的生命週期

注意,正如 WordPress nonce 不是「一次性使用的數字」一樣,nonce 的生命週期也不是真正的生命週期。WordPress 使用一個具有兩個標記的系統(一半的壽命),並驗證當前標記和上一個標記的 nonces。在預設設置下(24 小時的壽命),這意味著 nonce 中的時間資訊與自 Unix 時間以來過去了多少個 12 小時有關。這意思在中午至午夜之間產生的 nonce 將具有「壽命」直到隔天中午。實際「壽命」是在 12 至 24 小時之間變化。

當 nonce 有效時,驗證的函式會返回當前的標記號碼,即 1 或 2。例如,你可以使用此資訊來刷新處於第二個標記的 nonce,這樣它們就不會過期。

Nonce 的安全性

如果你已正確安裝 WordPress, Nonces 將使用獨特於你網站的「金鑰」和「鹽」產生。NONCE_KEYNONCE_SALT 定義在你的 wp-config.php 文件中,該文件包含提供更多資訊的註解。

你不應僅依賴 Nonces 進行身份驗證或授權或訪問控制。使用 current_user_can() 來保護你的函式,並始終假定 Nonces 可能會被破解。

取代 nonce 系統

nonce 系統的某些功能是可替換的,因此你可以透過提供自己的函式來替換它們。

如果要更改管理員請求或 AJAX 請求驗證的方式,可以替換 check_admin_referrer()check_ajax_referrer(),或者兩者兼具。

如果要使用其他 nonce 系統替換 nonce 系統,可以替換 wp_create_nonce()wp_verify_nonce()wp_nonce_tick() 等方法。

相關資訊

Nonce 相關函式: wp_nonce_ays(), wp_nonce_field(), wp_nonce_url(), wp_verify_nonce(), wp_create_nonce(), check_admin_referer(), check_ajax_referer(), wp_referer_field()

Nonce 相關勾點: nonce_life, nonce_user_logged_out, explain_nonce_(verb)-(noun), check_admin_referer

常見的漏洞

安全性是一個不斷變化的領域,漏洞也會隨時間而演變出現。以下討論了一些常見的漏洞,以及保護網站不受利用的技巧。

漏洞的類型

資料庫 SQL 注入攻擊(SQL Injection)

當輸入的數值未經適當處理而可能執行任何 SQL 命令時,就會發生 SQL 注入。為了防止這種情況發生,WordPress API 提供了廣泛的功能,例如 add_post_meta();,而不需要你透過 SQL 指令手動新增文章中繼資料 (INSERT INTO wp_postmeta…)。

exploits_of_a_mom

xkcd Exploits of a Mom

這個四格漫畫在說一個媽媽把小孩的名字命名為「Robert'); DROP TABLE Students;--」,然後學校的系統因為沒有做到逸出轉譯這小孩的名字,導致資料庫中全校學生的資料表被刪除了的意外。

網站防範 SQL 注入的第一個原則是:當有 WordPress 函式可以使用時,請使用它。

但有時你需要進行複雜的查詢,而這些查詢在 API 中並未設計。如果是這種情況,請務必使用 $wpdb 函式。這些函式是專門為了保護你的資料庫而建立的。

在執行 SQL 查詢之前,所有 SQL 查詢中的資料都必須進行 SQL 轉譯,以防止 SQL 注入攻擊。最好的 SQL 轉譯函式是 $wpdb->prepare(),它支持類似 sprintf()vsprintf() 這樣的語法。

$wpdb->get_var( $wpdb->prepare(
    "SELECT something FROM table WHERE foo = %s and status = %d",
    $name, // an unescaped string (function will do the sanitization for you)
    $status // an untrusted integer (function will do the sanitization for you)
) );

跨網站指令碼攻擊 Cross Site Scripting (XSS)

跨網站指令碼攻擊(XSS)是指惡意使用者注入 JavaScript 程式碼到網頁中。

透過轉譯輸出、去除非必要的資料,可以避免 XSS 漏洞危害。 作為主題的主要任務是輸出内容,主题應該根據內容類型使用合適的函式轉譯動態内容

其中一個轉譯函式的例子是從使用者資料中轉譯 URL。

包含 HTML 實體的内容可以透過只允許特定的 HTML 元素方式處理輸出。

$allowed_html = array(
    'a' => array(
        'href' => array(),
        'title' => array()
    ),
    'br' => array(),
    'em' => array(),
    'strong' => array(),
);

echo wp_kses( $custom_content, $allowed_html );

跨網站偽造要求 Cross-site Request Forgery (CSRF)

跨網站偽造要求(Cross-site request forgery 或縮寫 CSRF,發音為 sea-surf),是指當一個惡意的詐騙使用者,在已經驗證的網站應用程式中執行惡意的請求。例如,一封釣魚郵件可能包含一個連結到一個頁面,該頁面將刪除使用者在 WordPress 管理後台中的帳號。

如果你的主題包含任何 HTML 或基於 HTTP 的表單提交,請使用一個nonce來保證使用者意圖執行某個動作。

持續更新

保持對可能的安全漏洞的最新了解非常重要。以下資源提供一個好的開始:

完整的安全性範例

完整的安全性範例,包含權限檢查、資料驗證、安全輸入、安全輸出和 nonce:


/**
 * Generate a Delete link based on the homepage url.
 *
 * @param string $content   Existing content.
 *
 * @return string|null
 */
function wporg_generate_delete_link( $content ) {
    // Run only for single post page.
    if ( is_single() && in_the_loop() && is_main_query() ) {
        // Add query arguments: action, post, nonce
        $url = add_query_arg(
            [
                'action' => 'wporg_frontend_delete',
                'post'   => get_the_ID(),
                'nonce'  => wp_create_nonce( 'wporg_frontend_delete' ),
            ], home_url()
        );

        return $content . ' ' . esc_html__( 'Delete Post', 'wporg' ) . '';
    }

    return null;
}

/**
 * Request handler
 */
function wporg_delete_post() {
    if ( isset( $_GET['action'] )
         && isset( $_GET['nonce'] )
         && 'wporg_frontend_delete' === $_GET['action']
         && wp_verify_nonce( $_GET['nonce'], 'wporg_frontend_delete' ) ) {

        // Verify we have a post id.
        $post_id = ( isset( $_GET['post'] ) ) ? ( $_GET['post'] ) : ( null );

        // Verify there is a post with such a number.
        $post = get_post( (int) $post_id );
        if ( empty( $post ) ) {
            return;
        }

        // Delete the post.
        wp_trash_post( $post_id );

        // Redirect to admin page.
        $redirect = admin_url( 'edit.php' );
        wp_safe_redirect( $redirect );

        // We are done.
        die;
    }
}

/**
 * Add delete post ability
 */
add_action('plugins_loaded', 'wporg_add_delete_post_ability');

function wporg_add_delete_post_ability() {    
    if ( current_user_can( 'edit_others_posts' ) ) {
        /**
         * Add the delete link to the end of the post content.
         */
        add_filter( 'the_content', 'wporg_generate_delete_link' );

        /**
         * Register our request handler with the init hook.
         */
        add_action( 'init', 'wporg_delete_post' );
    }
}
```

Share:

作者: Chun

資訊愛好人士。主張「人人都該為了偷懶而進步」。期許自己成為斜槓到變進度條 100% 的年輕人。[///////////____36%_________]

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *


文章
Filter
Apply Filters
Mastodon