CSRF 高風險防範與 Nonce 使用方式(WordPress)

一、Nonce 函式說明

函式名稱 說明
wp_nonce_url( $url, $action ) 為 URL 附加 nonce 參數
wp_verify_nonce( $nonce, $action ) 驗證 URL 內的 nonce
wp_nonce_field( $action, $name, $referer, $echo ) 產生隱藏欄位用於表單 CSRF 驗證
check_admin_referer( $action, $query_arg ) 驗證表單送出的 nonce 是否有效
wp_referer_field() 表單中加入 _wp_http_referer 隱藏欄位
wp_original_referer_field() 表單中加入 _wp_original_http_referer 隱藏欄位
wp_get_referer() 取得 HTTP Referer 值
wp_get_raw_referer() 取得未經處理的 HTTP Referer 值
wp_get_original_referer() 取得原始 Referer
wp_validate_redirect( $location, $default ) 驗證重導向網址是否合法(防止 Open Redirect)

官方文件連結:https://developer.wordpress.org/reference/functions/


二、表單中加入 CSRF 防護

表單中應加入 wp_nonce_field() 隱藏欄位,以及於接收端驗證 nonce:

// 加入 nonce 欄位
add_action( 'login_form', 'add_login_nonce' );
function add_login_nonce() {
    wp_nonce_field( 'custom-login-action', '_wpnonce' );
}

// 驗證 nonce
add_action( 'wp_authenticate', 'check_login_nonce', 1 );
function check_login_nonce() {
    if ( ! isset( $_POST['_wpnonce'] ) || ! wp_verify_nonce( $_POST['_wpnonce'], 'custom-login-action' ) ) {
        wp_die( 'CSRF 驗證失敗' );
    }
}

適用於以下表單(可依需求加在對應的 action 中):

add_action('elementor_pro/search_form/before_input','add_wp_nonce_field');
add_action('lostpassword_form','add_wp_nonce_field');
add_action('register_form','add_wp_nonce_field');
add_action('login_form','add_wp_nonce_field');
add_action('comment_form','add_wp_nonce_field');

function add_wp_nonce_field() {
    wp_nonce_field( 'form-action', '_wpnonce' );
}

三、Script 與 Style 加入 Nonce(用於 CSP)

瀏覽器的 CSP(Content-Security-Policy)若啟用 script-src 'nonce-xxx',需為 <script><style> 加上對應的 nonce 屬性。

方法一:使用 template_redirect 並加上 output buffer

add_action( 'template_redirect', function () {
    ob_start( function ( $output ) {
        $nonce = wp_create_nonce( 'inline-script-style' );

        $output = preg_replace_callback( '#<script(?![^>]*\bnonce=)[^>]*>#i', function ( $matches ) use ( $nonce ) {
            return str_replace( '<script', "<script nonce=\"{$nonce}\"", $matches[0] );
        }, $output );

        $output = preg_replace_callback( '#<style(?![^>]*\bnonce=)[^>]*>#i', function ( $matches ) use ( $nonce ) {
            return str_replace( '<style', "<style nonce=\"{$nonce}\"", $matches[0] );
        }, $output );

        header( "Content-Security-Policy: script-src 'self' 'nonce-{$nonce}'; style-src 'self' 'nonce-{$nonce}';" );

        return $output;
    } );
} );

方法二:為已註冊 script 添加 nonce

此方法僅適用於 wp_enqueue_script() 註冊過的腳本:

add_filter( 'script_loader_tag', 'add_nonce_to_script', 10, 3 );

function add_nonce_to_script( $tag, $handle, $src ) {
    if ( $handle === 'your-script-handle' ) {
        $nonce = wp_create_nonce( 'inline-script' );
        return str_replace( '<script', "<script nonce=\"{$nonce}\"", $tag );
    }
    return $tag;
}

四、注意事項與最佳實踐

  1. wp_nonce_field() 預設會加上目前 URL 的 referer 值,如不需要可傳入 false

    ​​​wp_nonce_field( 'action', '_wpnonce', false, true );
    
  2. nonce 並非防止重放攻擊的完整機制,應搭配 HTTPS 與短時效設計。

  3. Nonce 本質上是一種 token,預設壽命 24 小時,可透過 nonce_life 過濾器調整。


五、參考資源


若需加入 CSP 強化防禦,建議搭配 HTTP header 設定,非單靠 nonce 即可杜絕攻擊,並應審慎避免過度寬鬆的設定如 unsafe-inline