先記結論:井號後面適合放金鑰,是因為瀏覽器發 HTTP 時會丟掉 fragment,不是因為多了一套加密協定。核對方法是對照網址列和 Network 的請求行。把完整連結貼進聊天、工單或瀏覽器歷程時,這一層保護對「看見整串的人」不再成立。
一句話先回答搜尋意圖
URL 的井號後面適合放金鑰,因為 RFC 9110 規定:請求的目標 URI 不含 fragment。瀏覽器向伺服器要文件時,會去掉 # 及之後的部分。託管方、CDN 和存取紀錄按設計只能看到路徑和問號後的查詢,看不到井號後面的金鑰。
這篇文章給開發者、維運,以及要把一次性連結發給同事之前想先分清「伺服器看不見」和「聊天紀錄看不見」的人。它不代替 閱後即焚 的操作說明,也不重複「怎麼建立一條連結」。你要帶走的是:問號和井號各自進哪一層、用 Network 怎麼對照、以及把整串 URL 轉發出去時會丟掉哪一層保護。
問號進 HTTP,井號預設留在分頁
同一條網址可以同時帶查詢和片段。形態是 https://example.com/s.html?id=abc#key。問號後面的 id=abc 是 query,井號後面的 key 是 fragment。瀏覽器解析時,fragment 從第一個 # 一直到字串結束;更前面的元件裡如果要寫字面量井號,必須寫成 %23,否則解析會提前截斷。
發文件請求時,瀏覽器組裝的是 Host、路徑和查詢。MDN 對 URI fragment 的說明很直白:片段在請求資源時不會發給伺服器,由用戶端在拿到文件之後再處理。HTML 裡它常用來捲到某個 id;在閱後即焚裡,它被借來承載本機解密要用的金鑰。
因此「伺服器只存密文」如果成立,前提必須是:定位用的編號走 query,解密金鑰走 fragment,並且頁面指令碼沒有把 location.hash 再寫進後續請求。少任何一步,井號就只是裝飾。
規範寫的是預設行為,不是額外加密
RFC 3986 §3.5 把 fragment 定義成對「主資源之上的次級資源」的識別,由用戶端依文件類型解釋。RFC 9110 §7.1 接著寫明:目標 URI 排除參照裡的 fragment,因為片段識別留給用戶端處理。兩份文件疊在一起,意思是:井號後面不是 HTTP 請求目標的一部分。
這也解釋了為什麼存取紀錄裡找不到 #。不是維運把欄位藏起來了,而是請求行裡本來就沒有這一段。想用伺服器端紀錄統計「使用者點了哪個錨點」,本身就會落空;想用同一份紀錄證明「金鑰沒進伺服器」,卻正好對得上。
Referer 也按同一原則處理。MDN 對 Referer 的說明是:這個標頭可以帶來源、路徑和查詢,但不得包含 URL fragment 和使用者名稱密碼。Referrer Policy 在「把 URL 收成 Referer」時會先把 fragment 清空。所以從閱讀頁再去抓一張圖、一個指令碼,預設也不會把井號後面的金鑰帶給第三方主機。這是瀏覽器的預設剝離,不是網站自己發明的協定。
頁面指令碼仍然讀得到片段。MDN 的 URL.hash 就是 # 加上片段本身;目前頁面也可以用 location.hash。閱讀頁正是靠這個介面取出金鑰,再呼叫 Web Crypto 做 AES-256-GCM 解密。同一介面也能被誤用:指令碼可以把 hash 拼進 query,或寫進分析上報。規範擋的是「瀏覽器預設發出的請求行」,擋不住「頁面自己讀出來再送出」。
不要用真實口令、API Key 或未遮蔽的連線字串做實驗。準備一句只有這次才有的測試文字。核對的是請求行和請求體,不是把憑證再暴露一遍。
用 Network 對照網址列和請求行
口號不能當證據。能當場做完的檢查,是開啟開發者工具的 Network,把網址列整串和每一條請求的 URL 放在一起看。上一篇寫過用金絲雀搜明文;這一篇只盯 fragment:井號後面那一段,應出現在網址列,不應出現在請求行。
- 開啟閱讀頁,確認網址列是
s.html?id={id}#{key}這種形態:query 只有編號,金鑰只在井號後面。 - 開啟 Network,勾選 Preserve log,篩選器先用 All。文件請求、後續介面、分析上報都不要先藏起來。
- 把井號後面的金鑰當成金絲雀,貼進篩選器。有紅色命中就停下來讀那條請求:實作把 fragment 誤寫成了 query,或指令碼讀出後又送了出去。
- 沒有命中時,再逐條開啟文件請求和
/secrets一類介面,對照請求行是否只含id,請求體是否沒有原文。
建立側也可以做一次。本機用 AES-256-GCM 加密後,出站欄位按設計只有密文、過期時間和閱讀次數;亂數金鑰應只出現在隨後拼好的分享連結裡,不應出現在 POST 的 JSON 裡。FastPwd 的閱後即焚按這條邊界實作:建立和閱讀都開啟即用,沒有帳號。承諾仍然只是承諾,以你這一次在 Network 裡看到的為準。
把 %23 和真正的 # 分清。請求行裡出現 %23 後面的字元,表示這段被當成路徑或查詢送出去了,伺服器看得到。只有未編碼的 # 才開始 fragment。有人把金鑰寫成 ?key= 或 /%23key,看起來像「藏在井號裡」,對紀錄來說已經是明文查詢。
頁面指令碼和分析上報仍能讀走 hash
預設 HTTP 請求不含 fragment,不等於「這個分頁裡沒有任何指令碼能看見它」。目前文件的 JavaScript 擁有 location.hash。閱讀頁必須讀它,才能在本機解密;分析指令碼如果把 location.href 整串上報,金鑰就會出現在統計請求的 query 或 body 裡。
所以金絲雀搜尋不能只看業務介面。單獨篩選造訪統計路徑,開啟 query 與 body。頁面標題和路徑可以出現;井號後面的金鑰、待測密碼和清洗前原文不應出現。FastPwd 的造訪統計走網站自己的 tracker,localhost 預設不送出;即便如此,你仍應用同一枚金絲雀確認上報裡沒有 fragment 金鑰。
擴充功能、企業 DLP 和某些「AI 瀏覽器」也能讀網址列全文。它們不走你正在看的那條 HTTP 請求行,Network 面板不會替你證明它們沒讀。把檢查收成一句能對同事說的話:「我搜過請求行和上報,沒有井號後的金鑰。」不要把它說成「這台電腦上沒有任何軟體看見過這段 hash」。
什麼時候這層保護會失效
fragment 擋的是「這次文件請求把金鑰交給託管方」。下面這些場景裡,看見整串 URL 的人已經等於持有金鑰,規範幫不上忙。
第一,把完整連結貼進聊天、郵件或工單。訊息本文裡的字串包含井號後面的部分。對方的用戶端、群組歷程、工單搜尋和將來的匯出,都能看到整串。對伺服器來說金鑰仍可能沒進 HTTP;對「能開啟聊天紀錄的人」來說,保護已經沒了。完整 URL 是持有即用的憑證。
第二,瀏覽器歷程和同步。網址列顯示過的 # 片段,通常會進本機歷程。開了同步的瀏覽器,還可能把同一條紀錄送到你的其他裝置。關掉分頁不等於清掉歷程。
第三,重新導向可能帶走 fragment。目標 Location 如果自己沒有片段,不少瀏覽器會把原來的 fragment 接到新網址後面。金鑰於是出現在你以為「已經跳到安全頁」的網址列裡。閱讀頁若再被重新導向到統計網域或登入網域,先用 Network 看最終請求行,再看網址列還剩什麼。
第四,連結預覽和安全掃描會去抓「井號前面」那一段。聊天軟體產生預覽時,依規範不會把 fragment 發給被預覽的主機,但它們會請求 s.html?id=…。如果實作在第一次 GET 文件或第一次 GET 密文時就焚燬,預覽機器人可能先於接收方把次數用掉。接收方點開時只看到「已被焚燬」,金鑰其實從未交給預覽主機——燒掉的是閱讀次數,不是 fragment 機密性。
FastPwd 的閱讀頁把這兩步拆開:開啟頁面時只查詢密文還在不在,不取走密文;接收方點「開啟並檢視」之後,才按編號取密文,並用井號後的金鑰在本機解密。建立頁和閱讀頁都不需要帳號。預覽機器人通常只會抓到達頁,不會替你點按鈕;這能降低「預覽即焚燬」的機率,但不能保證所有掃描器都停在到達頁。次數仍然是伺服器端的計數,不是 fragment 的一部分。
| 誰在看 | 看不看得到 # 後的金鑰 | 這一層還成不成立 |
|---|---|---|
| 文件請求的請求行 | 預設看不到 | 成立;用 Network 對照 |
| 託管方存取紀錄 / CDN | 預設看不到 | 成立,前提是金鑰沒進 query |
| Referer 發往第三方 | 預設看不到 | 成立;規範會去掉 fragment |
| 頁面指令碼 / 分析上報 | 能讀 location.hash |
看實作;用金絲雀搜 body |
| 聊天、郵件、工單本文 | 看得到整串 | 對轉發對象不成立 |
| 瀏覽器歷程 / 同步 | 通常看得到 | 對能開啟這台瀏覽器的人不成立 |
| 連結預覽機器人 | 依規範拿不到 fragment | 可能消耗閱讀次數 |
你能證明什麼,不能證明什麼
這次檢查能支援的結論很窄。能支援:在你使用的這個瀏覽器、這一次開啟裡,文件請求和已觀察到的介面請求行沒有帶上井號後面的金鑰;建立請求的 body 裡沒有原文。
不能支援:群組管理員沒儲存過完整連結;瀏覽器擴充功能沒讀網址列;對方開啟後不會截圖;密文被取走之後不會再出現在別的備份裡。閱後即焚刪除的是伺服器上的密文,不是已經解密到螢幕上的明文,也不是聊天紀錄裡的整串 URL。
也不要把 fragment 理解成「只有接收方能開啟」。誰持有 ?id=…#金鑰 整串,誰就能在公開的閱讀頁解密。閱讀頁不設登入門禁,這是設計:對方不必先註冊。代價是整串必須按憑證來保管,而不是按「一般可轉發的文件連結」來保管。
常見問題
URL 井號後面的內容會發給伺服器嗎?
預設不會。RFC 9110 寫明目標 URI 不含 fragment,瀏覽器發文件請求時會去掉井號及之後的部分。伺服器、反向代理和存取紀錄按設計只能看到路徑和問號後的查詢。若請求行裡出現了井號後的金鑰,那是實作錯誤,不是規範例外。
為什麼不把解密金鑰放在問號後面?
問號後的查詢會寫進 HTTP 請求行。託管方、CDN 和存取紀錄都能看見。金鑰一旦進 query,所謂「伺服器端只存密文」在紀錄這一層就不成立。編號可以放在 query 裡,因為它本來就要用來取密文;金鑰不行。
把帶井號的完整連結貼進聊天還安全嗎?
對伺服器仍然看不到金鑰;對聊天紀錄、工單和瀏覽器歷程不再成立。完整 URL 是持有即用的憑證,誰拿到整串,誰就能在閱讀頁解密。需要更高隔離時,把「帶 id 的連結」和「井號後的金鑰」分兩個管道給,而不是把整串丟進同一個群組。
開啟閱後即焚閱讀頁需要登入嗎?
不需要。閱讀頁對接收方公開,靠問號裡的 id 取密文、靠井號裡的金鑰在本機解密。FastPwd 沒有帳號和密碼庫,建立與閱讀都是開啟即用。登入門禁解決不了 fragment 的邊界問題,也不應被寫進這一層檢查。
用一條無害測試連結當場核對
若你希望用一個把 query 和 fragment 拆開的頁面來練習,可以從 FastPwd 的閱後即焚開始。開啟即可使用,無需註冊。寫入一句可丟棄的測試文字,產生本機加密後的連結。建立請求裡你應只看到密文和次數、過期一類欄位;分享連結才是 s.html?id={id}#{key}。
用這串連結開啟閱讀頁。網址列應同時有問號和井號。Network 裡,文件請求和狀態查詢應只帶 id。點「開啟並檢視」之後,取密文的請求同樣不應帶上井號後面的金鑰;明文只應出現在本機解密之後的頁面上。演算法是 AES-256-GCM,計算走 Web Crypto。單條文字上限 32 KB;整份檔案請改用 檔案加密盒,單檔案不超過 5 GB,輸出 .lock / .enc。
這些練習的目的不是證明某一個網站「絕對安全」,而是讓你分清兩層事實。第一層:託管方依規範看不到 fragment。第二層:看見完整連結的人等於持有金鑰。換任何聲稱「金鑰在井號後面」的頁面,步驟不變:對照網址列和請求行,再單獨看分析上報,最後問自己這串 URL 會不會進聊天紀錄。
下次傳送前記住三件事。第一,編號可以進 query,金鑰只能進 hash。第二,請求行乾淨,只說明伺服器沒拿到金鑰。第三,完整連結按憑證保管,不要當一般文件連結轉發。能重複的觀察,才值得寫進你們自己的交接說明。
延伸閱讀
上一篇把「明文有沒有離開這個分頁」收成可重複的 Network 檢查,適合和本文對照著做:瀏覽器本地加密怎麼核對明文沒有離開本機。本文只把問號和井號拆開,並寫清把完整連結轉發出去時會失效的那一層。