[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"post-\u002Fblog\u002Fself-host-cdn-assets":3},{"id":4,"title":5,"body":6,"category":375,"date":376,"description":377,"draft":378,"extension":379,"meta":380,"navigation":317,"path":381,"readingTime":161,"seo":382,"stem":383,"tags":384,"__hash__":388},"blog\u002Fblog\u002Fself-host-cdn-assets.md","弱點掃描的提醒：為什麼不要用 CDN 的 @latest",{"type":7,"value":8,"toc":362},"minimark",[9,13,18,21,55,58,61,66,69,72,80,85,98,101,105,108,176,189,193,196,204,207,228,235,238,241,252,259,271,278,281,284,338,341,344,358],[10,11,12],"p",{},"公司專案做弱點掃描時，報告列出一個中風險項目：「跨網域設定錯誤」。我一開始以為是後端的 CORS 設定，追查後才發現，跟前端載入的一個圖示字型有關。",[14,15,17],"h2",{"id":16},"問題出在一行-import","問題出在一行 @import",[10,19,20],{},"專案為了使用 Material Design Icons，寫了一個 CSS 檔，從 CDN 載入圖示：",[22,23,28],"pre",{"className":24,"code":25,"language":26,"meta":27,"style":27},"language-css shiki shiki-themes github-dark","@import url(\"https:\u002F\u002Fcdn.jsdelivr.net\u002Fnpm\u002F@mdi\u002Ffont@latest\u002Fcss\u002Fmaterialdesignicons.min.css\");\n","css","",[29,30,31],"code",{"__ignoreMap":27},[32,33,36,40,44,48,52],"span",{"class":34,"line":35},"line",1,[32,37,39],{"class":38},"snl16","@import",[32,41,43],{"class":42},"sDLfK"," url",[32,45,47],{"class":46},"s95oV","(",[32,49,51],{"class":50},"sU2Wk","\"https:\u002F\u002Fcdn.jsdelivr.net\u002Fnpm\u002F@mdi\u002Ffont@latest\u002Fcss\u002Fmaterialdesignicons.min.css\"",[32,53,54],{"class":46},");\n",[10,56,57],{},"這行寫起來很方便，也很常見，但它有幾個問題。",[14,59,60],{"id":60},"第三方資源的風險",[62,63,65],"h3",{"id":64},"_1-你的網站依賴別人的伺服器","1. 你的網站依賴別人的伺服器",[10,67,68],{},"CDN 上的 CSS 和字型是從另一個網域載入的。只要 CDN 服務異常，或使用者的網路環境擋掉該網域，圖示就全部顯示不出來。",[10,70,71],{},"對於網路環境受限的單位，這點特別容易發生，而且開發時完全看不出來。",[62,73,75,76,79],{"id":74},"_2-latest-代表內容隨時會變","2. ",[29,77,78],{},"@latest"," 代表內容隨時會變",[10,81,82,84],{},[29,83,78],{}," 不是固定的檔案，而是「目前最新的版本」。這代表：",[86,87,88,92,95],"ul",{},[89,90,91],"li",{},"套件發新版時，網站的內容會在你沒有部署的情況下改變。",[89,93,94],{},"如果新版調整了圖示名稱，畫面可能突然壞掉，程式碼卻完全沒動過。",[89,96,97],{},"上游被植入惡意內容時，你的網站也會直接載入。",[10,99,100],{},"CSS 雖然不能直接執行 JavaScript，但第三方內容能影響你的畫面，本身就是一個信任問題。",[62,102,104],{"id":103},"_3-無法做完整性檢查","3. 無法做完整性檢查",[10,106,107],{},"瀏覽器有 Subresource Integrity（SRI），可以檢查檔案內容是否被改過：",[22,109,113],{"className":110,"code":111,"language":112,"meta":27,"style":27},"language-html shiki shiki-themes github-dark","\u003Clink\n  rel=\"stylesheet\"\n  href=\"https:\u002F\u002Fcdn.example.com\u002Flib@1.2.3\u002Fstyle.css\"\n  integrity=\"sha384-...\"\n  crossorigin=\"anonymous\"\n>\n","html",[29,114,115,124,137,148,159,170],{"__ignoreMap":27},[32,116,117,120],{"class":34,"line":35},[32,118,119],{"class":46},"\u003C",[32,121,123],{"class":122},"s4JwU","link\n",[32,125,127,131,134],{"class":34,"line":126},2,[32,128,130],{"class":129},"svObZ","  rel",[32,132,133],{"class":46},"=",[32,135,136],{"class":50},"\"stylesheet\"\n",[32,138,140,143,145],{"class":34,"line":139},3,[32,141,142],{"class":129},"  href",[32,144,133],{"class":46},[32,146,147],{"class":50},"\"https:\u002F\u002Fcdn.example.com\u002Flib@1.2.3\u002Fstyle.css\"\n",[32,149,151,154,156],{"class":34,"line":150},4,[32,152,153],{"class":129},"  integrity",[32,155,133],{"class":46},[32,157,158],{"class":50},"\"sha384-...\"\n",[32,160,162,165,167],{"class":34,"line":161},5,[32,163,164],{"class":129},"  crossorigin",[32,166,133],{"class":46},[32,168,169],{"class":50},"\"anonymous\"\n",[32,171,173],{"class":34,"line":172},6,[32,174,175],{"class":46},">\n",[10,177,178,179,181,182,184,185,188],{},"但 SRI 比對的是固定內容的雜湊值，",[29,180,78],{}," 的內容會變，一更新就會比對失敗。而且用 CSS ",[29,183,39],{}," 載入時，也沒有地方可以寫 ",[29,186,187],{},"integrity","。",[14,190,192],{"id":191},"解法改成自己託管","解法：改成自己託管",[10,194,195],{},"修法很單純：把 CSS 和字型檔放進專案，由自己的伺服器提供。",[22,197,202],{"className":198,"code":200,"language":201,"meta":27},[199],"language-text","src\u002Fassets\u002F\n├── customized\u002F\n│   └── materialdesignicons.min.css\n└── fonts\u002F\n    ├── materialdesignicons-webfont.woff2\n    └── materialdesignicons-webfont.woff\n","text",[29,203,200],{"__ignoreMap":27},[10,205,206],{},"入口檔改成載入本地的 CSS：",[22,208,212],{"className":209,"code":210,"language":211,"meta":27,"style":27},"language-js shiki shiki-themes github-dark","\u002F\u002F main.js\nimport '.\u002Fassets\u002Fcustomized\u002Fmaterialdesignicons.min.css'\n","js",[29,213,214,220],{"__ignoreMap":27},[32,215,216],{"class":34,"line":35},[32,217,219],{"class":218},"sAwPA","\u002F\u002F main.js\n",[32,221,222,225],{"class":34,"line":126},[32,223,224],{"class":38},"import",[32,226,227],{"class":50}," '.\u002Fassets\u002Fcustomized\u002Fmaterialdesignicons.min.css'\n",[10,229,230,231,234],{},"再把 CSS 裡 ",[29,232,233],{},"@font-face"," 的路徑指到本地字型。這樣所有資源都來自同一個來源，版本也固定了，要更新時才手動更換。",[14,236,237],{"id":237},"代價是什麼",[10,239,240],{},"自己託管不是完全沒有成本：",[86,242,243,246,249],{},[89,244,245],{},"字型檔不小，專案也要多負責這些資源的快取和傳輸。",[89,247,248],{},"版本不會自動更新，要自己留意有沒有安全修正。",[89,250,251],{},"CSS 檔會進版本控制。這個檔案就有兩萬多行。",[10,253,254,255,258],{},"如果重新做一次，我會改用 npm 安裝固定版本的 ",[29,256,257],{},"@mdi\u002Ffont","：",[22,260,262],{"className":209,"code":261,"language":211,"meta":27,"style":27},"import '@mdi\u002Ffont\u002Fcss\u002Fmaterialdesignicons.min.css'\n",[29,263,264],{"__ignoreMap":27},[32,265,266,268],{"class":34,"line":35},[32,267,224],{"class":38},[32,269,270],{"class":50}," '@mdi\u002Ffont\u002Fcss\u002Fmaterialdesignicons.min.css'\n",[10,272,273,274,277],{},"打包工具會幫忙處理字型路徑，版本寫在 ",[29,275,276],{},"package-lock.json","，比手動複製檔案好維護。如果只用到少量圖示，也可以改用 SVG，只打包需要的部分。",[10,279,280],{},"現代瀏覽器都支援 woff2，通常只保留 woff2 就夠了，不需要把 eot、ttf 都帶上。",[14,282,283],{"id":283},"怎麼檢查專案裡還有沒有類似問題",[22,285,289],{"className":286,"code":287,"language":288,"meta":27,"style":27},"language-bash shiki shiki-themes github-dark","# 找出程式碼裡的外部資源\ngrep -rnE \"https?:\u002F\u002F\" src index.html\n\n# 特別留意沒有固定版本的引入\ngrep -rn \"@latest\" src index.html\n","bash",[29,290,291,296,313,319,324],{"__ignoreMap":27},[32,292,293],{"class":34,"line":35},[32,294,295],{"class":218},"# 找出程式碼裡的外部資源\n",[32,297,298,301,304,307,310],{"class":34,"line":126},[32,299,300],{"class":129},"grep",[32,302,303],{"class":42}," -rnE",[32,305,306],{"class":50}," \"https?:\u002F\u002F\"",[32,308,309],{"class":50}," src",[32,311,312],{"class":50}," index.html\n",[32,314,315],{"class":34,"line":139},[32,316,318],{"emptyLinePlaceholder":317},true,"\n",[32,320,321],{"class":34,"line":150},[32,322,323],{"class":218},"# 特別留意沒有固定版本的引入\n",[32,325,326,328,331,334,336],{"class":34,"line":161},[32,327,300],{"class":129},[32,329,330],{"class":42}," -rn",[32,332,333],{"class":50}," \"@latest\"",[32,335,309],{"class":50},[32,337,312],{"class":50},[10,339,340],{},"也可以打開 DevTools 的 Network 面板，篩選第三方網域，看實際載入了哪些資源。",[14,342,343],{"id":343},"這次學到的事",[86,345,346,349,352],{},[89,347,348],{},"弱點掃描的項目不一定在你想的地方，要追到實際的來源。",[89,350,351],{},"方便的一行引入，背後是對第三方的信任。上線前要想清楚值不值得。",[89,353,354,355,357],{},"不要在正式環境使用 ",[29,356,78],{},"，固定版本才能掌握網站實際載入的內容。",[359,360,361],"style",{},"html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .s4JwU, html code.shiki .s4JwU{--shiki-default:#85E89D}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}",{"title":27,"searchDepth":126,"depth":126,"links":363},[364,365,371,372,373,374],{"id":16,"depth":126,"text":17},{"id":60,"depth":126,"text":60,"children":366},[367,368,370],{"id":64,"depth":139,"text":65},{"id":74,"depth":139,"text":369},"2. @latest 代表內容隨時會變",{"id":103,"depth":139,"text":104},{"id":191,"depth":126,"text":192},{"id":237,"depth":126,"text":237},{"id":283,"depth":126,"text":283},{"id":343,"depth":126,"text":343},"quality","2026-03-06","弱點掃描回報中風險的跨網域設定問題，追查後發現來自一行 CDN 引入。記錄第三方資源的風險，以及改成自己託管的做法與取捨。",false,"md",{},"\u002Fblog\u002Fself-host-cdn-assets",{"title":5,"description":377},"blog\u002Fself-host-cdn-assets",[385,386,387],"Security","CDN","Vite","14hMgJGFSshCQERyEEfR1WtjTbtpGWvrvPdpauaVUVY"]