網路架站

網站裝了 SSL 憑證卻還顯示不安全?多半是混合內容,怎麼修

瀏覽器網址列跳出一個灰色警示三角形,點開之後寫著「這個網站的連線不安全」,可是網址明明是 https 開頭,SSL 憑證也早就裝好,後台看憑證還有一年多的效期。不少人第一反應是重新申請憑證、乾脆換一家憑證商,結果換完警告還是原封不動掛在那裡。

這種狀況多半不是憑證出問題,而是混合內容(mixed content)在作怪。混合內容指的是一個用 https 載入的頁面裡,夾雜著至少一項用 http 明文傳輸的資源,可能是一張圖片、一段字型、一支腳本,或是一個嵌入的地圖。瀏覽器與伺服器之間那條連線本身確實有 TLS 加密保護,但 SSL 憑證只顧得到這一條連線,管不到頁面裡引用的每一項資源各自走哪種協定。只要其中一項資源還是明文傳輸,那一部分的內容一樣可能被竊聽或竄改,瀏覽器才會把整個頁面判定為不安全。

麻煩的地方在於,混合內容不是單一原因造成的。可能出在後台的網址設定,也可能藏在幾年前寫進文章裡的舊圖片連結,還可能是外掛或頁面編輯器自己產生的樣式檔案。先搞懂瀏覽器怎麼分類這些資源,再用對的方法把確切來源揪出來,才不會把時間耗在憑感覺亂猜、亂改上。

混合內容是什麼?加密頁面裡夾雜著沒加密的資源

當使用者透過 https 瀏覽一個網站,瀏覽器與伺服器之間會用 TLS 協定把整段連線加密,防止有人在中途竊聽或竄改資料。這件事只保證「連線本身」是加密的,並不會自動檢查、也管不到這個頁面裡「引用的每一個資源」是不是同樣走加密管道。只要頁面裡有一項資源,例如圖片、字型、腳本或樣式表,是用 http:// 載入的,這個頁面就同時存在加密與未加密兩種傳輸方式,這正是「混合內容」名稱的由來,同一頁面混著 http 與 https 兩種協定。

Google 旗下的開發者網站 web.dev 用更白話的方式解釋同一件事,如果網頁的初始 HTML 是透過安全的 https 連線載入,但其他資源如圖片、影片、樣式表和指令碼卻是透過不安全的 http 連線載入,就會出現混合內容。MDN 的說明則點出這個狀態為什麼危險,因為一個含有 http 明文內容的 https 頁面只有部分加密,沒有加密的內容依舊容易遭到竊聽和中間人攻擊。換句話說,即使網址列的鎖頭本來該出現,只要有一個資源沒跟上,整頁的安全等級就被拉低到那個沒加密的資源身上。

這也是為什麼裝了憑證、網址也全面改成 https,警告卻還在的原因。SSL/TLS 保護的範圍只到「伺服器與瀏覽器之間的這條連線」,它不會回頭替頁面裡每一個舊有的資源網址做協定升級。

https 頁面只要夾雜一項用 http 傳輸的資源,整頁就會被瀏覽器判定為不安全,這就是混合內容
頁面用 https 載入,但只要有一項資源還走 http,瀏覽器就把整頁判定為不安全。

被動與主動混合內容的差異,決定警告還是直接封鎖

混合內容不是一種單一的狀態,瀏覽器會依資源可能造成的風險高低,把它分成兩大類來對待,這個分類直接決定讀者這次遇到的是「畫面還能看、只是網址列出現警告」,還是「某個功能整個壞掉、頁面跑版或按鈕沒反應」。MDN 把可以被瀏覽器自動升級協定的資源歸為一類,包括用 src 屬性載入的 <img>(含 SVG,但不含用 srcset 或 <picture> 載入的圖片)、CSS 裡的 background-image 與 border-image 等圖片屬性、以及用 src 載入的 <audio>、<video> 與 <source>。這些資源如果瀏覽器能在 https 網址找到對應版本,就會自動改用 https 重新請求。

另一類則被歸為「可封鎖內容」,凡是不屬於前一類的混合內容都算,包括 <script src>、<link href>(含樣式表)、<iframe src>、fetch()、XMLHttpRequest、CSS 裡所有用到 url() 的地方(如 @font-face、cursor)、<object data>、Navigator.sendBeacon(),以及用 srcset 或 <picture> 載入的圖片與網頁字型。這一類瀏覽器不會嘗試升級,直接擋下不載入。有個例外值得留意,如果資源網址的主機寫的是 IP 位址而不是網域名稱,原本應該被升級的資源也一樣會被直接封鎖,例如 http://93.184.215.14/image.png 這種寫法就不會被自動升級成 https,寫成網域名稱才會。另外還有「混合下載」這種特殊情形,指的是使用者在安全頁面上點了下載連結,實際檔案卻是從 http 來源抓取,瀏覽器通常會直接封鎖或跳出風險提示讓使用者自己決定要不要繼續。

被動混合內容多半被瀏覽器嘗試自動升級後仍能顯示,主動混合內容則直接被封鎖、對應功能整個失效
被動內容多半自動升級後畫面正常,主動內容則被直接封鎖、功能整個失效。

被動內容多半自動升級成 https 仍可正常載入

被動內容在現代瀏覽器裡的實際表現,多數情況下畫面看起來完全正常,圖片照樣顯示、影片照樣播放,但網址列還是會出現「不安全」的警告圖示,這是最容易被使用者或客戶回報「網站顯示不安全」的來源,卻也最容易被忽略嚴重性,因為表面上什麼都沒壞。

web.dev 說明了 Chrome 這類瀏覽器的自動升級機制,在某些情況下,Chrome 會自動升級被動式混合內容,也就是說,如果資源已經寫死成 http,但這個資源本身可以透過 https 存取,瀏覽器就會直接載入 https 版本;如果這個資源根本沒有安全版本,那項素材就不會被載入。不過即使升級成功、畫面看起來正常,大多數瀏覽器仍會在網址列標示這個頁面不安全,因為瀏覽器判斷的依據是「原始碼裡寫的網址」,不是「最後實際載入成功的版本」。這也是為什麼有人反映「圖片明明都看得到,為什麼還說不安全」的原因,問題不在畫面,而在原始碼裡那個還沒改乾淨的網址。

主動內容遭瀏覽器直接封鎖後功能整個失效

腳本、樣式表、iframe 這類主動內容之所以被評估得更危險,幾乎所有主流瀏覽器都預設直接擋下,是因為它們有權限直接介入 https 頁面的行為。web.dev 的說明點出這層風險,攻擊者可以攔截並竄改這類主動內容,藉此完全控制頁面甚至整個網站,包括顯示不同的內容、竊取使用者的密碼與其他登入憑證、竊取使用者的 session cookie,或是把使用者導向另一個網站。正因為風險這麼高,web.dev 也提到大多數瀏覽器預設就會直接封鎖這類內容,以保護使用者。

對應到實際會看到的症狀,通常不只是網址列多一個警告圖示而已,而是某個外掛功能整個不會動、頁面樣式跑掉變成沒排版的純文字、表單按下送出卻毫無反應。這種等級需要優先處理,因為它已經不是視覺上的警告,是功能真的壞了,讀者感受到的落差會比被動內容明顯得多。

瀏覽器主控台與網路面板抓出確切的 http 來源

與其憑印象猜「大概是哪張圖」,直接打開瀏覽器內建的開發者工具,把確切的網址揪出來會快得多,這也是後面每一種原因排查時共同的起手式,先定位問題、再動手改,才不會改錯地方或白忙一場。

具體的步驟大致是這樣:

  1. 開啟開發者工具,重新整理頁面,看瀏覽器自己列出的警告訊息。web.dev 提到,Chrome 偵測到混合內容或自動升級被動內容時,會把詳細訊息記錄在開發者工具的「Issues」(問題)頁籤裡;Firefox 與 Safari 則是在主控台(Console)顯示這類警告。
  2. 分辨訊息的種類,被動內容的警告通常代表「瀏覽器已經在 https 網址找到對應版本並自動升級」;主動內容的警告則代表「這個資源已經被封鎖,不會載入」。
  3. 切到網路(Network)面板,重新整理一次,找出被標紅或顯示載入失敗的請求,對照網址就能知道確切是哪一個檔案出問題。
  4. 把找到的 http 網址手動改成 https,直接貼到網址列打開看看。如果這個資源本身有 https 版本可用,通常代表只要改協定就能修好;如果打不開或跳出憑證錯誤,代表這個資源根本沒有安全版本,得換一種方式處理。

除了逐頁檢查,也可以直接在網站原始碼裡搜尋 http:// 字串,找出含有 http 網址屬性的標籤。web.dev 特別提醒,如果網站是透過內容管理系統發布的,發布頁面時有可能自動插入了不安全的網址連結,這時得回到內容管理系統裡去找出並修正,不是改前台顯示就能解決。MDN 的修復指南也補充了兩種驗證方式:把瀏覽器設定成直接封鎖所有混合內容再測試頁面是否還能正常運作(Safari 預設就是如此),或使用能遞迴掃描整站的爬蟲工具,一次找出所有含有不安全連結的頁面,而不是一頁一頁手動翻。

Chrome 把混合內容訊息記在開發者工具的 Issues 頁籤,Firefox 與 Safari 顯示在主控台,Network 面板會標紅失敗的 http 請求
Chrome 記在 Issues 頁籤,Firefox 與 Safari 顯示在主控台,Network 面板則把失敗的 http 請求標紅。

WordPress 網址設定殘留 http,前台其餘頁面已是 https

最基礎、也最常被漏檢的一種原因,出在後台「網站位址」這類設定本身沒有真的改成 https。WordPress 後台的「設定」底下有「WordPress 網址(URL)」與「網站網址(URL)」兩個欄位,這兩個值是系統產生連結、判斷自己身分時的基準。只要其中一個還留著 http:// 開頭,或是網域版本(有無 www)跟實際使用者連進來的版本不一致,系統自己輸出的資源路徑就會對不齊,連結、圖片、腳本的網址都可能因此帶著錯誤的協定或網域被印出來。

這個設定值之所以要優先檢查,理由很直接,如果連系統自己認定的網址都還是 http,後面每一個環節都會被它拖著跑,不管伺服器和憑證裝得多好,系統輸出的每一筆新連結都可能繼續帶著舊協定。這是遷移到 https 之後最先該確認的一步,而且改起來也最快,只是兩個欄位的設定。

不過只改這裡通常還不夠。它能解決的是「接下來新產生的內容」,例如系統之後重新輸出的連結會照新設定走,但改不了「已經存進資料庫裡的舊網址」。那些網址是過去某個時間點就寫死存好的,不會因為後台設定改了就自動跟著變。

文章頁面與小工具裡貼著舊版 http 圖片連結

第二種常見來源出在內容本身,文章、頁面、選單項目、小工具設定、留言區裡的嵌入內容,都可能貼著網站還是 http 時代就寫進資料庫的圖片網址、影片嵌入碼或外部連結。就算後台網址設定已經改好、憑證也裝妥,這些舊資料不會自動被改寫,一樣會在頁面產生時原封不動吐出 http 網址,這是遷移後最大宗、也最需要「全面搜尋」而不是「單篇修改」的類型。

會藏著舊網址的地方不只文章內文,自訂 HTML 區塊、選單項目、小工具設定裡都可能出現,只檢查文章本身容易漏掉。混合內容的成因,本質上是資源以絕對網址寫死 http://,而不是使用相對路徑或明確的 https:// 寫法。MDN 的修復建議也直接點出這個方向:把所有引用自己網域資源的連結,改成相對連結或 https 連結;如果用的是其他網站的資源,優先改用該網站的 https 版本。這也解釋了為什麼「絕對網址」比「相對網址」更容易出這個問題,相對路徑本來就不帶協定,遷移時自然跟著現在的協定走,絕對網址卻把當初寫下的協定原封不動保留下來。

至於要逐篇手動改,還是用批次工具一次處理,取決於篇數規模。文章數量少,直接進去把幾個網址改掉就好;篇數一多,逐篇檢查既耗時又容易漏,才需要考慮批次處理。批次處理有它自己的風險,尤其是直接對資料庫做字串取代這件事。

佈景主題、外掛與頁面編輯器產出的 CSS 藏著 http

跟前一節「內容裡的舊網址」不同,這裡的問題出在系統自動產生的資源路徑:佈景主題設定裡的 Logo 網址、自訂字型連結、頁首頁尾的自訂程式碼區塊,外掛設定欄位裡的地圖金鑰、追蹤碼、自訂樣式,以及視覺化頁面編輯器把版面存成獨立 CSS 或 JS 檔案之後產生的連結,這些都不是編輯者手動貼進去的,而是程式邏輯自己組出來的。如果組出來的邏輯本身還是用 http 起頭,單篇修改內容完全碰不到它,得回到該元件自己的設定裡才能修正。

這一類最容易讓人搞錯排查方向的地方在於,即使文章與小工具都已經確認乾淨,主控台卻還是報錯,這時該懷疑的方向就是程式邏輯自動產出的樣式或腳本,而不是繼續在文章編輯器裡翻找。要回頭檢查主題選項、外掛設定,或是頁面編輯器本身的設定值。

有些頁面編輯器把整份版面編譯成獨立的 CSS 檔案,再用連結的方式載入到頁面上。這種情況下,改了網址設定或把資源改成 https,通常還需要讓編輯器「重新產生」一次那些編譯出來的檔案,單純改設定並不會自動觸發重新編譯,舊的編譯結果還是留著當初寫下的舊協定,這一步常常被漏掉。

外部字型、地圖與嵌入影片仍提供純 http 版本

第三種類型比較特別,問題不在自己的主機上,而是外部服務商提供的資源,像是字型服務、地圖服務、社群分享按鈕、影片嵌入,或是第三方的追蹤與分析腳本。多數服務商現在都同時支援 https,通常只要把嵌入碼裡的 http 改成 https 就能解決,不需要動到功能本身。

要判斷某項外部資源支不支援加密,最快的方法就是前面提過的那招,先把網址裡的 http 直接改成 https,貼到網址列打開看看,能正常顯示就代表這個服務商已經提供加密版本,直接把嵌入碼裡的協定改掉即可。但也有少數老舊、不再維護的服務只剩 http 版本,這種情況沒辦法單靠改協定解決。面對這種情況,實務上的處理順序通常是:優先使用對方網站的 https 版本;如果沒有,可以考慮聯絡對方詢問能不能提供加密版本;如果授權條件允許,也可以考慮把這份內容下載下來,自己代管在自己的網站上;真的都行不通,最後的選項才是把這個資源整個從網站上移除。換句話說,遇到堅持只提供 http 的外部服務,能選的路只剩下換掉它、拿掉這個嵌入,或在合法授權下自己接手代管,沒有第四種選擇。

CDN 或反向代理誤判連線協定,快取又讓舊版本重現

有一種情況最容易讓人抓狂,明明已經全面改成 https,前面幾節能檢查的地方也都確認過了,主控台卻還是間歇性跳出警告。這通常是兩種機制造成的,而且往往同時存在,得分開來看。

第一種是傳輸層的問題。網站如果架在 CDN 或反向代理後面,伺服器端有時候會誤判使用者這次連線用的協定。原因是伺服器判斷連線是不是加密的,常見做法是去看請求標頭裡代理服務加註的協定資訊,而不是直接看那條原始連線本身,一旦這段標頭資訊沒有正確轉達,系統就可能誤以為這次連線是 http,進而自己又輸出了 http 開頭的連結,即使訪客實際上是用 https 連進來的。

第二種是快取層的問題。快取機制,不管是網站本身的快取外掛、主機端的快取,還是 CDN 的快取,都可能還留著修正之前產生的舊版頁面。就算來源端已經修好,訪客實際拿到的仍然是先前快取住的舊內容,看起來就像「怎麼改都改不好」。這幾層快取彼此獨立,清了一層不代表其他層也一起清掉了,得逐層確認才行。要分辨這次遇到的是傳輸層問題還是快取層問題,一個簡單的判斷法是打開無痕視窗重新整理,或是想辦法繞過 CDN 直接對原站發出請求測試,如果繞過之後就正常,問題多半出在中間那層,不在原站本身。

CDN 或反向代理可能誤判連線協定而輸出 http,各層快取又留住修正前的舊版本,讓混合內容反覆出現
傳輸層可能誤判協定而輸出 http,快取層則讓修正前的舊版本繼續被送出,兩者常同時存在。

批次搜尋取代前,序列化資料要先做好備份

當混合內容的來源是內容裡大量散落的舊網址,逐篇手動改一遍並不切實際,這時候會想到用批次的方式一次處理完。不過直接對資料庫做「找字串、換字串」這種操作,有一個很容易被忽略的風險,資料庫裡有些欄位存的不是給人看的純文字,而是程式讀取用的結構化格式,這種格式裡通常會夾帶一段記錄「資料長度」的數字。單純把字串內容換掉,卻沒有同步更新那段長度資訊,會讓這筆資料整個讀取失敗,對應的頁面或設定也可能跟著壞掉。這是批次處理最容易踩的雷,後果比手動漏改幾個網址嚴重得多。

因此動手批次取代之前,有兩件事一定要先做。第一是先把整個網站與資料庫完整備份一份,萬一出狀況才有得回復。第二是先用預覽模式看一次這次取代會影響到多少筆資料,確認範圍在預期之內,再真的執行取代動作,不要跳過預覽直接下手。

另外,取代用的搜尋字串要寫得夠精確,包含通訊協定與有無 www 的版本都要對齊,避免連不該動的外部連結也被誤改。文章裡如果只是連到別的網站的一般超連結,本身不算混合內容,不需要跟著被取代掉,搜尋字串寫得太寬鬆,反而會把這類正常連結一起改壞。

清除快取後在無痕視窗逐頁重新驗證

修完不代表就結束了,還得確認真的修好,而不是只看首頁網址列有沒有出現鎖頭圖示就結案。很多時候是來源已經修正,但看到的人拿到的還是快取住的舊版本,或者只測了首頁,某個特定的頁面模板、某張表單、某個嵌入區塊其實還在報錯。

驗證之前要先把該清的快取都清乾淨,網站本身的快取機制、主機端快取、CDN 快取,這幾層各自獨立,清一層不代表其他層也一起清了。清完之後,用無痕視窗或重新開一個全新的瀏覽器工作階段重新整理,避免自己看到的其實是本機瀏覽器留存的舊版本,而不是伺服器現在實際輸出的內容。

也不能只測首頁。舊文章、有表單的頁面、結帳或會員登入這類流程頁、手機版排版、有外部嵌入內容的頁面,都要各別打開主控台確認一次,因為混合內容常常只出現在某一種頁面模板或某個特定區塊,不是全站均勻分布。web.dev 建議修正問題後回頭查看原本出現錯誤的那個頁面,確認錯誤真的不再顯示;MDN 也提到可以把瀏覽器設定成封鎖所有混合內容,再測試整個網站是否還能正常運作,或用能遞迴掃描整站的爬蟲工具做一次全面確認,而不是只靠肉眼逐頁翻找。

混合內容排查修復流程:先用開發者工具定位、逐層檢查五類來源、批次取代前先備份、清快取後逐頁驗證、日後定期重掃
先定位再逐層排查,批次取代前先備份、改完清快取逐頁驗證,最後養成定期重掃的習慣。

每次新增外掛或服務後,養成重新掃描的習慣

混合內容不是修一次就永久免疫的狀態。之後任何時候新增一個外掛、串接一項新的外部服務,例如地圖、金流、客服嵌入或追蹤碼,或是換一次佈景主題、升級頁面編輯器版本,都可能重新帶入寫死 http 的資源,前面花時間修好的東西又悄悄破了一個洞。

比較實際的做法,是把檢查混合內容變成日常維運習慣的一部分,而不是等到訪客回報或客戶抱怨才處理。新增任何會嵌入外部資源的功能之前,先確認對方服務商有沒有提供 https 版本;自訂程式碼,不管是頭尾的腳本還是自訂 CSS,一律用 https 或相對路徑寫,不要圖方便先寫死 http 再說。MDN 的修復指南把「所有內容一律用 https 提供」列為避免混合內容問題的根本策略,日後新增資源時直接使用相對連結或明確的 https 連結,從源頭就避免重新出現混合內容,會比事後才回頭抓漏省事得多。定期,例如每次大幅更版或新增服務之後,重新走一次前面提過的開發者工具檢查流程,而不是假設「當初修好了就會一直是好的」。

混合內容處理的,其實是同一件事,網站裡每一個資源的協定,不管是後台設定、內容本身、佈景主題與外掛,還是第三方服務,一路到傳輸與快取層,都得一致對齊到 https,少一處沒跟上,警告就會找上門。把排查的順序記住,把檢查變成習慣而不是救火,那個灰色警示三角形就不會三不五時又跳出來提醒你少改了哪一處。

資料來源
  1. 混合內容 (Mixed content) — MDN Web Docs
  2. Mixed content — MDN Web Docs
  3. What is mixed content? — web.dev
  4. Fixing mixed content — web.dev
  5. How to fix a website with mixed content — MDN Web Docs