HTML 標準修正巢狀導覽的 traverse 事件判斷與替換金鑰保留
whatwg/html · 2026-09-15
HTML living standard 把巢狀 <iframe> 在瀏覽器前進、後退(traverse)時該不該對子可導覽物件觸發 navigate 事件的判斷式,從一個恆為 false 的比較式,換成比對「traversal 更新前實際顯示的歷史項目」;同一天另一個 commit 補上 history.replaceState() 等同文件替換操作會遺失 Navigation API 金鑰的漏洞。兩個修正都由 Shannon Booth 提交:10456f5 於 2026-09-15 由 Simon Pieters 合併,9ef219c 於同日直接併入 whatwg/html。
Navigation API 是瀏覽器逐步推出的導覽攔截介面:navigation.currentEntry 代表目前分頁所在的 session history entry,每個 entry 帶一組不可變的 key,讓程式可以跨導覽辨識同一筆歷史記錄;navigate 事件則讓程式在導覽(含使用者按上一頁、下一頁造成的 traversal)發生時攔截或觀察。這兩個修正都是在補這組介面底層依賴的 session history 演算法漏洞,不是新增功能。
原本的問題
第一個問題來自 issue #12859:規格裡「對子孫可導覽物件觸發 traverse 事件」演算法,會先把 navigable 目前的 session history entry 設成 targetEntry,接著才檢查「targetEntry 是否不等於 navigable 目前的 session history entry」。這兩者此時已經是同一個值,這個檢查因此永遠評估為 false。結果是巢狀 iframe 在整個瀏覽器歷史 traversal 過程中,理論上該觸發 navigate 事件的條件,實際上從未被滿足過。
第二個問題來自 issue #12932,作者在除錯一個真實網站的 crash 時發現:呼叫 history.replaceState() 後,navigation.currentEntry.key 應該維持不變,但規格裡「URL 與 history 更新步驟」建立新的 session history entry 時一律指派全新 UUID 當 key,從未把被取代的舊項目的 key 複製過去。同文件替換操作因此無法保留 Navigation API 的 entry key,違反 Navigation API 自身「replace 不換 key」的語意,也讓依賴 key 判斷歷史身分的程式碼失準。issue 附上的縮版 WPT 測試直接示範了這個落差:先讀出 navigation.currentEntry.key,呼叫一次 history.replaceState() 之後再讀一次,斷言兩次的 key 相等,但依照修正前的規格重新推導步驟,第二次讀到的會是全新 UUID,斷言必然失敗。
規格改動
第一個 commit(10456f5)把比較對象從「navigable 目前的 session history entry」換成新引入的 displayedEntry(traversal 更新前,該可導覽物件實際顯示的那個歷史項目),並把 navigationType 是否為 traverse 提升為明確條件之一:
| 項目 | 修改前 | 修改後 |
|---|---|---|
| 比對目標 | targetEntry is not navigable's current history entry | targetEntry is not displayedEntry |
| origin 比對 | navigable's current history entry's document state's origin | displayedEntry's document state's origin |
| 條件式 | 未限定 navigationType | 新增「navigationType is traverse」 |
第二個 commit(9ef219c)在「URL 與 history 更新步驟」裡新增同一段補丁,只要 entryToReplace 非 null(即 historyHandling 為 replace),就把新 entry 的 navigation API key 設成被取代 entry 的 key:
If entryToReplace is non-null, then set historyEntry's
navigation API key to entryToReplace's navigation API key.同樣的補丁在演算法裡出現兩處:一處用於 replaceState() 觸發的同文件更新,一處用於一般導覽流程裡 historyHandling 為 replace 的分支,兩處都補上才能涵蓋所有 replace 路徑。
影響範圍
這是規格文字修正,不是新 API,但直接決定瀏覽器引擎該怎麼實作 Navigation API,三類對象各有具體要檢查的地方:
- 瀏覽器引擎團隊(Chromium、WebKit、Gecko):需要對照新條件式調整 traverse 事件的內部判斷,並在同文件替換路徑補上 navigation API key 的複製邏輯,兩處都要改,漏一處都無法通過對應的 WPT 測試。
- 在巢狀
<iframe>架構裡監聽navigate事件的網站:跟上規格後,這些頁面可能開始收到先前從未出現過的 traversal 事件,需要重新檢查處理邏輯是否假設了「這個事件不會來」,避免重複觸發攔截或分析邏輯。 - 依賴
navigation.currentEntry.key穩定性的程式:例如用 key 做歷史狀態去重、SPA 路由比對的邏輯,在呼叫history.replaceState()之後不能假設 key 不變,要等瀏覽器實作跟上這次修正才能依賴這個語意。
原始來源:Fire traverse navigate events at descendants whose entry changes、Preserve the navigation API key for same-document replacements