跳到內容

Kali Forms 更新後訪客送不出表單,問題出在快取頁面裡的過期 nonce

整理日期:2026-10-27

一個用 Kali Forms 做預約表單的網站,訪客回報送出後看到 404,也有人看到 403,但管理員登入後台自己測試,每次都能正常送出。

查主機的 access log,失敗的訪客都有同一個順序,先送出一次表單請求到 admin-ajax.php,幾秒後同一位訪客的姓名、Email 和電話出現在網址後面,例如 /booking/?name=...&email=...,再由這個網址回 404 或 403。

這幾筆送出在表單外掛的送出紀錄裡都找不到。


  • 第一次送出的 admin-ajax.php 請求回了什麼,是成功還是被拒。
  • 表單資料為什麼會跑到網址上,表單的 JS 有沒有攔住瀏覽器原本的送出方式。
  • 登入和沒登入差在哪裡,通常是快取和權限。
  • 頁面經過哪幾層快取,包含 WordPress 外掛、主機的 Varnish 和 CDN。
  • 表單外掛最近有沒有更新,前後版本改了什麼。

Kali Forms 會把 nonce(表單用來確認請求來自網站本身的驗證碼)寫在頁面 HTML 裡。這個網站的主機用 Varnish 把整頁快取 7 天,但 WordPress 的 nonce 最多 24 小時有效,訪客拿到的快取頁面裡,nonce 早就過期了,送出時被外掛以 {"message":"Sneaky sneaky","error":true} 拒絕。

外掛其實有設計補救,前端每次載入頁面都會呼叫 kaliforms_get_js_var 換一個新的 nonce。問題是 2.4.17 的資安修正把這支 API 改成只開放給有編輯權限的帳號,未登入的訪客一律回 Denied,前端程式卻沒有跟著改。比對 2.4.16 和 2.4.27 的原始碼,差別就在 wp_ajax_nopriv_kaliforms_get_js_var 從原本的處理函式改接到 denied。

這也解釋了兩件事。2.4.16 以前訪客每次載入都能換到新 nonce,快取多舊都沒關係,網站自動更新後才開始出事。管理員登入時有權限換新 nonce,所以自己測試永遠正常。

被拒絕之後畫面沒有任何提示,因為表單的「全域錯誤訊息」預設是空白,訪客只看到轉圈出現又消失。Kali Forms 的 JS 在送出前會先拆掉自己攔截送出的程式,失敗後沒有裝回去,訪客再按一次,瀏覽器就照一般表單的方式用 GET 把欄位接在目前網址後面。欄位名稱 name 剛好是 WordPress 的保留參數,找不到對應文章就回 404,內容有引號或換行時則被主機防火牆擋成 403。

這些送出在 nonce 那一關就被拒絕,不會存進表單紀錄,唯一留下的痕跡是 access log 裡帶著欄位的那筆網址。


在程式碼片段外掛新增一段 PHP,補回未登入訪客換 nonce 的處理:

<?php
// Kali Forms 2.4.17 起不讓訪客換新 nonce,快取頁面的舊 nonce 過期就會送出失敗;這裡補回訪客版本
add_action( 'wp_ajax_nopriv_kaliforms_get_js_var', function () {
$key = isset( $_POST['data']['key'] ) ? sanitize_key( wp_unslash( $_POST['data']['key'] ) ) : '';
if ( 'ajax_nonce' === $key ) {
wp_send_json_success( wp_create_nonce( 'kaliforms_nonce' ) );
}
}, 1 );

這段要設成全站執行,不能只限前台。換 nonce 的請求走 admin-ajax.php,WordPress 把它算在後台,限定前台的片段不會執行到。

訪客拿到的 nonce 本來就公開寫在每一頁的 HTML 裡,所以用這支 API 給出去並沒有多暴露資訊。這段也只處理 ajax_nonce,其他請求照樣交回外掛原本的 Denied。

admin-ajax.php 的 POST 請求不會被頁面快取保存,補上之後訪客每次載入都會換到新的 nonce,表單頁可以繼續快取。

在程式補上之前,可以先把主機 Varnish 的快取時間從 7 天縮短到 10 小時左右,讓快取頁面裡的 nonce 還在有效期間內。這只能壓住大部分狀況,頁面開著放太久才送出的訪客還是會失敗,所以仍要補上面那段程式。

到每張表單的設定裡填上全域錯誤訊息,例如請訪客重新整理頁面,或改用電話、LINE 聯絡。之後就算因為別的原因送出失敗,訪客至少會知道要換一個方式,不會一直重按。


  • 未登入的狀態下對 admin-ajax.php 送出 action=kaliforms_get_js_var&data[key]=ajax_nonce,修正前回 Denied,修正後回 {"success":true,"data":"..."}。
  • 拿換到的 nonce 送出 action=kaliforms_form_process&data[nonce]=換到的值,刻意不帶表單 ID。回 Form didn`t send a form id 代表 nonce 有效,回 Sneaky sneaky 代表無效,兩種情況都不會真的產生一筆送出。
  • 用無痕視窗實際送出一筆,確認出現成功訊息。
  • 幾天後回頭看 access log,網址裡不應該再出現 ?name=。

看 access log 判斷是哪一種回應時要注意,nginx 記的是壓縮後的大小。這次 Denied 經 brotli 壓縮後是 10 bytes,Sneaky sneaky 的回應是 44 bytes,把候選的回應內容自己壓一次比對,就能分出是哪一種被拒。


  • 不要一開始就把所有表單頁排除在快取之外,這些頁面常常是最重要的落地頁,補上換 nonce 的程式就能保留快取。
  • 不要先改欄位名稱 name 來避開 404,那只會讓訪客看到一個看似正常的頁面,資料一樣沒有送出。
  • 不要用帶引號的網址反覆測試 403 是哪一層擋的,安全外掛可能直接封鎖自己的 IP。
  • 不要只看表單紀錄判斷有沒有漏掉詢問,這種失敗不會留下紀錄,要從 access log 找。

  • 表單外掛名稱、目前版本和最近一次更新日期。
  • 頁面經過哪些快取,各自的快取時間。
  • 未登入時 admin-ajax.php 的回應內容。
  • access log 裡 admin-ajax.php POST 的回應大小,以及之後有沒有帶表單欄位的 GET 網址。
  • 表單的全域錯誤訊息有沒有設定。

  • WordPress
  • Kali Forms
  • nonce
  • Varnish
  • 快取
  • 表單送出
  • 404